forked from Shopify/ejson
/
ejson.go
148 lines (124 loc) · 3.44 KB
/
ejson.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Package ejson implements the primary interface to interact with ejson
// documents and keypairs. The CLI implemented by cmd/ejson is a fairly thin
// wrapper around this package.
package ejson
import (
"encoding/hex"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/Shopify/ejson/crypto"
"github.com/Shopify/ejson/json"
)
// GenerateKeypair is used to create a new ejson keypair. It returns the keys as
// hex-encoded strings, suitable for printing to the screen. hex.DecodeString
// can be used to load the true representation if necessary.
func GenerateKeypair() (pub string, priv string, err error) {
var kp crypto.Keypair
if err := kp.Generate(); err != nil {
return "", "", err
}
return kp.PublicString(), kp.PrivateString(), nil
}
// EncryptFileInPlace takes a path to a file on disk, which must be a valid EJSON file
// (see README.md for more on what constitutes a valid EJSON file). Any
// encryptable-but-unencrypted fields in the file will be encrypted using the
// public key embdded in the file, and the resulting text will be written over
// the file present on disk.
func EncryptFileInPlace(filePath string) (int, error) {
data, err := readFile(filePath)
if err != nil {
return -1, err
}
fileMode, err := getMode(filePath)
if err != nil {
return -1, err
}
var myKP crypto.Keypair
if err := myKP.Generate(); err != nil {
return -1, err
}
pubkey, err := json.ExtractPublicKey(data)
if err != nil {
return -1, err
}
encrypter := myKP.Encrypter(pubkey)
walker := json.Walker{
Action: encrypter.Encrypt,
}
newdata, err := walker.Walk(data)
if err != nil {
return -1, err
}
if err := writeFile(filePath, newdata, fileMode); err != nil {
return -1, err
}
return len(newdata), nil
}
// DecryptFile takes a path to an encrypted EJSON file and returns the data
// decrypted. The public key used to encrypt the values is embedded in the
// referenced document, and the matching private key is searched for in keydir.
// There must exist a file in keydir whose name is the public key from the
// EJSON document, and whose contents are the corresponding private key. See
// README.md for more details on this.
func DecryptFile(filePath, keydir string) ([]byte, error) {
data, err := readFile(filePath)
if err != nil {
return nil, err
}
pubkey, err := json.ExtractPublicKey(data)
if err != nil {
return nil, err
}
privkey, err := findPrivateKey(pubkey, keydir)
if err != nil {
return nil, err
}
myKP := crypto.Keypair{
Public: pubkey,
Private: privkey,
}
decrypter := myKP.Decrypter()
walker := json.Walker{
Action: decrypter.Decrypt,
}
newdata, err := walker.Walk(data)
if err != nil {
return nil, err
}
return newdata, nil
}
func findPrivateKey(pubkey [32]byte, keydir string) (privkey [32]byte, err error) {
keyFile := fmt.Sprintf("%s/%x", keydir, pubkey)
var fileContents []byte
fileContents, err = readFile(keyFile)
if err != nil {
err = fmt.Errorf("couldn't read key file (%s)", err.Error())
return
}
bs, err := hex.DecodeString(strings.TrimSpace(string(fileContents)))
if err != nil {
return
}
if len(bs) != 32 {
err = fmt.Errorf("invalid private key retrieved from keydir")
return
}
copy(privkey[:], bs)
return
}
// for mocking in tests
func _getMode(path string) (os.FileMode, error) {
fi, err := os.Stat(path)
if err != nil {
return 0, err
}
return fi.Mode(), nil
}
// for mocking in tests
var (
readFile = ioutil.ReadFile
writeFile = ioutil.WriteFile
getMode = _getMode
)