Skip to content
This repository was archived by the owner on Dec 28, 2022. It is now read-only.

Commit f234a69

Browse files
committed
Init
0 parents  commit f234a69

18 files changed

+522
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ssh-crypt*
2+
vendor

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Maciej Filipiak
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
CURRENT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
2+
VERSION := $(shell git describe --tag --always --long)
3+
4+
build: ssh-crypt ssh-crypt.exe ssh-crypt_darwin
5+
6+
ssh-crypt: $(wildcard *.go) $(wildcard **/*.go)
7+
go get ./...
8+
go get github.com/stretchr/testify/assert
9+
go test -v github.com/suside/ssh-crypt/lib
10+
go build -o ssh-crypt -i -ldflags "-s -w -X main.version=${VERSION}"
11+
12+
ssh-crypt.exe: ssh-crypt
13+
env GOOS=windows go build -o ssh-crypt.exe -i -ldflags "-s -w -X main.version=${VERSION}"
14+
15+
ssh-crypt_darwin: ssh-crypt
16+
env GOOS=darwin go build -o ssh-crypt_darwin -i -ldflags "-s -w -X main.version=${VERSION}"

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# ssh-crypt 🔒 [![Build Status](https://travis-ci.org/suside/ssh-crypt.svg?branch=master)](https://travis-ci.org/suside/ssh-crypt) [![Coverage Status](https://coveralls.io/repos/github/suside/ssh-crypt/badge.svg?branch=master)](https://coveralls.io/github/suside/ssh-crypt?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/suside/ssh-crypt)](https://goreportcard.com/report/github.com/suside/ssh-crypt) [![Say thanks!](https://img.shields.io/badge/SayThanks.io-%F0%9F%91%8D-1EAEDB.svg)](https://saythanks.io/to/suside)
2+
3+
Share AES-256 encrypted vault file with your teammates using only ssh `authorized_keys`!
4+
5+
## Usage
6+
```
7+
$ echo "secret :)" | ssh-crypt edit --stdin -a ~/.ssh/authorized_keys VAULT.txt
8+
$ cat VAULT.txt
9+
dRAALGdpdGh1Yi5jb20vc3Vza...
10+
$ ssh-crypt view VAULT.txt
11+
secret :)
12+
```
13+
14+
## Install
15+
16+
Download binary release https://github.com/suside/ssh-crypt/releases/latest
17+
or install with `go` from master branch:
18+
```
19+
go get github.com/suside/ssh-crypt
20+
```
21+
22+
## Why
23+
* Sharing Keepass with one master password is a no go...
24+
* Not everyone have/want pgp keys...
25+
* ...
26+
27+
## Inspiration
28+
This is cheeky rewrite of great [ssh-vault](https://github.com/ssh-vault/ssh-vault) with less features **but** with support of multiple key pairs.

lib/key_read.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package lib
2+
3+
import (
4+
"crypto/rsa"
5+
"crypto/x509"
6+
"encoding/pem"
7+
"errors"
8+
"io/ioutil"
9+
"strings"
10+
11+
ssh "github.com/ianmcmahon/encoding_ssh"
12+
)
13+
14+
// ReadAuthorizedKeys file from path
15+
func (v *Vault) ReadAuthorizedKeys(path string) {
16+
authorizedKeys, _ := ioutil.ReadFile(path)
17+
authorizedKeysList := strings.Split(strings.TrimSpace(string(authorizedKeys)), "\n")
18+
for _, authorizedKey := range authorizedKeysList {
19+
if pubKey, err := ssh.DecodePublicKey(authorizedKey); err == nil {
20+
// TODO handle other key types
21+
v.publicKeys = append(v.publicKeys, pubKey.(*rsa.PublicKey))
22+
}
23+
}
24+
}
25+
26+
func (v *Vault) readPrivateKey(path string) error {
27+
pemData, err := ioutil.ReadFile(path)
28+
if err != nil {
29+
return err
30+
}
31+
block, _ := pem.Decode(pemData)
32+
if block == nil {
33+
return errors.New("Unable to decode private key")
34+
}
35+
if v.privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
36+
return err
37+
}
38+
return nil
39+
}

lib/serialize.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package lib
2+
3+
import (
4+
"bytes"
5+
"crypto/rsa"
6+
"encoding/base64"
7+
"encoding/gob"
8+
"hash/crc32"
9+
)
10+
11+
func init() {
12+
gob.Register([]*rsa.PublicKey{})
13+
gob.Register(rsa.PublicKey{})
14+
gob.Register(rsa.PrivateKey{})
15+
gob.Register(vaultSecured{})
16+
}
17+
18+
func toBytes(v interface{}) []byte {
19+
b := bytes.Buffer{}
20+
e := gob.NewEncoder(&b)
21+
e.Encode(&v)
22+
return b.Bytes()
23+
}
24+
25+
func toBase64(v interface{}) string {
26+
return base64.StdEncoding.EncodeToString(toBytes(v))
27+
}
28+
29+
func fromBase64(str string) (interface{}, error) {
30+
var m interface{}
31+
by, _ := base64.StdEncoding.DecodeString(str)
32+
b := bytes.Buffer{}
33+
b.Write(by)
34+
d := gob.NewDecoder(&b)
35+
if err := d.Decode(&m); err != nil {
36+
return m, err
37+
}
38+
return m, nil
39+
}
40+
41+
func crc32sum(v interface{}) uint32 {
42+
return crc32.Checksum(toBytes(v), crc32.IEEETable)
43+
}

lib/vault.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package lib
2+
3+
import "crypto/rsa"
4+
5+
// Vault struct
6+
type Vault struct {
7+
Plaintext []byte
8+
sessionKey []byte
9+
publicKeys []*rsa.PublicKey
10+
privateKey *rsa.PrivateKey
11+
}
12+
13+
type vaultSecured struct {
14+
EncryptedData []byte
15+
EncryptedSessionKeys map[uint32][]byte
16+
}

lib/vault_read.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package lib
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"crypto/sha1"
7+
"fmt"
8+
"io/ioutil"
9+
"os"
10+
11+
"github.com/ssh-vault/crypto/aead"
12+
)
13+
14+
// DecryptVaultWithKey decrypts vault with private id_rsa key read from path
15+
func (v *Vault) DecryptVaultWithKey(vaultPath string, keyPath string) error {
16+
var encryptedData []byte
17+
var err error
18+
var vsBase interface{}
19+
if err = v.readPrivateKey(keyPath); err != nil {
20+
return err
21+
}
22+
if _, err = os.Stat(vaultPath); os.IsNotExist(err) {
23+
v.Plaintext = []byte("")
24+
return nil
25+
}
26+
if encryptedData, err = ioutil.ReadFile(vaultPath); err != nil {
27+
return err
28+
}
29+
vsBase, err = fromBase64(string(encryptedData))
30+
if err != nil {
31+
return fmt.Errorf("%s content does not look like base64", vaultPath)
32+
}
33+
vs, ok := vsBase.(vaultSecured)
34+
if !ok {
35+
return fmt.Errorf("%s does not look like a vault file", vaultPath)
36+
}
37+
sessionKey, _ := rsa.DecryptOAEP(
38+
sha1.New(),
39+
rand.Reader,
40+
v.privateKey,
41+
vs.EncryptedSessionKeys[crc32sum(v.privateKey.Public())],
42+
[]byte(""),
43+
)
44+
if sessionKey != nil {
45+
v.Plaintext, _ = aead.Decrypt(sessionKey, vs.EncryptedData, []byte(""))
46+
return nil
47+
}
48+
return fmt.Errorf("Unable to read vault %s with key %s", vaultPath, keyPath)
49+
}
50+
51+
// ReadStdIn content from stdin?
52+
func (v *Vault) ReadStdIn() {
53+
v.Plaintext, _ = ioutil.ReadAll(os.Stdin)
54+
}

lib/vault_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package lib
2+
3+
import (
4+
"errors"
5+
"io/ioutil"
6+
"os"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func Test_readAuthorizedKeys(t *testing.T) {
13+
v := Vault{}
14+
v.ReadAuthorizedKeys("../test/authorized_keys")
15+
assert.Equal(t, uint32(0x752098b2), crc32sum(v.publicKeys[0]))
16+
assert.Equal(t, uint32(0xbc02a9b3), crc32sum(v.publicKeys[1]))
17+
}
18+
19+
func Test_readPrivateKey(t *testing.T) {
20+
v := Vault{}
21+
v.readPrivateKey("../test/t1_id_rsa")
22+
assert.Equal(t, uint32(0xaa0afb16), crc32sum(v.privateKey))
23+
}
24+
25+
func Test_storeAndReadVault(t *testing.T) {
26+
tmpfile, _ := ioutil.TempFile("", "ssh-crypt-test")
27+
defer os.Remove(tmpfile.Name())
28+
v1 := Vault{Plaintext: []byte("my secret")}
29+
v1.ReadAuthorizedKeys("../test/authorized_keys")
30+
v1.StoreSecuredVault(tmpfile.Name())
31+
32+
v2 := Vault{}
33+
v2.DecryptVaultWithKey(tmpfile.Name(), "../test/t1_id_rsa")
34+
v3 := Vault{}
35+
v3.DecryptVaultWithKey(tmpfile.Name(), "../test/t2_id_rsa")
36+
assert.Equal(t, v2.Plaintext, v3.Plaintext)
37+
}
38+
39+
func Test_storeAndReadVaultWithNotUsedKey(t *testing.T) {
40+
tmpfile, _ := ioutil.TempFile("", "ssh-crypt-test")
41+
defer os.Remove(tmpfile.Name())
42+
v1 := Vault{Plaintext: []byte("my secret")}
43+
v1.ReadAuthorizedKeys("../test/authorized_keys")
44+
v1.StoreSecuredVault(tmpfile.Name())
45+
46+
v3 := Vault{}
47+
err := v3.DecryptVaultWithKey(tmpfile.Name(), "../test/t3_id_rsa")
48+
assert.Equal(t, "Unable to read vault "+tmpfile.Name()+" with key ../test/t3_id_rsa", err.Error())
49+
}
50+
51+
func Test_EncodingVaultSecured(t *testing.T) {
52+
v := vaultSecured{}
53+
v1, err := fromBase64(toBase64(v))
54+
assert.Equal(t, v, v1)
55+
assert.Nil(t, err)
56+
}
57+
58+
func Test_ReadWithKeyEmptyVault(t *testing.T) {
59+
v := Vault{}
60+
v.DecryptVaultWithKey("/tmp/newvault123123", "../test/t1_id_rsa")
61+
assert.Equal(t, []byte(""), v.Plaintext)
62+
}
63+
64+
func Test_ReadWithKeyNotBase64(t *testing.T) {
65+
v := Vault{}
66+
assert.Equal(
67+
t,
68+
"../test/authorized_keys content does not look like base64",
69+
v.DecryptVaultWithKey("../test/authorized_keys", "../test/t1_id_rsa").Error(),
70+
)
71+
}
72+
73+
func Test_ReadWithKeyNoKey(t *testing.T) {
74+
v := Vault{}
75+
assert.Equal(
76+
t,
77+
"no such file",
78+
v.DecryptVaultWithKey("../test/authorized_keys", "no such file").(*os.PathError).Path,
79+
)
80+
}
81+
82+
func Test_readPrivateKeyNoKey(t *testing.T) {
83+
v := Vault{}
84+
assert.Equal(t, "no such file", v.readPrivateKey("no such file").(*os.PathError).Path)
85+
}
86+
87+
func Test_readPrivateKeyFail2(t *testing.T) {
88+
v := Vault{}
89+
assert.Equal(t, errors.New("Unable to decode private key"), v.readPrivateKey("../test/authorized_keys"))
90+
}

lib/vault_write.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package lib
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"crypto/sha1"
7+
"io/ioutil"
8+
"os"
9+
10+
"os/exec"
11+
12+
"github.com/ssh-vault/crypto/aead"
13+
)
14+
15+
// StoreSecuredVault encrypts Vault and stores it on vaultPath
16+
func (v *Vault) StoreSecuredVault(vaultPath string) error {
17+
var err error
18+
v.sessionKey = make([]byte, 32)
19+
rand.Read(v.sessionKey)
20+
vs := vaultSecured{EncryptedSessionKeys: make(map[uint32][]byte)}
21+
for _, key := range v.publicKeys {
22+
sessionKeySecure, _ := rsa.EncryptOAEP(
23+
sha1.New(),
24+
rand.Reader,
25+
key,
26+
v.sessionKey,
27+
[]byte(""),
28+
)
29+
vs.EncryptedSessionKeys[crc32sum(&key)] = sessionKeySecure
30+
}
31+
vs.EncryptedData, err = aead.Encrypt(v.sessionKey, v.Plaintext, []byte(""))
32+
if err != nil {
33+
return err
34+
}
35+
ioutil.WriteFile(vaultPath, []byte(toBase64(vs)), 0644)
36+
return nil
37+
}
38+
39+
// EditVaultFile vault.Path file
40+
func (v *Vault) EditVaultFile() {
41+
tmpfile, _ := ioutil.TempFile("", "ssh-crypt")
42+
defer os.Remove(tmpfile.Name())
43+
tmpfile.Write([]byte(v.Plaintext))
44+
editor := os.Getenv("EDITOR")
45+
if editor == "" {
46+
editor = "vi"
47+
}
48+
cmd := exec.Command(editor, tmpfile.Name())
49+
cmd.Stdin = os.Stdin
50+
cmd.Stdout = os.Stdout
51+
cmd.Run()
52+
v.Plaintext, _ = ioutil.ReadFile(tmpfile.Name())
53+
}

0 commit comments

Comments
 (0)