-
Notifications
You must be signed in to change notification settings - Fork 299
fix(wallet-key-share): Optimize createBulkWalletShare to Fetch Keychain Once and Prevent Rate Limiting #7849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -645,6 +645,11 @@ export interface BulkWalletShareOptions { | |
|
|
||
| export type WalletShareState = 'active' | 'accepted' | 'canceled' | 'rejected' | 'pendingapproval'; | ||
|
|
||
| export interface DecryptedKeychainData { | ||
| prv: string; | ||
| pub: string; | ||
| } | ||
|
Comment on lines
+648
to
+651
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we sure we don't already have this type defined somewhere? |
||
|
|
||
| export interface BulkWalletShareKeychain { | ||
| pub: string; | ||
| encryptedPrv: string; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -70,6 +70,7 @@ import { | |||||||||||||||||||||||||||||||
| CreatePolicyRuleOptions, | ||||||||||||||||||||||||||||||||
| CreateShareOptions, | ||||||||||||||||||||||||||||||||
| CrossChainUTXO, | ||||||||||||||||||||||||||||||||
| DecryptedKeychainData, | ||||||||||||||||||||||||||||||||
| DeployForwardersOptions, | ||||||||||||||||||||||||||||||||
| DownloadKeycardOptions, | ||||||||||||||||||||||||||||||||
| FanoutUnspentsOptions, | ||||||||||||||||||||||||||||||||
|
|
@@ -1685,8 +1686,27 @@ export class Wallet implements IWallet { | |||||||||||||||||||||||||||||||
| if (params.keyShareOptions.length === 0) { | ||||||||||||||||||||||||||||||||
| throw new Error('No share options provided'); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const bulkCreateShareOptions: BulkCreateShareOption[] = []; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Check if any share option needs a keychain (has 'spend' permission) | ||||||||||||||||||||||||||||||||
| const anyNeedsKeychain = params.keyShareOptions.some((opt) => opt.permissions && opt.permissions.includes('spend')); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Fetch and decrypt the keychain ONCE for all users | ||||||||||||||||||||||||||||||||
| let decryptedKeychain: DecryptedKeychainData | undefined; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (anyNeedsKeychain) { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| decryptedKeychain = await this.getDecryptedKeychainForSharing(params.walletPassphrase); | ||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||
| if (e instanceof MissingEncryptedKeychainError) { | ||||||||||||||||||||||||||||||||
| decryptedKeychain = undefined; | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| throw e; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| for (const shareOption of params.keyShareOptions) { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| common.validateParams(shareOption, ['userId', 'pubKey', 'path'], []); | ||||||||||||||||||||||||||||||||
|
|
@@ -1699,36 +1719,22 @@ export class Wallet implements IWallet { | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const needsKeychain = shareOption.permissions && shareOption.permissions.includes('spend'); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (needsKeychain) { | ||||||||||||||||||||||||||||||||
| const sharedKeychain = await this.prepareSharedKeychain( | ||||||||||||||||||||||||||||||||
| params.walletPassphrase, | ||||||||||||||||||||||||||||||||
| if (needsKeychain && decryptedKeychain) { | ||||||||||||||||||||||||||||||||
| const sharedKeychain = this.encryptPrvForUser( | ||||||||||||||||||||||||||||||||
| decryptedKeychain.prv, | ||||||||||||||||||||||||||||||||
| decryptedKeychain.pub, | ||||||||||||||||||||||||||||||||
| shareOption.pubKey, | ||||||||||||||||||||||||||||||||
| shareOption.path | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| const keychain = Object.keys(sharedKeychain ?? {}).length === 0 ? undefined : sharedKeychain; | ||||||||||||||||||||||||||||||||
| if (keychain) { | ||||||||||||||||||||||||||||||||
| assert(keychain.pub, 'pub must be defined for sharing'); | ||||||||||||||||||||||||||||||||
| assert(keychain.encryptedPrv, 'encryptedPrv must be defined for sharing'); | ||||||||||||||||||||||||||||||||
| assert(keychain.fromPubKey, 'fromPubKey must be defined for sharing'); | ||||||||||||||||||||||||||||||||
| assert(keychain.toPubKey, 'toPubKey must be defined for sharing'); | ||||||||||||||||||||||||||||||||
| assert(keychain.path, 'path must be defined for sharing'); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const bulkKeychain: BulkWalletShareKeychain = { | ||||||||||||||||||||||||||||||||
| pub: keychain.pub, | ||||||||||||||||||||||||||||||||
| encryptedPrv: keychain.encryptedPrv, | ||||||||||||||||||||||||||||||||
| fromPubKey: keychain.fromPubKey, | ||||||||||||||||||||||||||||||||
| toPubKey: keychain.toPubKey, | ||||||||||||||||||||||||||||||||
| path: keychain.path, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| bulkCreateShareOptions.push({ | ||||||||||||||||||||||||||||||||
| user: shareOption.userId, | ||||||||||||||||||||||||||||||||
| permissions: shareOption.permissions, | ||||||||||||||||||||||||||||||||
| keychain: bulkKeychain, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| bulkCreateShareOptions.push({ | ||||||||||||||||||||||||||||||||
| user: shareOption.userId, | ||||||||||||||||||||||||||||||||
| permissions: shareOption.permissions, | ||||||||||||||||||||||||||||||||
| keychain: sharedKeychain, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return await this.createBulkKeyShares(bulkCreateShareOptions); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -1776,62 +1782,107 @@ export class Wallet implements IWallet { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| async prepareSharedKeychain( | ||||||||||||||||||||||||||||||||
| walletPassphrase: string | undefined, | ||||||||||||||||||||||||||||||||
| pubkey: string, | ||||||||||||||||||||||||||||||||
| path: string | ||||||||||||||||||||||||||||||||
| ): Promise<SharedKeyChain> { | ||||||||||||||||||||||||||||||||
| let sharedKeychain: SharedKeyChain = {}; | ||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Fetches and decrypts the wallet keychain for sharing. | ||||||||||||||||||||||||||||||||
| * This method fetches the keychain once and decrypts it, returning the decrypted | ||||||||||||||||||||||||||||||||
| * private key and public key info needed for sharing with multiple users. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * @param walletPassphrase - The passphrase to decrypt the keychain | ||||||||||||||||||||||||||||||||
| * @returns Object containing decrypted prv and pub, or undefined for cold wallets | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| async getDecryptedKeychainForSharing( | ||||||||||||||||||||||||||||||||
| walletPassphrase: string | undefined | ||||||||||||||||||||||||||||||||
| ): Promise<DecryptedKeychainData | undefined> { | ||||||||||||||||||||||||||||||||
| const keychain = await this.getEncryptedWalletKeychainForWalletSharing(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| const keychain = await this.getEncryptedWalletKeychainForWalletSharing(); | ||||||||||||||||||||||||||||||||
| if (!keychain.encryptedPrv) { | ||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Decrypt the user key with a passphrase | ||||||||||||||||||||||||||||||||
| if (keychain.encryptedPrv) { | ||||||||||||||||||||||||||||||||
| if (!walletPassphrase) { | ||||||||||||||||||||||||||||||||
| throw new Error('Missing walletPassphrase argument'); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (!walletPassphrase) { | ||||||||||||||||||||||||||||||||
| throw new Error('Missing walletPassphrase argument'); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const userPrv = decryptKeychainPrivateKey(this.bitgo, keychain, walletPassphrase); | ||||||||||||||||||||||||||||||||
| if (!userPrv) { | ||||||||||||||||||||||||||||||||
| throw new IncorrectPasswordError('Password shared is incorrect for this wallet'); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| const prv = decryptKeychainPrivateKey(this.bitgo, keychain, walletPassphrase); | ||||||||||||||||||||||||||||||||
| if (!prv) { | ||||||||||||||||||||||||||||||||
| throw new IncorrectPasswordError('Password shared is incorrect for this wallet'); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| keychain.prv = userPrv; | ||||||||||||||||||||||||||||||||
| const eckey = makeRandomKey(); | ||||||||||||||||||||||||||||||||
| const secret = getSharedSecret(eckey, Buffer.from(pubkey, 'hex')).toString('hex'); | ||||||||||||||||||||||||||||||||
| const newEncryptedPrv = this.bitgo.encrypt({ | ||||||||||||||||||||||||||||||||
| password: secret, | ||||||||||||||||||||||||||||||||
| input: keychain.prv, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| // Only one of pub/commonPub/commonKeychain should be present in the keychain | ||||||||||||||||||||||||||||||||
| let pub = keychain.pub ?? keychain.commonPub; | ||||||||||||||||||||||||||||||||
| if (keychain.commonKeychain) { | ||||||||||||||||||||||||||||||||
|
Comment on lines
+1811
to
+1813
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
commonPub is deprecated field, we shouldn't have any active wallets that are using this field. |
||||||||||||||||||||||||||||||||
| pub = | ||||||||||||||||||||||||||||||||
| this.baseCoin.getMPCAlgorithm() === 'eddsa' | ||||||||||||||||||||||||||||||||
| ? EddsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain) | ||||||||||||||||||||||||||||||||
| : EcdsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+1814
to
+1817
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this necessary??
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will ping on slack. |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Only one of pub/commonPub/commonKeychain should be present in the keychain | ||||||||||||||||||||||||||||||||
| let pub = keychain.pub ?? keychain.commonPub; | ||||||||||||||||||||||||||||||||
| if (keychain.commonKeychain) { | ||||||||||||||||||||||||||||||||
| pub = | ||||||||||||||||||||||||||||||||
| this.baseCoin.getMPCAlgorithm() === 'eddsa' | ||||||||||||||||||||||||||||||||
| ? EddsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain) | ||||||||||||||||||||||||||||||||
| : EcdsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (!pub) { | ||||||||||||||||||||||||||||||||
| throw new Error('Unable to determine public key from keychain'); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| sharedKeychain = { | ||||||||||||||||||||||||||||||||
| pub, | ||||||||||||||||||||||||||||||||
| encryptedPrv: newEncryptedPrv, | ||||||||||||||||||||||||||||||||
| fromPubKey: eckey.publicKey.toString('hex'), | ||||||||||||||||||||||||||||||||
| toPubKey: pubkey, | ||||||||||||||||||||||||||||||||
| path: path, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| return { prv, pub }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Encrypts a decrypted private key for sharing with a specific user. | ||||||||||||||||||||||||||||||||
| * This is the pure encryption step - no API calls, no decryption. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * @param decryptedPrv - The already-decrypted private key | ||||||||||||||||||||||||||||||||
| * @param pub - The wallet's public key | ||||||||||||||||||||||||||||||||
| * @param userPubkey - The recipient user's public key | ||||||||||||||||||||||||||||||||
| * @param path - The key path | ||||||||||||||||||||||||||||||||
| * @returns The encrypted keychain for the recipient with all required fields | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| encryptPrvForUser(decryptedPrv: string, pub: string, userPubkey: string, path: string): BulkWalletShareKeychain { | ||||||||||||||||||||||||||||||||
| const eckey = makeRandomKey(); | ||||||||||||||||||||||||||||||||
| const secret = getSharedSecret(eckey, Buffer.from(userPubkey, 'hex')).toString('hex'); | ||||||||||||||||||||||||||||||||
| const newEncryptedPrv = this.bitgo.encrypt({ password: secret, input: decryptedPrv }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const keychain: BulkWalletShareKeychain = { | ||||||||||||||||||||||||||||||||
| pub, | ||||||||||||||||||||||||||||||||
| encryptedPrv: newEncryptedPrv, | ||||||||||||||||||||||||||||||||
| fromPubKey: eckey.publicKey.toString('hex'), | ||||||||||||||||||||||||||||||||
| toPubKey: userPubkey, | ||||||||||||||||||||||||||||||||
| path: path, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| assert(keychain.pub, 'pub must be defined for sharing'); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+1842
to
+1850
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This shouldn't be necessary. |
||||||||||||||||||||||||||||||||
| assert(keychain.encryptedPrv, 'encryptedPrv must be defined for sharing'); | ||||||||||||||||||||||||||||||||
| assert(keychain.fromPubKey, 'fromPubKey must be defined for sharing'); | ||||||||||||||||||||||||||||||||
| assert(keychain.toPubKey, 'toPubKey must be defined for sharing'); | ||||||||||||||||||||||||||||||||
| assert(keychain.path, 'path must be defined for sharing'); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return keychain; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Prepares a keychain for sharing with another user. | ||||||||||||||||||||||||||||||||
| * Fetches the wallet keychain, decrypts it, and encrypts it for the recipient. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * @param walletPassphrase - The passphrase to decrypt the keychain | ||||||||||||||||||||||||||||||||
| * @param pubkey - The recipient's public key | ||||||||||||||||||||||||||||||||
| * @param path - The key path | ||||||||||||||||||||||||||||||||
| * @returns The encrypted keychain for the recipient | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| async prepareSharedKeychain( | ||||||||||||||||||||||||||||||||
| walletPassphrase: string | undefined, | ||||||||||||||||||||||||||||||||
| pubkey: string, | ||||||||||||||||||||||||||||||||
| path: string | ||||||||||||||||||||||||||||||||
| ): Promise<SharedKeyChain> { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| const decryptedKeychain = await this.getDecryptedKeychainForSharing(walletPassphrase); | ||||||||||||||||||||||||||||||||
| if (!decryptedKeychain) { | ||||||||||||||||||||||||||||||||
| return {}; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return this.encryptPrvForUser(decryptedKeychain.prv, decryptedKeychain.pub, pubkey, path); | ||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||
| if (e instanceof MissingEncryptedKeychainError) { | ||||||||||||||||||||||||||||||||
| sharedKeychain = {}; | ||||||||||||||||||||||||||||||||
| // ignore this error because this looks like a cold wallet | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| throw e; | ||||||||||||||||||||||||||||||||
| return {}; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| throw e; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return sharedKeychain; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Expand test coverage: Also verify
encryptPrvForUseris called once per user to ensure encryption happens for each recipient.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added