github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/bundle/boxer.go (about) 1 package bundle 2 3 import ( 4 "crypto/hmac" 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/secretbox" 15 ) 16 17 // BoxedEncoded is the result of boxing and encoding a Bundle object. 18 type BoxedEncoded struct { 19 EncParent stellar1.EncryptedBundle 20 EncParentB64 string // base64 msgpacked Enc 21 VisParentB64 string 22 FormatVersionParent stellar1.BundleVersion 23 AcctBundles map[stellar1.AccountID]AcctBoxedEncoded 24 } 25 26 func newBoxedEncoded() *BoxedEncoded { 27 return &BoxedEncoded{ 28 FormatVersionParent: stellar1.BundleVersion_V2, 29 AcctBundles: make(map[stellar1.AccountID]AcctBoxedEncoded), 30 } 31 } 32 33 func newVisibleParent(a *stellar1.Bundle, accountsVisible []stellar1.BundleVisibleEntryV2) stellar1.BundleVisibleV2 { 34 return stellar1.BundleVisibleV2{ 35 Revision: a.Revision, 36 Prev: a.Prev, 37 Accounts: accountsVisible, 38 } 39 } 40 41 func (b BoxedEncoded) toBundleEncodedB64() BundleEncoded { 42 benc := BundleEncoded{ 43 EncParent: b.EncParentB64, 44 VisParent: b.VisParentB64, 45 AcctBundles: make(map[stellar1.AccountID]string), 46 } 47 48 for acctID, acctBundle := range b.AcctBundles { 49 benc.AcctBundles[acctID] = acctBundle.EncB64 50 } 51 52 return benc 53 } 54 55 // BundleEncoded contains all the encoded fields for communicating 56 // with the api server to post and get account bundles. 57 type BundleEncoded struct { 58 EncParent string `json:"encrypted_parent"` // base64 msgpacked Enc 59 VisParent string `json:"visible_parent"` 60 FormatVersionParent stellar1.BundleVersion `json:"version_parent"` 61 AcctBundles map[stellar1.AccountID]string `json:"account_bundles"` 62 } 63 64 // BoxAndEncode encrypts and encodes a Bundle object. 65 func BoxAndEncode(a *stellar1.Bundle, pukGen keybase1.PerUserKeyGeneration, puk libkb.PerUserKeySeed) (*BoxedEncoded, error) { 66 err := a.CheckInvariants() 67 if err != nil { 68 return nil, err 69 } 70 71 accountsVisible, accountsSecret := visibilitySplit(a) 72 73 // visible portion parent 74 visibleV2 := newVisibleParent(a, accountsVisible) 75 76 boxed := newBoxedEncoded() 77 78 // encrypted account bundles 79 for i, acctEntry := range visibleV2.Accounts { 80 secret, ok := a.AccountBundles[acctEntry.AccountID] 81 if !ok { 82 continue 83 } 84 ab, err := accountBoxAndEncode(acctEntry.AccountID, secret, pukGen, puk) 85 if err != nil { 86 return nil, err 87 } 88 boxed.AcctBundles[acctEntry.AccountID] = *ab 89 90 visibleV2.Accounts[i].EncAcctBundleHash = ab.EncHash 91 } 92 93 // have to do this after to get hashes of encrypted account bundles 94 visiblePack, err := msgpack.Encode(visibleV2) 95 if err != nil { 96 return nil, err 97 } 98 visibleHash := sha256.Sum256(visiblePack) 99 boxed.VisParentB64 = base64.StdEncoding.EncodeToString(visiblePack) 100 101 // secret portion parent 102 versionedSecret := stellar1.NewBundleSecretVersionedWithV2(stellar1.BundleSecretV2{ 103 VisibleHash: visibleHash[:], 104 Accounts: accountsSecret, 105 }) 106 boxed.EncParent, boxed.EncParentB64, err = parentBoxAndEncode(versionedSecret, pukGen, puk) 107 if err != nil { 108 return nil, err 109 } 110 111 return boxed, nil 112 } 113 114 func visibilitySplit(a *stellar1.Bundle) ([]stellar1.BundleVisibleEntryV2, []stellar1.BundleSecretEntryV2) { 115 vis := make([]stellar1.BundleVisibleEntryV2, len(a.Accounts)) 116 sec := make([]stellar1.BundleSecretEntryV2, len(a.Accounts)) 117 for i, acct := range a.Accounts { 118 vis[i] = stellar1.BundleVisibleEntryV2{ 119 AccountID: acct.AccountID, 120 Mode: acct.Mode, 121 IsPrimary: acct.IsPrimary, 122 AcctBundleRevision: acct.AcctBundleRevision, 123 EncAcctBundleHash: acct.EncAcctBundleHash, 124 } 125 sec[i] = stellar1.BundleSecretEntryV2{ 126 AccountID: acct.AccountID, 127 Name: acct.Name, 128 } 129 } 130 return vis, sec 131 } 132 133 func parentBoxAndEncode(bundle stellar1.BundleSecretVersioned, pukGen keybase1.PerUserKeyGeneration, puk libkb.PerUserKeySeed) (stellar1.EncryptedBundle, string, error) { 134 // Msgpack (inner) 135 clearpack, err := msgpack.Encode(bundle) 136 if err != nil { 137 return stellar1.EncryptedBundle{}, "", err 138 } 139 140 // Derive key 141 symmetricKey, err := puk.DeriveSymmetricKey(libkb.DeriveReasonPUKStellarBundle) 142 if err != nil { 143 return stellar1.EncryptedBundle{}, "", err 144 } 145 146 // Secretbox 147 var nonce [libkb.NaclDHNonceSize]byte 148 nonce, err = libkb.RandomNaclDHNonce() 149 if err != nil { 150 return stellar1.EncryptedBundle{}, "", err 151 } 152 secbox := secretbox.Seal(nil, clearpack, &nonce, (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey)) 153 154 // Annotate 155 res := stellar1.EncryptedBundle{ 156 V: 2, 157 E: secbox, 158 N: nonce, 159 Gen: pukGen, 160 } 161 162 // Msgpack (outer) + b64 163 cipherpack, err := msgpack.Encode(res) 164 if err != nil { 165 return stellar1.EncryptedBundle{}, "", err 166 } 167 resB64 := base64.StdEncoding.EncodeToString(cipherpack) 168 return res, resB64, nil 169 } 170 171 // AcctBoxedEncoded is the result of boxing and encoding the per-account secrets. 172 type AcctBoxedEncoded struct { 173 Enc stellar1.EncryptedAccountBundle 174 EncHash stellar1.Hash 175 EncB64 string // base64 msgpacked Enc 176 FormatVersion stellar1.AccountBundleVersion 177 } 178 179 func accountBoxAndEncode(accountID stellar1.AccountID, accountBundle stellar1.AccountBundle, pukGen keybase1.PerUserKeyGeneration, puk libkb.PerUserKeySeed) (*AcctBoxedEncoded, error) { 180 versionedSecret := stellar1.NewAccountBundleSecretVersionedWithV1(stellar1.AccountBundleSecretV1{ 181 AccountID: accountID, 182 Signers: accountBundle.Signers, 183 }) 184 185 encBundle, b64, err := accountEncrypt(versionedSecret, pukGen, puk) 186 if err != nil { 187 return nil, err 188 } 189 190 encPack, err := msgpack.Encode(encBundle) 191 if err != nil { 192 return nil, err 193 } 194 encHash := sha256.Sum256(encPack) 195 196 res := AcctBoxedEncoded{Enc: encBundle, EncHash: encHash[:], EncB64: b64, FormatVersion: 1} 197 198 return &res, nil 199 } 200 201 // PukFinder helps this package find puks. 202 type PukFinder interface { 203 SeedByGeneration(m libkb.MetaContext, generation keybase1.PerUserKeyGeneration) (libkb.PerUserKeySeed, error) 204 } 205 206 type AccountPukGens map[stellar1.AccountID](keybase1.PerUserKeyGeneration) 207 208 // DecodeAndUnbox decodes the encrypted and visible encoded bundles and unboxes 209 // the encrypted bundle using PukFinder to find the correct puk. It combines 210 // the results into a stellar1.Bundle and also returns additional information 211 // about the bundle: its version, pukGen, and the pukGens of each of the 212 // decrypted account secrets. 213 func DecodeAndUnbox(m libkb.MetaContext, finder PukFinder, encodedBundle BundleEncoded) (*stellar1.Bundle, stellar1.BundleVersion, keybase1.PerUserKeyGeneration, AccountPukGens, error) { 214 accountPukGens := make(AccountPukGens) 215 encBundle, hash, err := decodeParent(encodedBundle.EncParent) 216 if err != nil { 217 return nil, 0, 0, accountPukGens, err 218 } 219 220 puk, err := finder.SeedByGeneration(m, encBundle.Gen) 221 if err != nil { 222 return nil, 0, 0, accountPukGens, err 223 } 224 225 parent, parentVersion, err := unboxParent(encBundle, hash, encodedBundle.VisParent, puk) 226 if err != nil { 227 return nil, 0, 0, accountPukGens, err 228 } 229 parent.AccountBundles = make(map[stellar1.AccountID]stellar1.AccountBundle) 230 for _, parentEntry := range parent.Accounts { 231 if acctEncB64, ok := encodedBundle.AcctBundles[parentEntry.AccountID]; ok { 232 acctBundle, acctGen, err := decodeAndUnboxAcctBundle(m, finder, acctEncB64, parentEntry) 233 accountPukGens[parentEntry.AccountID] = acctGen 234 if err != nil { 235 return nil, 0, 0, accountPukGens, err 236 } 237 if acctBundle == nil { 238 return nil, 0, 0, accountPukGens, fmt.Errorf("error unboxing account bundle: missing for account %s", parentEntry.AccountID) 239 } 240 241 parent.AccountBundles[parentEntry.AccountID] = *acctBundle 242 } 243 } 244 if err = parent.CheckInvariants(); err != nil { 245 return nil, 0, 0, accountPukGens, err 246 } 247 return parent, parentVersion, encBundle.Gen, accountPukGens, nil 248 } 249 250 func decodeAndUnboxAcctBundle(m libkb.MetaContext, finder PukFinder, encB64 string, parentEntry stellar1.BundleEntry) (*stellar1.AccountBundle, keybase1.PerUserKeyGeneration, error) { 251 eab, hash, err := decode(encB64) 252 if err != nil { 253 return nil, 0, err 254 } 255 256 if !libkb.SecureByteArrayEq(hash, parentEntry.EncAcctBundleHash) { 257 return nil, 0, errors.New("account bundle and parent entry hash mismatch") 258 } 259 260 puk, err := finder.SeedByGeneration(m, eab.Gen) 261 if err != nil { 262 return nil, 0, err 263 } 264 ab, _, err := unbox(eab, hash, puk) 265 if err != nil { 266 return nil, 0, err 267 } 268 if ab.AccountID != parentEntry.AccountID { 269 return nil, 0, errors.New("account bundle and parent entry account ID mismatch") 270 } 271 return ab, eab.Gen, nil 272 } 273 274 // accountEncrypt encrypts the stellar account key bundle for the PUK. 275 // Returns the encrypted struct and a base64 encoding for posting to the server. 276 // Does not check invariants. 277 func accountEncrypt(bundle stellar1.AccountBundleSecretVersioned, pukGen keybase1.PerUserKeyGeneration, puk libkb.PerUserKeySeed) (res stellar1.EncryptedAccountBundle, resB64 string, err error) { 278 // Msgpack (inner) 279 clearpack, err := msgpack.Encode(bundle) 280 if err != nil { 281 return res, resB64, err 282 } 283 284 // Derive key 285 symmetricKey, err := puk.DeriveSymmetricKey(libkb.DeriveReasonPUKStellarAcctBundle) 286 if err != nil { 287 return res, resB64, err 288 } 289 290 // Secretbox 291 var nonce [libkb.NaclDHNonceSize]byte 292 nonce, err = libkb.RandomNaclDHNonce() 293 if err != nil { 294 return res, resB64, err 295 } 296 secbox := secretbox.Seal(nil, clearpack, &nonce, (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey)) 297 298 // Annotate 299 res = stellar1.EncryptedAccountBundle{ 300 V: 1, 301 E: secbox, 302 N: nonce, 303 Gen: pukGen, 304 } 305 306 // Msgpack (outer) + b64 307 cipherpack, err := msgpack.Encode(res) 308 if err != nil { 309 return res, resB64, err 310 } 311 resB64 = base64.StdEncoding.EncodeToString(cipherpack) 312 return res, resB64, nil 313 } 314 315 // decodeParent decodes a base64-encoded encrypted parent bundle. 316 func decodeParent(encryptedB64 string) (stellar1.EncryptedBundle, stellar1.Hash, error) { 317 cipherpack, err := base64.StdEncoding.DecodeString(encryptedB64) 318 if err != nil { 319 return stellar1.EncryptedBundle{}, stellar1.Hash{}, err 320 } 321 encHash := sha256.Sum256(cipherpack) 322 var enc stellar1.EncryptedBundle 323 if err = msgpack.Decode(&enc, cipherpack); err != nil { 324 return stellar1.EncryptedBundle{}, stellar1.Hash{}, err 325 } 326 return enc, encHash[:], nil 327 } 328 329 // unboxParent unboxes an encrypted parent bundle and decodes the visual portion of the bundle. 330 // It validates the visible hash in the secret portion. 331 func unboxParent(encBundle stellar1.EncryptedBundle, hash stellar1.Hash, visB64 string, puk libkb.PerUserKeySeed) (*stellar1.Bundle, stellar1.BundleVersion, error) { 332 versioned, err := decryptParent(encBundle, puk) 333 if err != nil { 334 return nil, 0, err 335 } 336 version, err := versioned.Version() 337 if err != nil { 338 return nil, 0, err 339 } 340 341 var bundleOut stellar1.Bundle 342 switch version { 343 case stellar1.BundleVersion_V2: 344 bundleOut, err = unboxParentV2(versioned, visB64) 345 if err != nil { 346 return nil, 0, err 347 } 348 default: 349 return nil, 0, fmt.Errorf("unsupported parent bundle version: %d", version) 350 } 351 352 bundleOut.OwnHash = hash 353 if len(bundleOut.OwnHash) == 0 { 354 return nil, 0, errors.New("stellar account bundle missing own hash") 355 } 356 357 return &bundleOut, version, nil 358 } 359 360 func unboxParentV2(versioned stellar1.BundleSecretVersioned, visB64 string) (stellar1.Bundle, error) { 361 var empty stellar1.Bundle 362 visiblePack, err := base64.StdEncoding.DecodeString(visB64) 363 if err != nil { 364 return empty, err 365 } 366 visibleHash := sha256.Sum256(visiblePack) 367 secretV2 := versioned.V2() 368 if !hmac.Equal(visibleHash[:], secretV2.VisibleHash) { 369 return empty, errors.New("corrupted bundle: visible hash mismatch") 370 } 371 var visibleV2 stellar1.BundleVisibleV2 372 err = msgpack.Decode(&visibleV2, visiblePack) 373 if err != nil { 374 return empty, err 375 } 376 return merge(secretV2, visibleV2) 377 } 378 379 // decryptParent decrypts an encrypted parent bundle with the provided puk. 380 func decryptParent(encBundle stellar1.EncryptedBundle, puk libkb.PerUserKeySeed) (res stellar1.BundleSecretVersioned, err error) { 381 switch encBundle.V { 382 case 1: 383 // CORE-8135 384 return res, fmt.Errorf("stellar secret bundle encryption version 1 has been retired") 385 case 2: 386 default: 387 return res, fmt.Errorf("unsupported stellar secret bundle encryption version: %v", encBundle.V) 388 } 389 390 // Derive key 391 reason := libkb.DeriveReasonPUKStellarBundle 392 symmetricKey, err := puk.DeriveSymmetricKey(reason) 393 if err != nil { 394 return res, err 395 } 396 397 // Secretbox 398 clearpack, ok := secretbox.Open(nil, encBundle.E, 399 (*[libkb.NaclDHNonceSize]byte)(&encBundle.N), 400 (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey)) 401 if !ok { 402 return res, errors.New("stellar bundle secret box open failed") 403 } 404 405 // Msgpack (inner) 406 err = msgpack.Decode(&res, clearpack) 407 return res, err 408 } 409 410 // decode decodes a base64-encoded encrypted account bundle. 411 func decode(encryptedB64 string) (stellar1.EncryptedAccountBundle, stellar1.Hash, error) { 412 cipherpack, err := base64.StdEncoding.DecodeString(encryptedB64) 413 if err != nil { 414 return stellar1.EncryptedAccountBundle{}, stellar1.Hash{}, err 415 } 416 encHash := sha256.Sum256(cipherpack) 417 var enc stellar1.EncryptedAccountBundle 418 if err = msgpack.Decode(&enc, cipherpack); err != nil { 419 return stellar1.EncryptedAccountBundle{}, stellar1.Hash{}, err 420 } 421 return enc, encHash[:], nil 422 } 423 424 // unbox unboxes an encrypted account bundle and decodes the visual portion of the bundle. 425 // It validates the visible hash in the secret portion. 426 func unbox(encBundle stellar1.EncryptedAccountBundle, hash stellar1.Hash /* visB64 string, */, puk libkb.PerUserKeySeed) (*stellar1.AccountBundle, stellar1.AccountBundleVersion, error) { 427 versioned, err := decrypt(encBundle, puk) 428 if err != nil { 429 return nil, 0, err 430 } 431 version, err := versioned.Version() 432 if err != nil { 433 return nil, 0, err 434 } 435 436 var bundleOut stellar1.AccountBundle 437 switch version { 438 case stellar1.AccountBundleVersion_V1: 439 secretV1 := versioned.V1() 440 bundleOut = stellar1.AccountBundle{ 441 AccountID: secretV1.AccountID, 442 Signers: secretV1.Signers, 443 } 444 case stellar1.AccountBundleVersion_V2, 445 stellar1.AccountBundleVersion_V3, 446 stellar1.AccountBundleVersion_V4, 447 stellar1.AccountBundleVersion_V5, 448 stellar1.AccountBundleVersion_V6, 449 stellar1.AccountBundleVersion_V7, 450 stellar1.AccountBundleVersion_V8, 451 stellar1.AccountBundleVersion_V9, 452 stellar1.AccountBundleVersion_V10: 453 return nil, 0, errors.New("unsupported AccountBundleSecret version") 454 default: 455 return nil, 0, errors.New("invalid AccountBundle version") 456 } 457 458 bundleOut.OwnHash = hash 459 if len(bundleOut.OwnHash) == 0 { 460 return nil, 0, errors.New("stellar account bundle missing own hash") 461 } 462 463 return &bundleOut, version, nil 464 } 465 466 // decrypt decrypts an encrypted account bundle with the provided puk. 467 func decrypt(encBundle stellar1.EncryptedAccountBundle, puk libkb.PerUserKeySeed) (stellar1.AccountBundleSecretVersioned, error) { 468 var empty stellar1.AccountBundleSecretVersioned 469 if encBundle.V != 1 { 470 return empty, errors.New("invalid stellar secret account bundle encryption version") 471 } 472 473 // Derive key 474 reason := libkb.DeriveReasonPUKStellarAcctBundle 475 symmetricKey, err := puk.DeriveSymmetricKey(reason) 476 if err != nil { 477 return empty, err 478 } 479 480 // Secretbox 481 clearpack, ok := secretbox.Open(nil, encBundle.E, 482 (*[libkb.NaclDHNonceSize]byte)(&encBundle.N), 483 (*[libkb.NaclSecretBoxKeySize]byte)(&symmetricKey)) 484 if !ok { 485 return empty, errors.New("stellar bundle secret box open failed") 486 } 487 488 // Msgpack (inner) 489 var bver stellar1.AccountBundleSecretVersioned 490 err = msgpack.Decode(&bver, clearpack) 491 if err != nil { 492 return empty, err 493 } 494 return bver, nil 495 } 496 func convertVisibleAccounts(in []stellar1.BundleVisibleEntryV2) []stellar1.BundleEntry { 497 out := make([]stellar1.BundleEntry, len(in)) 498 for i, e := range in { 499 out[i] = stellar1.BundleEntry{ 500 AccountID: e.AccountID, 501 Mode: e.Mode, 502 IsPrimary: e.IsPrimary, 503 AcctBundleRevision: e.AcctBundleRevision, 504 EncAcctBundleHash: e.EncAcctBundleHash, 505 } 506 } 507 return out 508 } 509 510 // merge combines the versioned secret account bundle and the visible account bundle into 511 // a stellar1.AccountBundle for local use. 512 func merge(secret stellar1.BundleSecretV2, visible stellar1.BundleVisibleV2) (stellar1.Bundle, error) { 513 if len(secret.Accounts) != len(visible.Accounts) { 514 return stellar1.Bundle{}, errors.New("invalid bundle, mismatched number of visible and secret accounts") 515 } 516 accounts := convertVisibleAccounts(visible.Accounts) 517 518 // these should be in the same order 519 for i, secretAccount := range secret.Accounts { 520 if accounts[i].AccountID != secretAccount.AccountID { 521 return stellar1.Bundle{}, errors.New("invalid bundle, mismatched order of visible and secret accounts") 522 } 523 accounts[i].Name = secretAccount.Name 524 } 525 return stellar1.Bundle{ 526 Revision: visible.Revision, 527 Prev: visible.Prev, 528 Accounts: accounts, 529 }, nil 530 }