github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  }