Repository navigation
Expand file tree
/
Copy pathencryption_ee.go
More file actions
1842 lines (1585 loc) · 60 KB
/
Copy pathencryption_ee.go
File metadata and controls
1842 lines (1585 loc) · 60 KB
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
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2026-Present Couchbase, Inc.
//
// Use of this software is governed by the Business Source License included
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
// in that file, in accordance with the Business Source License, use of this
// software will be governed by the Apache License, Version 2.0, included in
// the file licenses/APL2.txt.
//go:build enterprise
// +build enterprise
package cbft
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/blevesearch/bleve/v2"
index "github.com/blevesearch/bleve_index_api"
"github.com/couchbase/cbauth"
"github.com/couchbase/cbauth/cbauthimpl"
"github.com/couchbase/cbft/search_history"
"github.com/couchbase/cbgt"
log "github.com/couchbase/clog"
gocbcrypto "github.com/couchbase/gocbcrypto"
)
// ------------------------- encryption manager implementation -----------------------
var encryptionManagerInstance *encryptionManager
var encryptionManagerOnce sync.Once
// This is responsible for all encryption related operations.
// Managing encryption, decryption, key management,
// bleve callbacks, cbauth callbacks and bucket updates related to encryption.
type encryptionManager struct {
mgr *cbgt.Manager
// key store is responsible for managing encryption keys
// and caching keys in use for different buckets and key types
keyStore *keyStore
// bucket store is responsible for managing bucket name to UUID mappings
// and caching the mappings for quick retrieval
bucketStore *bucketStore
// pindex source store caches pindex name to source (bucket) name
// mappings for non-scoped indexes, so the bleve writer hook does
// not have to consult the cfg on every file creation
pindexSourceStore *pindexSourceStore
}
// unique context used for deriving encryption keys for search service
var searchEncryptionContext = []byte("search")
const (
// key types tracked by search's encryption manager
bucketKeyType = "service_bucket"
otherKeyType = "other"
// cipher key string
aes256gcmCipher = "AES-256-GCM"
// nonce length for AES-256-GCM encryption
noncelength = 12
// timeout for getting encryption keys from cbauth
getKeyTimeout = 2 * time.Minute
// timeout for key import by cbauth during rebalance
importKeyTimeout = 2 * 60 * 1000 // 2 minutes in milliseconds
// number of parallel workers for drop keys operation
numParallelDrops = 4
)
// NewEncryptionManager creates a new instance of encryption manager and registers
// the necessary callbacks with cbauth and cbgt manager. It returns an error if
// there is an issue with registering the callbacks or creating the manager.
func NewEncryptionManager(mgr *cbgt.Manager) (*encryptionManager, error) {
if mgr == nil {
return nil, fmt.Errorf("encryptionManager: manager cannot be nil")
}
encryptionManagerOnce.Do(func() {
encryptionManagerInstance = &encryptionManager{
keyStore: newKeyStore(mgr),
bucketStore: newBucketStore(mgr),
pindexSourceStore: newPIndexSourceStore(mgr),
mgr: mgr,
}
})
// register callbacks with cbauths for key functionality
err := cbauth.RegisterEncryptionKeysCallbacks(
encryptionManagerInstance.refreshKeys,
encryptionManagerInstance.getInUseKeys,
encryptionManagerInstance.dropKeys,
encryptionManagerInstance.synchroniseKeys,
)
if err != nil {
return nil, fmt.Errorf("failed to register encryption keys callbacks: %w",
err)
}
// register callbacks with bleve for readers and writers
index.WriterHook = encryptionManagerInstance.bleveWriterHook
index.ReaderHook = encryptionManagerInstance.bleveReaderHook
// search_history derives keys from the same path-shaped context as bleve;
// reuse the bleve hooks directly. Per-record callback caching lives inside
// search_history's logWriter so the hook itself stays stateless.
search_history.WriterHook = encryptionManagerInstance.bleveWriterHook
search_history.ReaderHook = encryptionManagerInstance.bleveReaderHook
// register callbacks with cbgt manager for bucket updates and file encryption/decryption
mgr.SetBucketUpdateCallback(
encryptionManagerInstance.bucketUpdateCallback)
mgr.SetEncryptAndWriteFileCallback(
encryptionManagerInstance.encryptAndWriteFile)
mgr.SetDecryptAndReadFileCallback(
encryptionManagerInstance.decryptAndReadFile)
// a pindex added to or removed from this node, such as one moved by a
// rebalance, changes the keys in use
cbgt.RegisteredPIndexCallbacks.OnCreate = encryptionManagerInstance.pindexChanged
cbgt.RegisteredPIndexCallbacks.OnDelete = encryptionManagerInstance.pindexChanged
log.Print("encryptionManager: encryption manager created and callbacks " +
"registered successfully")
return encryptionManagerInstance, nil
}
// ---------------------------- callbacks implementation --------------------------
// requests a keys in use cache refresh when a pindex is registered on or
// unregistered from this node
func (em *encryptionManager) pindexChanged(pindexName string) {
em.keyStore.requestRefresh()
}
// updates bucket cache when there is an index introduction/deletion,
// which is the only time we need to check bucket UUID changes.
// The pindex source cache is also cleared here - index create/update/
// delete mints new pindex names
func (em *encryptionManager) bucketUpdateCallback() {
em.bucketStore.refreshBucketUUIDMap()
em.pindexSourceStore.reset()
}
// The callback is called in a separate go-routine (not protected by mutex)
// when there is a change in encryption keys. Returns nil if keys are ok, or
// "error" if any of the mandatory keys is missing and the service can't
// continue working. The callback must not take longer than a few seconds.
func (em *encryptionManager) refreshKeys(keyData cbauth.KeyDataType) error {
log.Printf("encryptionManager: refreshKeys callback called for key data %v",
keyData)
keysInUse, err := em.keyStore.getKeysInUse(keyData.TypeName, keyData.BucketUUID)
if err != nil {
// log and continue operation when keys in use fails
// failure is likely due to booting up
log.Printf("encryptionManager: failed to get keys in use for key data "+
"%v: %v", keyData, err)
}
newKeyInfo, err := em.getEncryptionKeys(keyData)
if err != nil {
return err
}
newKeysMap := make(map[string]struct{})
for _, key := range newKeyInfo.Keys {
newKeysMap[key.Id] = struct{}{}
}
// compare all of the keys in use with the new keys. If any of the keys in use is not
// present in the new keys, return an error as we cannot guarantee continued
// operation without the key. The empty key ID signals unencrypted data and
// is not present in cbauth's key registry, so exclude it from validation.
delete(keysInUse, "")
for keyId := range keysInUse {
if _, exists := newKeysMap[keyId]; !exists {
return fmt.Errorf("encryptionManager: key with ID %s is still in "+
"use but not present in new keys for key data %v", keyId, keyData)
}
}
// update the key store with the new keys
em.keyStore.updateMasterKey(keyData.BucketUUID, keyData.TypeName, newKeyInfo)
log.Printf("encryptionManager: refreshKeys completed successfully for key data %v, "+
"new active key ID: %s", keyData, newKeyInfo.ActiveKeyId)
return nil
}
// The callback is called in a separate go-routine (not protected by mutex) when
// Cluster Manager performs garbage collection for unused keys. The goal is to
// determine which keys are actually used by the service. Returns list of key
// ids that are being used. The callback must return quickly. The service
// should cache the list of keys in use if its calculation requires significant
// time.
// The service must include the empty key (empty key id) in the returned list if
// and only if there is unencrypted data on disk.
func (em *encryptionManager) getInUseKeys(keyData cbauth.KeyDataType) (
[]string, error) {
// get all of the keys in use from the cache
keysInUse, err := em.keyStore.getKeysInUse(keyData.TypeName, keyData.BucketUUID)
if err != nil {
return nil, fmt.Errorf("encryptionManager: failed to get keys in "+
"use for key data %v: %w", keyData, err)
}
// append the active key for the bucket if it's not already present in the
// keys obtained
keyInfo, err := em.keyStore.getKeyInfo(keyData.TypeName, keyData.BucketUUID)
if err != nil {
return nil, fmt.Errorf("encryptionManager: failed to get active key for "+
"key data %v: %w", keyData, err)
}
if _, ok := keysInUse[keyInfo.ActiveKeyId]; !ok {
keysInUse[keyInfo.ActiveKeyId] = struct{}{}
}
keys := make([]string, 0, len(keysInUse))
for keyId := range keysInUse {
keys = append(keys, keyId)
}
return keys, nil
}
// The callback is called in a separate go-routine (not protected by mutex) when
// Cluster Manager needs to get rid of one or more encryption keys. This means
// that the service should find the data that is encrypted by the specified keys,
// and re-encrypt that data with the current active key. The callback must not
// block. The service should initiate the process of re-encryption and return
// immediately. When re-encryption is finished, the service should call
// KeysDropComplete(DataType, Res).
// KeyIdsToDrop may include the empty key (empty key id), which should be treated
// as an ask to encrypt the data on disk that is not encrypted yet.
func (em *encryptionManager) dropKeys(keyData cbauth.KeyDataType,
KeyIdsToDrop []string) {
log.Printf("encryptionManager: dropKeys callback called for key data %v "+
"and keys to drop %v", keyData, KeyIdsToDrop)
if em.mgr != nil && em.mgr.BootingDone() {
go em.dropKeysAsync(keyData, KeyIdsToDrop)
} else {
// should not be hit considering that encryption keys should be loaded
// before cbauth issues a drop keys callback, but adding a safeguard
// just in case to avoid potential issues.
go func() {
log.Printf("encryptionManager: manager is not ready for drop keys request, "+
"waiting for boot to complete. Key data: %v, keys to drop: %v",
keyData, KeyIdsToDrop)
em.keyStore.waitPreLoad()
log.Printf("encryptionManager: boot completed, proceeding with drop"+
" keys for key data %v and keys to drop %v", keyData, KeyIdsToDrop)
em.dropKeysAsync(keyData, KeyIdsToDrop)
}()
}
}
func (em *encryptionManager) dropKeysAsync(keyData cbauth.KeyDataType,
KeyIdsToDrop []string) {
_, pIndexes := em.mgr.CurrentMaps()
keysToDropMap := make(map[string]struct{})
for _, keyId := range KeyIdsToDrop {
keysToDropMap[keyId] = struct{}{}
}
// initialize channels for jobs and errors. The error buffer also covers
// the otherKeyType branch (planPIndexes + search history goroutines).
jobs := make(chan *cbgt.PIndex, len(pIndexes))
errs := make(chan error, len(pIndexes)+2)
wg := sync.WaitGroup{}
// traverse through all planPIndexes and re-encrypt the ones encrypted with the keys to drop
if keyData.TypeName == otherKeyType {
wg.Add(2)
go em.dropPlanPIndexes(keysToDropMap, &wg, errs)
go em.dropSearchHistory(keysToDropMap, &wg, errs)
} else {
for _ = range numParallelDrops {
go em.dropWorker(jobs, keyData, keysToDropMap, &wg, errs)
}
// traverse through all pindexes and re-encrypt the ones
// encrypted with the keys to drop.
for _, pIndex := range pIndexes {
wg.Add(1)
jobs <- pIndex
}
}
// blockingly wait for all re-encryption go-routines to finish and
// aggregate errors if any before calling KeysDropComplete
go func() {
wg.Wait()
// close channels so workers can exit
close(jobs)
close(errs)
errMsg := ""
for err := range errs {
if err != nil {
errMsg += err.Error() + "; "
}
}
if errMsg != "" {
log.Printf("encryptionManager: errors occurred during key drop: %s",
errMsg)
cbauth.KeysDropComplete(keyData,
fmt.Errorf("encryptionManager: errors occurred during "+
"key drop: %s", errMsg))
} else {
log.Printf("encryptionManager: keys drop completed successfully "+
"for key data - %+v and key IDs to drop - %v",
keyData, KeyIdsToDrop)
cbauth.KeysDropComplete(keyData, nil)
}
em.bucketStore.refreshBucketUUIDMap()
em.keyStore.refreshKeysInUseCache()
}()
return
}
// re-encrypts records in the search history log whose key id is in the
// keysToDropMap. Runs in its own goroutine under the same WaitGroup as
// dropPlanPIndexes so the drop-keys API stays unblocked but the final
// KeysDropComplete only fires after both finish.
func (em *encryptionManager) dropSearchHistory(keysToDropMap map[string]struct{},
wg *sync.WaitGroup, errs chan error) {
defer wg.Done()
if search_history.Service == nil {
return
}
if err := search_history.Service.Reencrypt(keysToDropMap); err != nil {
log.Printf("encryptionManager: failed to re-encrypt search history: %v",
err)
errs <- err
}
}
func (em *encryptionManager) dropPlanPIndexes(keysToDropMap map[string]struct{}, wg *sync.WaitGroup, errs chan error) {
defer wg.Done()
planPIndexesPath := filepath.Join(em.mgr.DataDir(), "planPIndexes")
files, err := os.ReadDir(planPIndexesPath)
if err != nil {
log.Printf("encryptionManager: failed to read planPIndexes directory at "+
"path %s: %v", planPIndexesPath, err)
errs <- err
return
}
for _, file := range files {
if file.IsDir() {
continue
}
path := filepath.Join(planPIndexesPath, file.Name())
err = em.reencryptFile(path, keysToDropMap)
if err != nil {
log.Printf("encryptionManager: failed to re-encrypt planPIndexes "+
"file at path %s: %v", path, err)
errs <- err
return
}
}
}
func (em *encryptionManager) dropPIndex(pIndex *cbgt.PIndex, keysToDropMap map[string]struct{}) error {
info, err := em.pindexSourceStore.getPIndexInfo(pIndex.Name)
if err != nil {
log.Printf("encryptionManager: failed to get pindex info for "+
"pindex %s: %v", pIndex.Name, err)
return err
}
// a pindex that cannot be encrypted only ever holds the empty key, so
// nothing is dropped and only a request to drop the empty key is
// reported as failed
if !info.encryptable() {
if _, ok := keysToDropMap[""]; !ok {
return nil
}
err = fmt.Errorf("index %s (pindex %s) uses zap segment version %d, "+
"which does not support encryption. Recreate the index with "+
"segmentVersion %d or higher to encrypt it", pIndex.IndexName,
pIndex.Name, info.segmentVersion,
bleveHierarchicalNestedBinaryVectorGeoV2ZapVersion)
log.Warnf("encryptionManager: %v", err)
return err
}
// obtain the bleve index for the pindex
bIndex, _, _, err := bleveIndex(pIndex)
if err != nil {
log.Printf("encryptionManager: failed to get bleve index for "+
"pindex %s: %v", pIndex.Name, err)
return err
}
// drop the file writer ids used by the index
if cIndex, ok := bIndex.(bleve.IndexWithCallbacks); ok {
err := cIndex.DropFileWriterIDs(keysToDropMap)
if err != nil {
log.Printf("encryptionManager:failed to re-encrypt index for "+
"pindex %s: %v", pIndex.Name, err)
return err
}
}
path := pIndex.Path
if path == "" {
log.Printf("encryptionManager: empty path for pindex %s, skipping "+
"re-encryption", pIndex.Name)
return fmt.Errorf("empty path for pindex %s", pIndex.Name)
}
// re-encrypt meta files associated with the pindex as well
for _, metaFile := range []string{"PINDEX_BLEVE_META", "PINDEX_META"} {
metaFilePath := filepath.Join(path, metaFile)
err = em.reencryptFile(metaFilePath, keysToDropMap)
if err != nil {
log.Printf("encryptionManager: failed to re-encrypt "+
"meta file %s for pindex %s: %v",
metaFile, pIndex.Name, err)
return err
}
}
return nil
}
func (em *encryptionManager) dropWorker(jobs <-chan *cbgt.PIndex, keyData cbauth.KeyDataType, keysToDropMap map[string]struct{},
wg *sync.WaitGroup, errs chan error) {
for pIndex := range jobs {
if pIndex.SourceUUID == keyData.BucketUUID {
err := em.dropPIndex(pIndex, keysToDropMap)
if err != nil {
errs <- err
}
}
wg.Done()
}
}
// Not applicable to search service as we do not store keys outside rebalances
func (em *encryptionManager) synchroniseKeys(keyData cbauth.KeyDataType) error {
return nil
}
// -------------------------- cbauth connectors for keys -----------------------
// obtain encryption keys from cbauth for the given key data
func (em *encryptionManager) getEncryptionKeys(keyData cbauth.KeyDataType) (
*cbauth.EncrKeysInfo, error) {
// try to get keys non blockingly if possible
newKeyInfo, err := cbauth.GetEncryptionKeys(keyData)
if err == cbauth.ErrKeysNotAvailable {
// attempt to get keys blockingly with a timeout.
ctx, cancel := context.WithTimeout(context.Background(), getKeyTimeout)
defer cancel()
newKeyInfo, err = cbauth.GetEncryptionKeysBlocking(ctx, keyData)
}
if err != nil {
return nil, fmt.Errorf("encryptionManager: failed to get encryption keys "+
"for key data %v: %w", keyData, err)
}
return newKeyInfo, nil
}
// ---------------------------- utility functions ----------------------------
func (em *encryptionManager) getAllKeysInUse() (map[string][]string, error) {
return em.keyStore.getAllKeysInUse()
}
// obtain the active key for the given bucket and key type
func (em *encryptionManager) getKey(keyType, bucketUUID string) (*cbauthimpl.EaRKey, error) {
keyInfo, err := em.keyStore.getKeyInfo(keyType, bucketUUID)
if err != nil {
return nil, fmt.Errorf("encryptionManager: failed to get key info "+
"for bucket %s: %w", bucketUUID, err)
}
// if there is no active key for the bucket, return nil indicating that encryption
// is not enabled for the bucket
if keyInfo.ActiveKeyId == "" {
return nil, nil
}
// Return the active key for the bucket
for _, key := range keyInfo.Keys {
if key.Id == keyInfo.ActiveKeyId {
return &key, nil
}
}
return nil, fmt.Errorf("encryptionManager: active key not found for "+
"bucket: %s", bucketUUID)
}
// obtain the key with the given ID for the given bucket and key type
func (em *encryptionManager) getKeyById(keyType, bucketUUID string, keyId string) (*cbauthimpl.EaRKey, error) {
// if keyId is empty, return nil indicating that encryption is not enabled for the bucket
if keyId == "" {
return nil, nil
}
keyInfo, err := em.keyStore.getKeyInfo(keyType, bucketUUID)
if err != nil {
return nil, fmt.Errorf("encryptionManager: failed to get key info for "+
"bucket %s: %w", bucketUUID, err)
}
for _, key := range keyInfo.Keys {
if key.Id == keyId {
return &key, nil
}
}
return nil, fmt.Errorf("encryptionManager: key with ID %s not found for "+
"bucket %s", keyId, bucketUUID)
}
// -------------------------- bleve hooks -------------------------------
// hook used by bleve to obtain a writer callback for encrypting data.
// The callback bucket name and context from the path provided
func (em *encryptionManager) bleveWriterHook(path []byte) (
string, func(data []byte) []byte, error) {
if path == nil || len(path) == 0 {
return "", emptyWriterCallback(), nil
}
// process the path to obtain the key type, bucket name and context for encryption
keyType, bucketName, context, pindexName, err := processPath(string(path))
if err != nil {
return "", nil, err
}
// obtain the bucket UUID for the bucket name
bucketUUID, err := em.bucketStore.getUUIDforBucket(bucketName)
if err != nil {
return "", nil, fmt.Errorf("failed to get bucket UUID for bucket %s: %w",
bucketName, err)
}
// create and return the writer callback for encrypting data with the obtained key type, bucket UUID and context
return em.newWriter(keyType, bucketUUID, context, pindexName)
}
// hook used by bleve to obtain a reader callback for decrypting data. The callback
// obtains the bucket name and context from the path provided
func (em *encryptionManager) bleveReaderHook(keyId string, path []byte) (
func(data []byte) ([]byte, error), error) {
// if the path or keyId is empty, return a reader callback that returns the data as is without decryption
if path == nil || len(path) == 0 || keyId == "" {
return emptyReaderCallback(), nil
}
// process the path to obtain the key type, bucket name and context for decryption
keyType, bucketName, context, _, err := processPath(string(path))
if err != nil {
return nil, err
}
// obtain the bucket UUID for the bucket name
bucketUUID, err := em.bucketStore.getUUIDforBucket(bucketName)
if err != nil {
return nil, fmt.Errorf("failed to get bucket UUID for bucket %s: %w",
bucketName, err)
}
// create and return the reader callback for decrypting data with the obtained key type, bucket UUID, key ID and context
return em.newReader(keyType, bucketUUID, keyId, context)
}
// -------------------------- reader and writer callback creators -----------------------
// generate a cipher block for AES-256-GCM encryption using the provided key and context
func (em *encryptionManager) generateAES256GCMCipherBlock(key []byte,
context string) (cipher.AEAD, error) {
// derive a 256-bit key from the provided key and context using OpenSSL's KBKDF
derivedKey := make([]byte, 32)
derivedKey, err := gocbcrypto.OpenSSLKBKDFDeriveKey(key, searchEncryptionContext,
[]byte(context), derivedKey, "SHA2-256", "")
if err != nil {
return nil, err
}
// create a new AES cipher block using the derived key
block, err := aes.NewCipher(derivedKey)
if err != nil {
return nil, fmt.Errorf("encryptionManager: failed to create AES "+
"cipher: %w", err)
}
// create a new GCM cipher mode instance using the AES block cipher
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("encryptionManager: failed to create AES "+
"GCM: %w", err)
}
return aesgcm, nil
}
// make a writer callback for the given cipher, key and context
func (em *encryptionManager) makeWriterCallback(cipher string, key []byte,
context string) (func(data []byte) []byte, error) {
switch cipher {
case aes256gcmCipher:
// generate a cipher block for AES-256-GCM encryption using the provided key and context
aesgcm, err := em.generateAES256GCMCipherBlock(key, context)
if err != nil {
return nil, err
}
// generate a random nonce for encryption. The nonce will be
// incremented for each encryption operation ensuring uniqueness
nonce := make([]byte, noncelength)
if _, err := rand.Read(nonce); err != nil {
return nil, fmt.Errorf("failed to generate random nonce: %w", err)
}
writerCallback := func(data []byte) []byte {
// increment the nonce for each encryption operation to ensure uniqueness
for i := len(nonce) - 1; i >= 0; i-- {
if nonce[i] < 255 {
nonce[i]++
break
}
nonce[i] = 0
}
// encrypt the data using AES-256-GCM with the generated nonce
ciphertext := aesgcm.Seal(nil, nonce, data, nil)
// append the nonce to the end of the ciphertext for use in decryption
result := append(ciphertext, nonce...)
return result
}
return writerCallback, nil
default:
return func(data []byte) []byte {
return data
}, nil
}
}
// make a reader callback for the given cipher, key and context
func (em *encryptionManager) makeReaderCallback(cipher string, key []byte,
context string) (func(data []byte) ([]byte, error), error) {
switch cipher {
case aes256gcmCipher:
// generate a cipher block for AES-256-GCM encryption using the provided key and context
aesgcm, err := em.generateAES256GCMCipherBlock(key, context)
if err != nil {
return nil, err
}
readerCallback := func(data []byte) ([]byte, error) {
if len(data) < noncelength {
return nil, fmt.Errorf("ciphertext too short")
}
// extract the nonce from the end of the data
nonce := data[len(data)-noncelength:]
ciphertext := data[:len(data)-noncelength]
// decrypt the data using AES-256-GCM with the extracted nonce
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt data: %w", err)
}
return plaintext, nil
}
return readerCallback, nil
default:
return func(data []byte) ([]byte, error) {
return data, nil
}, nil
}
}
// ------------------------- key store implementation -----------------------
const keysInUseCacheFrequency = time.Hour
// keyStore is responsible for managing encryption keys.
// Includes caching master keys, obtaining current active keys,
// searching keys by ids, maintaining a cache for all keys
// currently in use and monitoring for changes in active keys.
type keyStore struct {
mgr *cbgt.Manager
// store of all master keys, keyed by keyType+bucketUUID
keysLock sync.RWMutex
keys map[string]*cbauth.EncrKeysInfo
// cache of keys currently in use, keyed by keyType+bucketUUID.
// recompiled every keysInUseCacheFrequency duration by scanning
// all disk files for keys in use
keysInUseCache atomic.Pointer[map[string]map[string]struct{}]
// cache of keys distributed since last keys in use cache refresh,
// keyed by keyType+bucketUUID. This is needed to ensure that keys
// distributed after the last cache refresh are included in the keys
// in use until the next refresh happens.
newKeysLock sync.RWMutex
newKeys map[string]map[string]struct{}
// signals the monitor to refresh the keys in use cache outside of its
// regular interval.
refreshCh chan struct{}
}
// Initializes the key store and starts the monitor for active keys.
func newKeyStore(mgr *cbgt.Manager) *keyStore {
ks := &keyStore{
keys: make(map[string]*cbauth.EncrKeysInfo),
newKeys: make(map[string]map[string]struct{}),
refreshCh: make(chan struct{}, 1),
mgr: mgr,
}
ks.keysInUseCache.Store(&map[string]map[string]struct{}{})
go ks.startMonitor()
return ks
}
// obtain the key info for the given key type and bucket UUID.
// get key from cbauth on cache miss and update the cache.
func (ks *keyStore) getKeyInfo(keyType, bucketUUID string) (*cbauth.EncrKeysInfo,
error) {
var keyInfo *cbauth.EncrKeysInfo
// check cache first for key information
ks.keysLock.RLock()
keyInfo = ks.keys[keyType+bucketUUID]
ks.keysLock.RUnlock()
// if not present, obtain key information from cbauth
if keyInfo == nil {
keyData := cbauth.KeyDataType{
TypeName: keyType,
BucketUUID: bucketUUID,
}
// create a context with timeout to ensure a non infinite wait time
ctx, cancel := context.WithTimeout(context.Background(), getKeyTimeout)
defer cancel()
// block and wait for cbauth to return the keys. Failure to obtain keys
// is a critical error and may cause operations to fail
var err error
keyInfo, err = cbauth.GetEncryptionKeysBlocking(ctx, keyData)
if err != nil {
return nil, fmt.Errorf("keyStore: failed to get encryption keys for bucket %s: %w",
bucketUUID, err)
}
// update the cache with the new key information
ks.updateMasterKey(bucketUUID, keyType, keyInfo)
}
return keyInfo, nil
}
// set the new key in the newKeys cache to ensure it is included in the keys in use
func (ks *keyStore) setNewKey(keyType, bucketUUID string, keyId string) {
ks.newKeysLock.Lock()
if _, exists := ks.newKeys[keyType+bucketUUID]; !exists {
ks.newKeys[keyType+bucketUUID] = make(map[string]struct{})
}
ks.newKeys[keyType+bucketUUID][keyId] = struct{}{}
ks.newKeysLock.Unlock()
}
// starts a monitor that refreshes the keys in use cache at a regular interval,
// and whenever a refresh is requested.
func (ks *keyStore) startMonitor() {
// monitor is paused until initial load is done to
// avoid improper cache updates when keys are loaded
// on demand during booting
ks.waitPreLoad()
// pindexes registered while booting are covered by the initial refresh
select {
case <-ks.refreshCh:
default:
}
ks.refreshKeysInUseCache()
// refresh keys in use cache at a regular interval to
// ensure correctness
ticker := time.NewTicker(keysInUseCacheFrequency)
defer ticker.Stop()
// runs forever, regardless of whether encryption is enabled
for {
select {
case <-ticker.C:
ks.refreshKeysInUseCache()
case <-ks.refreshCh:
ks.refreshKeysInUseCache()
}
}
}
// requests the monitor to refresh the keys in use cache. Never blocks, so it
// is safe to call while holding the cbgt manager lock. A request made while
// one is already pending is merged into it.
func (ks *keyStore) requestRefresh() {
select {
case ks.refreshCh <- struct{}{}:
default:
}
}
// waits for manager to finish loading data dir before starting monitor
// ensuring all pindexes are registered
func (ks *keyStore) waitPreLoad() {
// initialize a ticker with an arbitrary frequency
// to check if loading is done
preLoadTicker := time.NewTicker(5 * time.Second)
defer preLoadTicker.Stop()
// wait until loading is done before starting the monitor to avoid
// improper cache updates when keys are loaded on demand during booting
for {
select {
case <-preLoadTicker.C:
if ks.mgr != nil && ks.mgr.BootingDone() {
return
}
}
}
}
// refreshes keys in use cache by scanning all disk files and then
// combining with any new keys distributed since last refresh to
// ensure an up to date list of keys in use.
func (ks *keyStore) refreshKeysInUseCache() {
if ks.mgr == nil {
log.Printf("keyStore: manager is not initialized, cannot refresh " +
"keys in use cache")
return
}
// if booting is not done, we still refresh the cache, but
// we will be unable to accuratly judge if the keys in use
// are present since pindexes are still being loaded and registered
if !ks.mgr.BootingDone() {
log.Printf("keyStore: Load in progress, keys in use cache " +
"update skipped")
}
// get the current list of pindexes
curKeyMap := make(map[string]map[string]struct{})
_, pIndexes := ks.mgr.CurrentMaps()
// obtain keys in use from all pindexes as well as their meta files
// all errors are logged and skipped to ensure keys refresh still completes
for _, pIndex := range pIndexes {
if _, exists := curKeyMap[bucketKeyType+pIndex.SourceUUID]; !exists {
curKeyMap[bucketKeyType+pIndex.SourceUUID] = make(map[string]struct{})
}
bIndex, _, _, err := bleveIndex(pIndex)
if err != nil {
log.Printf("keyStore: failed to get bleve index for pindex %s: %v",
pIndex.Name, err)
continue
}
// obtain keys in use from the bleve index
if cIndex, ok := bIndex.(bleve.IndexWithCallbacks); ok {
keysInUse, err := cIndex.FileWriterIDsInUse()
if err != nil {
log.Printf("keyStore: failed to get writer ids in use for pindex %s: %v",
pIndex.Name, err)
continue
}
for key := range keysInUse {
curKeyMap[bucketKeyType+pIndex.SourceUUID][key] = struct{}{}
}
}
path := pIndex.Path
if path == "" {
log.Printf("keyStore: empty path for pindex %s, skipping keys "+
"in use cache update", pIndex.Name)
continue
}
// obtain keys in use from the meta files not tracked by bleve
for _, metaFile := range []string{"PINDEX_BLEVE_META", "PINDEX_META"} {
pathMeta := filepath.Join(path, metaFile)
_, metaId, err := encryptionManagerInstance.decryptAndReadFile(pathMeta)
if err != nil {
log.Printf("keyStore: failed to read meta file %s for pindex %s: %v",
metaFile, pIndex.Name, err)
continue
}
curKeyMap[bucketKeyType+pIndex.SourceUUID][metaId] = struct{}{}
}
}
if curKeyMap[otherKeyType] == nil {
curKeyMap[otherKeyType] = make(map[string]struct{})
}
// also obtain keys in use from other on disk files like recovery plans
planPIndexesPath := filepath.Join(ks.mgr.DataDir(), "planPIndexes")
files, err := os.ReadDir(planPIndexesPath)
if err == nil {
for _, file := range files {
path := filepath.Join(planPIndexesPath, file.Name())
_, metaId, err := encryptionManagerInstance.decryptAndReadFile(path)
if err != nil {
log.Printf("keyStore: failed to read planPIndexes meta file at path %s: %v",
path, err)
continue
}
curKeyMap[otherKeyType][metaId] = struct{}{}
}
}
// also obtain keys in use from the search history log. Each line in the
// log is independently encrypted and carries its own key id, so all
// distinct key ids (including the empty key id for legacy plaintext
// lines) are added.
if search_history.Service != nil {
ids, err := search_history.Service.KeyIdsInUse()
if err != nil {
log.Printf("keyStore: failed to scan search history keys: %v", err)
} else {
for id := range ids {
curKeyMap[otherKeyType][id] = struct{}{}
}
}
}
// combine keys obtained with all keys distributed since the last refresh
// to ensure correctness of the cache
ks.newKeysLock.Lock()
for identifier, keys := range ks.newKeys {
if _, exists := curKeyMap[identifier]; !exists {
curKeyMap[identifier] = make(map[string]struct{})
}
for key := range keys {
curKeyMap[identifier][key] = struct{}{}
}
}
// reset the new keys cache after consuming to avoid unbounded growth
ks.newKeys = make(map[string]map[string]struct{})
ks.newKeysLock.Unlock()
// update the keys in use cache with the new list of keys in use
ks.keysInUseCache.Store(&curKeyMap)
}
// quick function to combine keys from newKeys cache and current keys in use cache
// used by cbauth callback for quick but accurate retrieval of keys in use
func (ks *keyStore) getKeysInUse(keyType, bucketUUID string) (map[string]struct{}, error) {
if ks.mgr == nil {
return nil, fmt.Errorf("keyStore: manager is not initialized, cannot " +
"get keys in use")
}
// if booting is not done, we will be unable to accuratly judge if the keys in use
// are present since pindexes are still being loaded and registered
// user is expected to retry after some time if the information is still needed
if !ks.mgr.BootingDone() {
return nil, fmt.Errorf("keyStore: data dir load in progress, keys in " +
"use retrieval might be inaccurate")
}
// combine keys from the new keys cache and the current keys in use cache to
// ensure an up to date list of keys in use
keysMap := make(map[string]struct{})
ks.newKeysLock.RLock()