github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kvstore/boxer.go (about) 1 package kvstore 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 7 "github.com/keybase/client/go/chat/signencrypt" 8 "github.com/keybase/client/go/kbcrypto" 9 "github.com/keybase/client/go/libkb" 10 "github.com/keybase/client/go/msgpack" 11 "github.com/keybase/client/go/protocol/keybase1" 12 "github.com/keybase/go-crypto/ed25519" 13 ) 14 15 type KVStoreBoxer interface { 16 Box(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, cleartextValue string) (ciphertext string, 17 teamKeyGen keybase1.PerTeamKeyGeneration, ciphertextVersion int, err error) 18 Unbox(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, ciphertext string, teamKeyGen keybase1.PerTeamKeyGeneration, formatVersion int, 19 senderUID keybase1.UID, senderEldestSeqno keybase1.Seqno, senderDeviceID keybase1.DeviceID) (cleartext string, err error) 20 } 21 22 var _ KVStoreBoxer = (*KVStoreRealBoxer)(nil) 23 24 type KVStoreRealBoxer struct { 25 libkb.Contextified 26 } 27 28 func NewKVStoreBoxer(g *libkb.GlobalContext) *KVStoreRealBoxer { 29 return &KVStoreRealBoxer{ 30 Contextified: libkb.NewContextified(g), 31 } 32 } 33 34 type kvStoreMetadata struct { 35 EntryID keybase1.KVEntryID `codec:"e" json:"e"` 36 Revision int `codec:"r" json:"r"` 37 EncKey keybase1.Bytes32 `codec:"k" json:"k"` 38 CiphertextVersion int `codec:"v" json:"v"` 39 UID keybase1.UID `codec:"u" json:"u"` 40 EldestSeqno keybase1.Seqno `codec:"s" json:"s"` 41 DeviceID keybase1.DeviceID `codec:"d" json:"d"` 42 } 43 44 func newNonce() (ret [signencrypt.NonceSize]byte, err error) { 45 randBytes, err := libkb.RandBytes(signencrypt.NonceSize) 46 if err != nil { 47 return ret, err 48 } 49 copy(ret[:], randBytes) 50 return ret, nil 51 } 52 53 func (b *KVStoreRealBoxer) fetchEncryptionKey(mctx libkb.MetaContext, entryID keybase1.KVEntryID, generation *keybase1.PerTeamKeyGeneration) (res [signencrypt.SecretboxKeySize]byte, gen keybase1.PerTeamKeyGeneration, err error) { 54 // boxing can always use the latest key, unboxing will pass in a team generation to load 55 loadArg := keybase1.FastTeamLoadArg{ 56 ID: entryID.TeamID, 57 Applications: []keybase1.TeamApplication{keybase1.TeamApplication_KVSTORE}, 58 } 59 if generation == nil { 60 loadArg.NeedLatestKey = true 61 } else { 62 loadArg.KeyGenerationsNeeded = []keybase1.PerTeamKeyGeneration{*generation} 63 } 64 teamLoadRes, err := mctx.G().GetFastTeamLoader().Load(mctx, loadArg) 65 if err != nil { 66 return res, gen, err 67 } 68 if len(teamLoadRes.ApplicationKeys) != 1 { 69 return res, gen, fmt.Errorf("wrong number of keys from fast-team-loading encryption key; wanted 1, got %d", len(teamLoadRes.ApplicationKeys)) 70 } 71 appKey := teamLoadRes.ApplicationKeys[0] 72 if generation != nil && appKey.KeyGeneration != *generation { 73 return res, gen, fmt.Errorf("wrong app key generation; wanted %d but got %d", *generation, appKey.KeyGeneration) 74 } 75 if appKey.Application != keybase1.TeamApplication_KVSTORE { 76 return res, gen, fmt.Errorf("wrong app key application; wanted %d but got %d", keybase1.TeamApplication_KVSTORE, appKey.Application) 77 } 78 var encKey [signencrypt.SecretboxKeySize]byte = appKey.Key 79 return encKey, appKey.KeyGeneration, nil 80 } 81 82 func (b *KVStoreRealBoxer) fetchVerifyKey(mctx libkb.MetaContext, uid keybase1.UID, deviceID keybase1.DeviceID) (ret signencrypt.VerifyKey, err error) { 83 upk, err := b.G().GetUPAKLoader().LoadUPAKWithDeviceID(mctx.Ctx(), uid, deviceID) 84 if err != nil { 85 return nil, err 86 } 87 verifyKID, _ := upk.Current.FindSigningDeviceKID(deviceID) 88 verifyKey := kbcrypto.KIDToNaclSigningKeyPublic(verifyKID.ToBytes()) 89 if verifyKey == nil { 90 return nil, kbcrypto.BadKeyError{} 91 } 92 var verKey [ed25519.PublicKeySize]byte = *verifyKey 93 return &verKey, nil 94 } 95 96 func (b *KVStoreRealBoxer) Box(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, cleartext string) ( 97 ciphertext string, teamKeyGen keybase1.PerTeamKeyGeneration, version int, err error) { 98 99 defer mctx.Trace(fmt.Sprintf("KVStoreRealBoxer#Box: %s, %s, %s", entryID.TeamID, entryID.Namespace, entryID.EntryKey), 100 &err)() 101 102 clearBytes := []byte(cleartext) 103 ciphertextVersion := 1 104 nonce, err := newNonce() 105 if err != nil { 106 mctx.Debug("error making a nonce: %v", err) 107 return "", keybase1.PerTeamKeyGeneration(0), 0, err 108 } 109 // get encryption key (and team generation) and this device's signing key 110 encKey, teamGen, err := b.fetchEncryptionKey(mctx, entryID, nil) 111 if err != nil { 112 mctx.Debug("error fetching encryption key for entry %+v: %v", entryID, err) 113 return "", keybase1.PerTeamKeyGeneration(0), 0, err 114 } 115 uv, deviceID, _, signingKey, _ := mctx.G().ActiveDevice.AllFields() 116 signingKP, ok := signingKey.(libkb.NaclSigningKeyPair) 117 if !ok || signingKP.Private == nil { 118 mctx.Debug("error with signing key: %v", err) 119 return "", keybase1.PerTeamKeyGeneration(0), 0, libkb.KeyCannotSignError{} 120 } 121 var signKey [ed25519.PrivateKeySize]byte = *signingKP.Private 122 // build associated data 123 associatedData := kvStoreMetadata{ 124 EntryID: entryID, 125 Revision: revision, 126 EncKey: encKey, 127 CiphertextVersion: ciphertextVersion, 128 UID: uv.Uid, 129 EldestSeqno: uv.EldestSeqno, 130 DeviceID: deviceID, 131 } 132 133 // seal it all up 134 signEncryptedBytes, err := signencrypt.SealWithAssociatedData( 135 clearBytes, associatedData, &encKey, &signKey, kbcrypto.SignaturePrefixTeamStore, &nonce) 136 if err != nil { 137 mctx.Debug("error sealing message and associated data: %v", err) 138 return "", keybase1.PerTeamKeyGeneration(0), 0, err 139 } 140 boxed := keybase1.EncryptedKVEntry{ 141 V: 1, 142 E: signEncryptedBytes, 143 N: nonce[:], 144 } 145 // pack it, string it, ship it. 146 packed, err := msgpack.Encode(boxed) 147 if err != nil { 148 mctx.Debug("error msgpacking secretbox for entry %+v: %v", entryID, err) 149 return "", keybase1.PerTeamKeyGeneration(0), 0, err 150 } 151 return base64.StdEncoding.EncodeToString(packed), teamGen, ciphertextVersion, nil 152 } 153 154 func (b *KVStoreRealBoxer) Unbox(mctx libkb.MetaContext, entryID keybase1.KVEntryID, revision int, ciphertext string, 155 teamKeyGen keybase1.PerTeamKeyGeneration, formatVersion int, senderUID keybase1.UID, senderEldestSeqno keybase1.Seqno, 156 senderDeviceID keybase1.DeviceID) (cleartext string, err error) { 157 158 defer mctx.Trace(fmt.Sprintf("KVStoreRealBoxer#Unbox: t:%s, n:%s, k:%s", entryID.TeamID, entryID.Namespace, entryID.EntryKey), 159 &err)() 160 161 if formatVersion != 1 { 162 return "", fmt.Errorf("unsupported format version %d isn't 1", formatVersion) 163 } 164 // basic decoding into a not-yet-unsealed box 165 decoded, err := base64.StdEncoding.DecodeString(ciphertext) 166 if err != nil { 167 mctx.Debug("boxed message isn't base64: %v", err) 168 return "", err 169 } 170 var box keybase1.EncryptedKVEntry 171 err = msgpack.Decode(&box, decoded) 172 if err != nil { 173 mctx.Debug("msgpack decode error on boxed message: %v", err) 174 return "", err 175 } 176 if box.V != 1 { 177 return "", fmt.Errorf("unsupported secret box version: %v", box.V) 178 } 179 // fetch encryption and verification keys 180 encKey, _, err := b.fetchEncryptionKey(mctx, entryID, &teamKeyGen) 181 if err != nil { 182 mctx.Debug("error fetching decryption key: %v", err) 183 return "", err 184 } 185 verKey, err := b.fetchVerifyKey(mctx, senderUID, senderDeviceID) 186 if err != nil { 187 mctx.Debug("error fetching verify key: %v", err) 188 return "", err 189 } 190 var nonce [signencrypt.NonceSize]byte 191 if copy(nonce[:], box.N) != signencrypt.NonceSize { 192 return "", libkb.DecryptBadNonceError{} 193 } 194 associatedData := kvStoreMetadata{ 195 EntryID: entryID, 196 Revision: revision, 197 EncKey: encKey, 198 CiphertextVersion: box.V, 199 UID: senderUID, 200 EldestSeqno: senderEldestSeqno, 201 DeviceID: senderDeviceID, 202 } 203 204 // open it up 205 clearBytes, err := signencrypt.OpenWithAssociatedData(box.E, associatedData, &encKey, verKey, kbcrypto.SignaturePrefixTeamStore, &nonce) 206 if err != nil { 207 return "", err 208 } 209 return string(clearBytes), nil 210 }