-
Notifications
You must be signed in to change notification settings - Fork 63
/
ejson.go
201 lines (167 loc) · 4.86 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// 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 (
"bytes"
"encoding/hex"
"fmt"
"io"
"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
}
// Encrypt reads all contents from 'in', extracts the pubkey
// and performs the requested encryption operation, writing
// the resulting data to 'out'.
// Returns the number of bytes written and any error that might have
// occurred.
func Encrypt(in io.Reader, out io.Writer) (int, error) {
data, err := io.ReadAll(in)
if err != nil {
return -1, err
}
var myKP crypto.Keypair
if err = myKP.Generate(); err != nil {
return -1, err
}
data, err = json.CollapseMultilineStringLiterals(data)
if 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
}
return out.Write(newdata)
}
// 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) {
var fileMode os.FileMode
if stat, err := os.Stat(filePath); err == nil {
fileMode = stat.Mode()
} else {
return -1, err
}
file, err := os.Open(filePath)
if err != nil {
return -1, err
}
var outBuffer bytes.Buffer
written, err := Encrypt(file, &outBuffer)
if err != nil {
return -1, err
}
if err = file.Close(); err != nil {
return -1, err
}
if err := os.WriteFile(filePath, outBuffer.Bytes(), fileMode); err != nil {
return -1, err
}
return written, nil
}
// Decrypt reads an ejson stream from 'in' and writes the decrypted data to 'out'.
// The private key is expected to be under 'keydir'.
// Returns error upon failure, or nil on success.
func Decrypt(in io.Reader, out io.Writer, keydir string, userSuppliedPrivateKey string) error {
data, err := io.ReadAll(in)
if err != nil {
return err
}
pubkey, err := json.ExtractPublicKey(data)
if err != nil {
return err
}
privkey, err := findPrivateKey(pubkey, keydir, userSuppliedPrivateKey)
if err != nil {
return 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 err
}
_, err = out.Write(newdata)
return err
}
// 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, userSuppliedPrivateKey string) ([]byte, error) {
if _, err := os.Stat(filePath); err != nil {
return nil, err
}
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
var outBuffer bytes.Buffer
err = Decrypt(file, &outBuffer, keydir, userSuppliedPrivateKey)
return outBuffer.Bytes(), err
}
func readPrivateKeyFromDisk(pubkey [32]byte, keydir string) (privkey string, err error) {
keyFile := fmt.Sprintf("%s/%x", keydir, pubkey)
var fileContents []byte
fileContents, err = os.ReadFile(keyFile)
if err != nil {
err = fmt.Errorf("couldn't read key file (%s)", err.Error())
return
}
privkey = string(fileContents)
return
}
func findPrivateKey(pubkey [32]byte, keydir string, userSuppliedPrivateKey string) (privkey [32]byte, err error) {
var privkeyString string
if userSuppliedPrivateKey != "" {
privkeyString = userSuppliedPrivateKey
} else {
privkeyString, err = readPrivateKeyFromDisk(pubkey, keydir)
if err != nil {
return privkey, err
}
}
privkeyBytes, err := hex.DecodeString(strings.TrimSpace(privkeyString))
if err != nil {
return
}
if len(privkeyBytes) != 32 {
err = fmt.Errorf("invalid private key")
return
}
copy(privkey[:], privkeyBytes)
return
}