github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/note.go (about) 1 package stellar 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "encoding/base64" 7 "errors" 8 "fmt" 9 10 "github.com/keybase/client/go/libkb" 11 "github.com/keybase/client/go/msgpack" 12 "github.com/keybase/client/go/protocol/keybase1" 13 "github.com/keybase/client/go/protocol/stellar1" 14 "golang.org/x/crypto/nacl/box" 15 "golang.org/x/crypto/nacl/secretbox" 16 ) 17 18 type noteBuildSecret struct { 19 symmetricKey libkb.NaclSecretBoxKey 20 sender stellar1.NoteRecipient 21 recipient *stellar1.NoteRecipient 22 } 23 24 // noteSymmetricKey returns a symmetric key to be used in encrypting a note. 25 // The key is available to the current user and to the optional `other` recipient. 26 // If `other` is nil the key is derived from the latest PUK seed. 27 // If `other` is non-nil the key is derived from the nacl shared key of both users' latest PUK encryption keys. 28 func noteSymmetricKey(mctx libkb.MetaContext, other *keybase1.UserVersion) (res noteBuildSecret, err error) { 29 meUV, err := mctx.G().GetMeUV(mctx.Ctx()) 30 if err != nil { 31 return res, err 32 } 33 puk1Gen, puk1Seed, err := loadOwnLatestPuk(mctx) 34 if err != nil { 35 return res, err 36 } 37 symmetricKey, err := puk1Seed.DeriveSymmetricKey(libkb.DeriveReasonPUKStellarNoteSelf) 38 if err != nil { 39 return res, err 40 } 41 var recipient *stellar1.NoteRecipient 42 if other != nil && !other.Eq(meUV) { 43 u2, err := loadUvUpk(mctx, *other) 44 if err != nil { 45 return res, fmt.Errorf("error loading recipient: %v", err) 46 } 47 puk2 := u2.GetLatestPerUserKey() 48 if puk2 == nil { 49 return res, fmt.Errorf("recipient has no per-user key") 50 } 51 // Overwite symmetricKey with the shared key. 52 symmetricKey, err = noteMixKeys(mctx, puk1Seed, puk2.EncKID) 53 if err != nil { 54 return res, err 55 } 56 recipient = &stellar1.NoteRecipient{ 57 User: *other, 58 PukGen: keybase1.PerUserKeyGeneration(puk2.Gen), 59 } 60 } 61 return noteBuildSecret{ 62 symmetricKey: symmetricKey, 63 sender: stellar1.NoteRecipient{ 64 User: meUV, 65 PukGen: puk1Gen, 66 }, 67 recipient: recipient, 68 }, nil 69 } 70 71 func noteSymmetricKeyForDecryption(mctx libkb.MetaContext, encNote stellar1.EncryptedNote) (res libkb.NaclSecretBoxKey, err error) { 72 meUV, err := mctx.G().GetMeUV(mctx.Ctx()) 73 if err != nil { 74 return res, err 75 } 76 var mePukGen keybase1.PerUserKeyGeneration 77 var them *stellar1.NoteRecipient 78 if encNote.Sender.User.Eq(meUV) { 79 mePukGen = encNote.Sender.PukGen 80 them = encNote.Recipient 81 } 82 if encNote.Recipient != nil && encNote.Recipient.User.Eq(meUV) { 83 mePukGen = encNote.Recipient.PukGen 84 them = &encNote.Sender 85 } 86 if mePukGen == 0 { 87 return res, fmt.Errorf("note not encrypted for logged-in user") 88 } 89 pukring, err := mctx.G().GetPerUserKeyring(mctx.Ctx()) 90 if err != nil { 91 return res, err 92 } 93 pukSeed, err := pukring.GetSeedByGenerationOrSync(mctx, mePukGen) 94 if err != nil { 95 return res, err 96 } 97 if them == nil { 98 return pukSeed.DeriveSymmetricKey(libkb.DeriveReasonPUKStellarNoteSelf) 99 } 100 u2, err := loadUvUpk(mctx, them.User) 101 if err != nil { 102 return res, err 103 } 104 puk2 := u2.GetPerUserKeyByGen(them.PukGen) 105 if puk2 == nil { 106 return res, fmt.Errorf("could not find other user's key: %v %v", them.User.String(), them.PukGen) 107 } 108 return noteMixKeys(mctx, pukSeed, puk2.EncKID) 109 } 110 111 // noteMixKeys derives a shared symmetric key for two DH keys. 112 // The key is the last 32 bytes of the nacl box of 32 zeros with a use-specific nonce. 113 func noteMixKeys(mctx libkb.MetaContext, puk1 libkb.PerUserKeySeed, puk2EncKID keybase1.KID) (res libkb.NaclSecretBoxKey, err error) { 114 puk1Enc, err := puk1.DeriveDHKey() 115 if err != nil { 116 return res, err 117 } 118 puk2EncGeneric, err := libkb.ImportKeypairFromKID(puk2EncKID) 119 if err != nil { 120 return res, err 121 } 122 puk2Enc, ok := puk2EncGeneric.(libkb.NaclDHKeyPair) 123 if !ok { 124 return res, fmt.Errorf("recipient per-user key was not a DH key") 125 } 126 var zeros [32]byte 127 // This is a constant nonce used for key derivation. 128 // The derived key will be used with one-time random nonces for the actual encryption/decryption. 129 nonce := noteMixPukNonce() 130 sharedSecretBox := box.Seal(nil, zeros[:], &nonce, (*[32]byte)(&puk2Enc.Public), (*[32]byte)(puk1Enc.Private)) 131 return libkb.MakeByte32Soft(sharedSecretBox[len(sharedSecretBox)-32:]) 132 } 133 134 // noteMixPukNonce is a nonce used in key derivation for shared notes. 135 // 24-byte prefix of the sha256 hash of a constant string. 136 func noteMixPukNonce() (res [24]byte) { 137 reasonHash := sha256.Sum256([]byte(libkb.DeriveReasonPUKStellarNoteShared)) 138 copy(res[:], reasonHash[:]) 139 return res 140 } 141 142 func NoteEncryptB64(mctx libkb.MetaContext, note stellar1.NoteContents, other *keybase1.UserVersion) (noteB64 string, err error) { 143 if len(note.Note) > libkb.MaxStellarPaymentNoteLength { 144 return "", fmt.Errorf("Note of size %d bytes exceeds the maximum length of %d bytes", 145 len(note.Note), libkb.MaxStellarPaymentNoteLength) 146 } 147 obj, err := noteEncrypt(mctx, note, other) 148 if err != nil { 149 return "", err 150 } 151 pack, err := msgpack.Encode(obj) 152 if err != nil { 153 return "", err 154 } 155 noteB64 = base64.StdEncoding.EncodeToString(pack) 156 if len(noteB64) > libkb.MaxStellarPaymentBoxedNoteLength { 157 return "", fmt.Errorf("Encrypted note of size %d bytes exceeds the maximum length of %d bytes", 158 len(noteB64), libkb.MaxStellarPaymentBoxedNoteLength) 159 } 160 return noteB64, nil 161 } 162 163 // noteEncrypt encrypts a note for the logged-in user as well as optionally for `other`. 164 func noteEncrypt(mctx libkb.MetaContext, note stellar1.NoteContents, other *keybase1.UserVersion) (res stellar1.EncryptedNote, err error) { 165 nbs, err := noteSymmetricKey(mctx, other) 166 if err != nil { 167 return res, fmt.Errorf("error getting encryption key for note: %v", err) 168 } 169 if nbs.symmetricKey.IsZero() { 170 // This should never happen 171 return res, fmt.Errorf("unexpected zero key") 172 } 173 res, err = noteEncryptHelper(mctx.Ctx(), note, nbs.symmetricKey) 174 if err != nil { 175 return res, err 176 } 177 res.Sender = nbs.sender 178 res.Recipient = nbs.recipient 179 return res, nil 180 } 181 182 // noteEncryptHelper does the encryption part and returns a partially populated result. 183 func noteEncryptHelper(ctx context.Context, note stellar1.NoteContents, symmetricKey libkb.NaclSecretBoxKey) (res stellar1.EncryptedNote, err error) { 184 // Msgpack 185 clearpack, err := msgpack.Encode(note) 186 if err != nil { 187 return res, err 188 } 189 190 // Secretbox 191 var nonce [libkb.NaclDHNonceSize]byte 192 nonce, err = libkb.RandomNaclDHNonce() 193 if err != nil { 194 return res, err 195 } 196 secbox := secretbox.Seal(nil, clearpack, &nonce, (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey)) 197 198 return stellar1.EncryptedNote{ 199 V: 1, 200 E: secbox, 201 N: nonce, 202 }, nil 203 } 204 205 func NoteDecryptB64(mctx libkb.MetaContext, noteB64 string) (res stellar1.NoteContents, err error) { 206 pack, err := base64.StdEncoding.DecodeString(noteB64) 207 if err != nil { 208 return res, err 209 } 210 var obj stellar1.EncryptedNote 211 err = msgpack.Decode(&obj, pack) 212 if err != nil { 213 return res, err 214 } 215 return noteDecrypt(mctx, obj) 216 } 217 218 func noteDecrypt(mctx libkb.MetaContext, encNote stellar1.EncryptedNote) (res stellar1.NoteContents, err error) { 219 if encNote.V != 1 { 220 return res, fmt.Errorf("unsupported note version: %v", encNote.V) 221 } 222 symmetricKey, err := noteSymmetricKeyForDecryption(mctx, encNote) 223 if err != nil { 224 return res, err 225 } 226 return noteDecryptHelper(mctx.Ctx(), encNote, symmetricKey) 227 } 228 229 func noteDecryptHelper(ctx context.Context, encNote stellar1.EncryptedNote, symmetricKey libkb.NaclSecretBoxKey) (res stellar1.NoteContents, err error) { 230 // Secretbox 231 clearpack, ok := secretbox.Open(nil, encNote.E, 232 (*[libkb.NaclDHNonceSize]byte)(&encNote.N), 233 (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey)) 234 if !ok { 235 return res, errors.New("could not decrypt note secretbox") 236 } 237 238 // Msgpack 239 err = msgpack.Decode(&res, clearpack) 240 return res, err 241 }