github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/data/node_obfuscator.go (about) 1 // Copyright 2019 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package data 6 7 import ( 8 "crypto/hmac" 9 "crypto/sha256" 10 "fmt" 11 "strconv" 12 "strings" 13 "sync" 14 15 "github.com/keybase/client/go/libkb" 16 ) 17 18 const ( 19 separator = "-" 20 ) 21 22 // NodeObfuscatorSecret is a byte slice wrapper for a secret that can 23 // be passed to `NodeObfuscator`. It overrides `String` so there's no 24 // chance of the secret being accidentally logged. 25 type NodeObfuscatorSecret []byte 26 27 // String implements the `fmt.Stringer` interface for NodeObfuscatorSecret. 28 func (nos NodeObfuscatorSecret) String() string { 29 return fmt.Sprintf("{private %d bytes}", len(nos)) 30 } 31 32 // NodeObfuscator takes a secret, and uses it to create obfuscate 33 // strings based on BIP-0039 dictionary words. It remembers previous 34 // obfuscations, and if there happens to be a collision on the 35 // obfuscated text for a new plaintext string, it appends a numeric 36 // suffix to the new obfuscation. 37 type NodeObfuscator struct { 38 secret NodeObfuscatorSecret 39 40 lock sync.RWMutex 41 obsCache map[string]string 42 usedObs map[string]bool 43 } 44 45 var _ Obfuscator = (*NodeObfuscator)(nil) 46 47 // NewNodeObfuscator creates a new `NodeObfuscator` instance. 48 func NewNodeObfuscator(secret NodeObfuscatorSecret) *NodeObfuscator { 49 return &NodeObfuscator{ 50 secret: secret, 51 // Don't initialize caches yet, for memory reasons, in case 52 // they're never used. 53 } 54 } 55 56 func (no *NodeObfuscator) checkCacheIfExistsLocked( 57 plaintext string) (string, bool) { 58 if no.obsCache == nil { 59 return "", false 60 } 61 62 obs, ok := no.obsCache[plaintext] 63 return obs, ok 64 } 65 66 func (no *NodeObfuscator) checkCacheIfExists(plaintext string) (string, bool) { 67 no.lock.RLock() 68 defer no.lock.RUnlock() 69 return no.checkCacheIfExistsLocked(plaintext) 70 } 71 72 func (no *NodeObfuscator) obfuscateWithHasher( 73 plaintext string, hasher func(string) []byte) string { 74 obs, ok := no.checkCacheIfExists(plaintext) 75 if ok { 76 return obs 77 } 78 79 no.lock.Lock() 80 defer no.lock.Unlock() 81 // See if it's been cached since we last released the lock. 82 obs, ok = no.checkCacheIfExistsLocked(plaintext) 83 if ok { 84 return obs 85 } 86 87 if no.obsCache == nil { 88 no.obsCache = make(map[string]string) 89 no.usedObs = make(map[string]bool) 90 } 91 92 // HMAC the plaintext with the secret, to get a hash. 93 buf := hasher(plaintext) 94 95 // Look up two words based on the first three bytes of the mac. 96 // (Each word takes 11 bits to lookup a unique word among the 2048 97 // secret words.) 98 99 // Put the first 22 bits in an int. 100 b := (int(buf[0])<<16 | int(buf[1])<<8 | int(buf[2])) >> 2 101 102 second := b & (1<<11 - 1) // second 11 bits are the second word. 103 first := b >> 11 // first 11 bits are the first word. 104 105 firstWord := libkb.SecWord(first) 106 secondWord := libkb.SecWord(second) 107 obs = strings.Join([]string{firstWord, secondWord}, separator) 108 109 suffix := 1 110 for no.usedObs[obs] { 111 suffix++ 112 obs = strings.Join( 113 []string{firstWord, secondWord, strconv.Itoa(suffix)}, separator) 114 } 115 116 no.obsCache[plaintext] = obs 117 no.usedObs[obs] = true 118 return obs 119 } 120 121 func (no *NodeObfuscator) defaultHash(plaintext string) []byte { 122 mac := hmac.New(sha256.New, no.secret) 123 _, _ = mac.Write([]byte(plaintext)) 124 return mac.Sum(nil) 125 } 126 127 // Obfuscate implements the `Obfuscator` interface for NodeObfuscator. 128 func (no *NodeObfuscator) Obfuscate(plaintext string) string { 129 return no.obfuscateWithHasher(plaintext, no.defaultHash) 130 }