-
Notifications
You must be signed in to change notification settings - Fork 6
/
chestnut.go
448 lines (416 loc) · 14.3 KB
/
chestnut.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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
package chestnut
import (
"errors"
"fmt"
"reflect"
"github.com/jrapoport/chestnut/encoding/compress"
"github.com/jrapoport/chestnut/encoding/compress/zstd"
"github.com/jrapoport/chestnut/encoding/json"
"github.com/jrapoport/chestnut/encoding/json/encoders/secure"
"github.com/jrapoport/chestnut/log"
"github.com/jrapoport/chestnut/storage"
"github.com/jrapoport/chestnut/value"
)
// Chestnut is used to manage an encrypted store. It provides additional features such
// as chained encryption, independently secured secrets, sparse encryption, and hashing.
// For more detail, SEE: https://github.com/jrapoport/chestnut/blob/master/README.md
type Chestnut struct {
opts ChestOptions
store storage.Storage
log log.Logger
}
// NewChestnut is used to create a new chestnut encrypted store.
func NewChestnut(store storage.Storage, opt ...ChestOption) *Chestnut {
const logName = "chestnut"
//logger := storage.LoggerFromStore(store, logName)
opts := applyOptions(DefaultChestOptions, opt...)
logger := log.Named(opts.log, logName)
cn := &Chestnut{opts, store, logger}
if err := cn.validConfig(); err != nil {
logger.Panic(err)
return nil
}
return cn
}
func (cn *Chestnut) validConfig() error {
if cn.store == nil {
return errors.New("store required")
}
if cn.opts.encryptor == nil {
return errors.New("encryptor is required")
}
if cn.opts.compressor != nil || cn.opts.decompressor != nil {
cn.opts.compression = compress.Custom
}
if cn.opts.compression == compress.Custom && cn.opts.compressor == nil {
return errors.New("compressor is required")
}
if cn.opts.compression == compress.Custom && cn.opts.decompressor == nil {
return errors.New("decompressor is required")
}
if !cn.opts.compression.Valid() {
return errors.New("invalid compression format")
}
return nil
}
// Open the storage chest
func (cn *Chestnut) Open() error {
if err := cn.validConfig(); err != nil {
return err
}
cn.log.Debug("opening storage chest")
if err := cn.store.Open(); err != nil {
return cn.logError("open", err)
}
cn.log.Info("storage chest open")
if cn.opts.encryptor != nil {
cn.log.Infof("using encryption: %s",
cn.opts.encryptor.Name())
}
if cn.opts.compression != compress.None {
cn.log.Infof("%s compression active",
cn.opts.compression)
}
if !cn.opts.overwrites {
cn.log.Info("overwrites are disabled")
}
return nil
}
// Put encrypts the plaintext and stores it at key.
func (cn *Chestnut) Put(name string, key []byte, plaintext []byte) error {
cn.log.Debugf("put: %d plaintext bytes to key: %s", len(plaintext), key)
// the store will make these same checks, but encryption
// is expensive, so we are going to do them upfront here.
if err := storage.ValidKey(name, key); err != nil {
return cn.logError("put", err)
} else if len(plaintext) <= 0 {
err = errors.New("plaintext cannot be empty")
return cn.logError("put", err)
} else if err = cn.CanPut(name, key); err != nil {
return cn.logError("put", err)
}
if cn.opts.compression != compress.None {
var err error
if plaintext, err = cn.compress(plaintext); err != nil {
return cn.logError("put", err)
}
}
cn.log.Debugf("put: encrypt %d bytes", len(plaintext))
cipherText, err := cn.encrypt(plaintext)
if err != nil {
return cn.logError("put", err)
}
cn.log.Debugf("put: encrypted %d bytes", len(cipherText))
return cn.logError("", cn.store.Put(name, key, cipherText))
}
// Get decrypts the ciphertext at key and returns the plaintext.
func (cn *Chestnut) Get(name string, key []byte) ([]byte, error) {
cn.log.Debugf("get: ciphertext at key: %s", key)
ciphertext, err := cn.store.Get(name, key)
if err != nil {
return nil, cn.logError("", err)
}
cn.log.Debugf("get: decrypt %d bytes", len(ciphertext))
plaintext, err := cn.decrypt(ciphertext)
if err != nil {
return nil, cn.logError("get", err)
}
cn.log.Debugf("put: decrypted %d bytes", len(plaintext))
// decompress will check to see if the data is compressed.
// if sit is not compressed, it returns the buffer.
if plaintext, err = cn.decompress(plaintext); err != nil {
return nil, cn.logError("get", err)
}
return plaintext, nil
}
// Save encrypts the struct in v and stores the encoded result at key.
func (cn *Chestnut) Save(name string, key []byte, v interface{}) error {
cn.log.Debugf("save: %v value to key: %s", reflect.TypeOf(v), key)
// the store will make these same checks, but encryption
// is expensive, so we are going to do them upfront here.
if err := storage.ValidKey(name, key); err != nil {
return cn.logError("save", err)
} else if v == nil {
err = errors.New("value cannot be nil")
return cn.logError("save", err)
} else if err = cn.CanPut(name, key); err != nil {
return cn.logError("save", err)
}
cn.log.Debugf("save: encrypt %v value", reflect.TypeOf(v))
ciphertext, err := cn.marshal(v)
if err != nil {
return cn.logError("save", err)
}
cn.log.Debugf("save: put %d encrypted bytes", len(ciphertext))
if err = cn.store.Put(name, key, ciphertext); err != nil {
return cn.logError("save", err)
}
cn.log.Debugf("save: encrypted %v value", reflect.TypeOf(v))
return nil
}
// Load decrypts the struct at key and returns the decoded result in v.
func (cn *Chestnut) Load(name string, key []byte, v interface{}) error {
cn.log.Debugf("load: %v value at key: %s", reflect.TypeOf(v), key)
if err := cn.load(name, key, v, false); err != nil {
return cn.logError("load", err)
}
cn.log.Debugf("load: decrypted %v value", reflect.TypeOf(v))
return nil
}
// Sparse loads the struct at key and returns the sparsely decoded result in v.
// Unlike Load, it does not decrypt the encoded struct and secure fields are
// replaced with empty values. If the struct was not saved as a sparsely encoded
// struct this has no effect and is equivalent to calling Load. Structs must
// have been saved with secure fields to be loaded as sparse structs by Sparse.
func (cn *Chestnut) Sparse(name string, key []byte, v interface{}) error {
cn.log.Debugf("sparse: %v value at key: %s", reflect.TypeOf(v), key)
if err := cn.load(name, key, v, true); err != nil {
return cn.logError("sparse", err)
}
cn.log.Debugf("sparse: decrypted sparse %v value", reflect.TypeOf(v))
return nil
}
// SaveKeyed encrypts the keyed value and stores the result.
func (cn *Chestnut) SaveKeyed(v value.Keyed) error {
if v == nil || reflect.ValueOf(v).IsNil() {
err := errors.New("value cannot be nil")
return cn.logError("save value", err)
} else if err := v.ValidKey(); err != nil {
return cn.logError("save value", err)
}
err := cn.Save(v.Namespace(), v.Key(), v)
return cn.logError("save value", err)
}
// LoadKeyed decrypts the keyed value and returns the result.
func (cn *Chestnut) LoadKeyed(v value.Keyed) error {
if v == nil || reflect.ValueOf(v).IsNil() {
err := errors.New("value cannot be nil")
return cn.logError("load value", err)
} else if err := v.ValidKey(); err != nil {
return cn.logError("load value", err)
}
err := cn.Load(v.Namespace(), v.Key(), v)
return cn.logError("load value", err)
}
// SparseKeyed sparsely loads the keyed value and returns the result.
// Unlike LoadKeyed, it does not decrypt the keyed value. SEE: Sparse.
func (cn *Chestnut) SparseKeyed(v value.Keyed) error {
if v == nil || reflect.ValueOf(v).IsNil() {
err := errors.New("value cannot be nil")
return cn.logError("sparse value", err)
} else if err := v.ValidKey(); err != nil {
return cn.logError("sparse value", err)
}
err := cn.Sparse(v.Namespace(), v.Key(), v)
return cn.logError("sparse value", err)
}
// Has checks for a key in the storage chest. Has returns true
// if the key is found, otherwise false.
func (cn *Chestnut) Has(name string, key []byte) (bool, error) {
cn.log.Debugf("has: key: %s", key)
has, err := cn.store.Has(name, key)
cn.log.Debugf("has: key %s: %t", key, has)
return has, cn.logError("", err)
}
// CanPut returns nil if writing to key is ok. If overwrites
// are disabled and the key exists, ErrForbidden is returned.
func (cn *Chestnut) CanPut(name string, key []byte) error {
cn.log.Debugf("can put: key: %s", key)
if err := storage.ValidKey(name, key); err != nil {
return cn.logError("can put", err)
}
if cn.opts.overwrites {
cn.log.Debug("can put: overwrites enabled")
return nil
}
// if overwrites are disabled check to see if we have the key. if
// the key does not exist, has will return an error. technically,
// this could get confused because an error would also be returned
// if the key was invalid, but since we already check that above
// we can safely assume that's not the error. since we expect an error
// when the key is not found has Has will log it, we can ignore it.
if has, _ := cn.Has(name, key); has {
return cn.logError("can put", ErrForbidden)
}
// we didn't find the key and there is no error, this is not an overwrite.
cn.log.Debugf("can put: can write to key: %s", key)
return nil
}
// Delete removes a key from the storage chest.
func (cn *Chestnut) Delete(name string, key []byte) error {
cn.log.Debugf("delete: key: %s", key)
return cn.logError("", cn.store.Delete(name, key))
}
// List returns a list of keys in the namespace.
func (cn *Chestnut) List(namespace string) ([][]byte, error) {
cn.log.Infof("list: all keys")
keys, err := cn.store.List(namespace)
cn.log.Debugf("list: found %d keys: %s", len(keys), keys)
return keys, cn.logError("", err)
}
// Export saves a copy of the storage chest to directory at path.
func (cn *Chestnut) Export(path string) error {
cn.log.Debugf("export: to path: %s", path)
return cn.logError("", cn.store.Export(path))
}
// Close the storage chest
func (cn *Chestnut) Close() error {
cn.log.Info("closing storage chest")
if err := cn.store.Close(); err != nil {
return cn.logError("close", err)
}
cn.log.Info("storage chest closed")
return nil
}
// Logger gets a copy of the logger from the storage chest's options
func (cn *Chestnut) Logger() log.Logger {
return cn.opts.log
}
// SetLogger sets the logger for the storage chest
func (cn *Chestnut) SetLogger(l log.Logger) {
if l == nil {
l = log.Log
}
cn.log = l
}
// load decrypts the secure or sparse value at key and stores the result in v.
func (cn *Chestnut) load(name string, key []byte, v interface{}, sparse bool) error {
if v == nil {
return errors.New("value cannot be nil")
}
ciphertext, err := cn.store.Get(name, key)
if err != nil {
return err
}
return cn.unmarshal(ciphertext, v, sparse)
}
// encrypt returns the plaintext data as ciphertext.
func (cn *Chestnut) encrypt(plaintext []byte) (ciphertext []byte, err error) {
cn.log.Debugf("encrypt: encrypting %d bytes", len(plaintext))
ciphertext, err = cn.opts.encryptor.Encrypt(plaintext)
if err != nil {
err = cn.logError("encrypt", err)
return
}
cn.log.Debugf("encrypt: encrypted %d bytes", len(ciphertext))
return
}
// decrypt returns the ciphertext data as plaintext.
func (cn *Chestnut) decrypt(ciphertext []byte) (plaintext []byte, err error) {
cn.log.Debugf("decrypt: decrypting %d bytes", len(ciphertext))
plaintext, err = cn.opts.encryptor.Decrypt(ciphertext)
if err != nil {
err = cn.logError("decrypt", err)
return
}
cn.log.Debugf("decrypt: decrypted %d bytes", len(plaintext))
return
}
// marshal returns the JSON encoding of v as ciphertext.
func (cn *Chestnut) marshal(v interface{}) (ciphertext []byte, err error) {
if v == nil {
err = errors.New("value cannot be nil")
return nil, cn.logError("marshal", err)
}
cn.log.Debugf("marshal: %v value", reflect.TypeOf(v))
ciphertext, err = json.SecureMarshal(v, cn.encrypt, secure.WithLogger(cn.log))
if err != nil {
err = cn.logError("marshal", err)
return
}
cn.log.Debugf("marshal: encrypted %d bytes", len(ciphertext))
return
}
// unmarshal returns the plaintext decoded JSON value at v.
func (cn *Chestnut) unmarshal(ciphertext []byte, v interface{}, sparse bool) error {
if v == nil {
err := errors.New("value cannot be nil")
return cn.logError("unmarshal", err)
}
cn.log.Debugf("unmarshal: decrypt %d bytes to %v value",
len(ciphertext), reflect.TypeOf(v))
opts := []secure.Option{secure.WithLogger(cn.log)}
if sparse {
cn.log.Debug("use sparse decoding")
opts = append(opts, secure.SparseDecode())
}
err := json.SecureUnmarshal(ciphertext, v, cn.decrypt, opts...)
if err != nil {
return cn.logError("unmarshal", err)
}
cn.log.Debugf("unmarshal: decrypted %v value", reflect.TypeOf(v))
return nil
}
func (cn *Chestnut) compress(data []byte) ([]byte, error) {
format := cn.opts.compression
if format == compress.None {
return data, nil
}
var compressor compress.CompressorFunc
switch format {
case compress.Zstd:
compressor = zstd.Compress
case compress.Custom:
compressor = cn.opts.compressor
default:
break
}
if compressor == nil {
err := fmt.Errorf("%s unsupported", format)
return nil, cn.logError("compress", err)
}
size := len(data)
cn.log.Debugf("compressing %d bytes with %s", size, format)
compressed, err := compressor(data)
if err != nil {
return nil, cn.logError("compress", err)
}
compressed = compress.EncodeFormat(compressed, format)
cn.log.Debugf("%s compressed encrypted bytes to %d (%0.2f%% reduction)",
format, len(compressed), calcReduction(size, len(compressed)))
return compressed, nil
}
func calcReduction(oldSize, newSize int) float64 {
return ((float64(oldSize) - float64(newSize)) / float64(oldSize)) * 100
}
func (cn *Chestnut) decompress(data []byte) ([]byte, error) {
// check for compression
compressed, format := compress.DecodeFormat(data)
// this does not appear to be data we compressed
if format == compress.None {
return compressed, nil
}
var decompressor compress.DecompressorFunc
switch format {
case compress.Zstd:
decompressor = zstd.Decompress
case compress.Custom:
decompressor = cn.opts.decompressor
default:
break
}
if decompressor == nil {
err := fmt.Errorf("%s unsupported", format)
return nil, cn.logError("decompress", err)
}
cn.log.Debugf("decompressing %d bytes with %s", len(compressed), format)
decompressed, err := decompressor(compressed)
if err != nil {
return nil, cn.logError("decompress", err)
}
cn.log.Debugf("decompressed %d bytes with %s",
len(decompressed), format)
return decompressed, nil
}
func (cn *Chestnut) logError(name string, err error) error {
if err == nil {
return nil
}
if name != "" {
err = fmt.Errorf("%s: %w", name, err)
}
cn.log.Error(err)
return err
}
// ErrForbidden the storage operation is forbidden
var ErrForbidden = errors.New("forbidden")