@@ -70,6 +70,7 @@ import {
7070 CreatePolicyRuleOptions ,
7171 CreateShareOptions ,
7272 CrossChainUTXO ,
73+ DecryptedKeychainData ,
7374 DeployForwardersOptions ,
7475 DownloadKeycardOptions ,
7576 FanoutUnspentsOptions ,
@@ -1685,8 +1686,27 @@ export class Wallet implements IWallet {
16851686 if ( params . keyShareOptions . length === 0 ) {
16861687 throw new Error ( 'No share options provided' ) ;
16871688 }
1689+
16881690 const bulkCreateShareOptions : BulkCreateShareOption [ ] = [ ] ;
16891691
1692+ // Check if any share option needs a keychain (has 'spend' permission)
1693+ const anyNeedsKeychain = params . keyShareOptions . some ( ( opt ) => opt . permissions && opt . permissions . includes ( 'spend' ) ) ;
1694+
1695+ // Fetch and decrypt the keychain ONCE for all users
1696+ let decryptedKeychain : DecryptedKeychainData | undefined ;
1697+
1698+ if ( anyNeedsKeychain ) {
1699+ try {
1700+ decryptedKeychain = await this . getDecryptedKeychainForSharing ( params . walletPassphrase ) ;
1701+ } catch ( e ) {
1702+ if ( e instanceof MissingEncryptedKeychainError ) {
1703+ decryptedKeychain = undefined ;
1704+ } else {
1705+ throw e ;
1706+ }
1707+ }
1708+ }
1709+
16901710 for ( const shareOption of params . keyShareOptions ) {
16911711 try {
16921712 common . validateParams ( shareOption , [ 'userId' , 'pubKey' , 'path' ] , [ ] ) ;
@@ -1699,36 +1719,22 @@ export class Wallet implements IWallet {
16991719
17001720 const needsKeychain = shareOption . permissions && shareOption . permissions . includes ( 'spend' ) ;
17011721
1702- if ( needsKeychain ) {
1703- const sharedKeychain = await this . prepareSharedKeychain (
1704- params . walletPassphrase ,
1722+ if ( needsKeychain && decryptedKeychain ) {
1723+ const sharedKeychain = this . encryptPrvForUser (
1724+ decryptedKeychain . prv ,
1725+ decryptedKeychain . pub ,
17051726 shareOption . pubKey ,
17061727 shareOption . path
17071728 ) ;
1708- const keychain = Object . keys ( sharedKeychain ?? { } ) . length === 0 ? undefined : sharedKeychain ;
1709- if ( keychain ) {
1710- assert ( keychain . pub , 'pub must be defined for sharing' ) ;
1711- assert ( keychain . encryptedPrv , 'encryptedPrv must be defined for sharing' ) ;
1712- assert ( keychain . fromPubKey , 'fromPubKey must be defined for sharing' ) ;
1713- assert ( keychain . toPubKey , 'toPubKey must be defined for sharing' ) ;
1714- assert ( keychain . path , 'path must be defined for sharing' ) ;
1715-
1716- const bulkKeychain : BulkWalletShareKeychain = {
1717- pub : keychain . pub ,
1718- encryptedPrv : keychain . encryptedPrv ,
1719- fromPubKey : keychain . fromPubKey ,
1720- toPubKey : keychain . toPubKey ,
1721- path : keychain . path ,
1722- } ;
17231729
1724- bulkCreateShareOptions . push ( {
1725- user : shareOption . userId ,
1726- permissions : shareOption . permissions ,
1727- keychain : bulkKeychain ,
1728- } ) ;
1729- }
1730+ bulkCreateShareOptions . push ( {
1731+ user : shareOption . userId ,
1732+ permissions : shareOption . permissions ,
1733+ keychain : sharedKeychain ,
1734+ } ) ;
17301735 }
17311736 }
1737+
17321738 return await this . createBulkKeyShares ( bulkCreateShareOptions ) ;
17331739 }
17341740
@@ -1776,62 +1782,107 @@ export class Wallet implements IWallet {
17761782 }
17771783 }
17781784
1779- async prepareSharedKeychain (
1780- walletPassphrase : string | undefined ,
1781- pubkey : string ,
1782- path : string
1783- ) : Promise < SharedKeyChain > {
1784- let sharedKeychain : SharedKeyChain = { } ;
1785+ /**
1786+ * Fetches and decrypts the wallet keychain for sharing.
1787+ * This method fetches the keychain once and decrypts it, returning the decrypted
1788+ * private key and public key info needed for sharing with multiple users.
1789+ *
1790+ * @param walletPassphrase - The passphrase to decrypt the keychain
1791+ * @returns Object containing decrypted prv and pub, or undefined for cold wallets
1792+ */
1793+ async getDecryptedKeychainForSharing (
1794+ walletPassphrase : string | undefined
1795+ ) : Promise < DecryptedKeychainData | undefined > {
1796+ const keychain = await this . getEncryptedWalletKeychainForWalletSharing ( ) ;
17851797
1786- try {
1787- const keychain = await this . getEncryptedWalletKeychainForWalletSharing ( ) ;
1798+ if ( ! keychain . encryptedPrv ) {
1799+ return undefined ;
1800+ }
17881801
1789- // Decrypt the user key with a passphrase
1790- if ( keychain . encryptedPrv ) {
1791- if ( ! walletPassphrase ) {
1792- throw new Error ( 'Missing walletPassphrase argument' ) ;
1793- }
1802+ if ( ! walletPassphrase ) {
1803+ throw new Error ( 'Missing walletPassphrase argument' ) ;
1804+ }
17941805
1795- const userPrv = decryptKeychainPrivateKey ( this . bitgo , keychain , walletPassphrase ) ;
1796- if ( ! userPrv ) {
1797- throw new IncorrectPasswordError ( 'Password shared is incorrect for this wallet' ) ;
1798- }
1806+ const prv = decryptKeychainPrivateKey ( this . bitgo , keychain , walletPassphrase ) ;
1807+ if ( ! prv ) {
1808+ throw new IncorrectPasswordError ( 'Password shared is incorrect for this wallet' ) ;
1809+ }
17991810
1800- keychain . prv = userPrv ;
1801- const eckey = makeRandomKey ( ) ;
1802- const secret = getSharedSecret ( eckey , Buffer . from ( pubkey , 'hex' ) ) . toString ( 'hex' ) ;
1803- const newEncryptedPrv = this . bitgo . encrypt ( {
1804- password : secret ,
1805- input : keychain . prv ,
1806- } ) ;
1811+ // Only one of pub/commonPub/commonKeychain should be present in the keychain
1812+ let pub = keychain . pub ?? keychain . commonPub ;
1813+ if ( keychain . commonKeychain ) {
1814+ pub =
1815+ this . baseCoin . getMPCAlgorithm ( ) === 'eddsa'
1816+ ? EddsaUtils . getPublicKeyFromCommonKeychain ( keychain . commonKeychain )
1817+ : EcdsaUtils . getPublicKeyFromCommonKeychain ( keychain . commonKeychain ) ;
1818+ }
18071819
1808- // Only one of pub/commonPub/commonKeychain should be present in the keychain
1809- let pub = keychain . pub ?? keychain . commonPub ;
1810- if ( keychain . commonKeychain ) {
1811- pub =
1812- this . baseCoin . getMPCAlgorithm ( ) === 'eddsa'
1813- ? EddsaUtils . getPublicKeyFromCommonKeychain ( keychain . commonKeychain )
1814- : EcdsaUtils . getPublicKeyFromCommonKeychain ( keychain . commonKeychain ) ;
1815- }
1820+ if ( ! pub ) {
1821+ throw new Error ( 'Unable to determine public key from keychain' ) ;
1822+ }
18161823
1817- sharedKeychain = {
1818- pub,
1819- encryptedPrv : newEncryptedPrv ,
1820- fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
1821- toPubKey : pubkey ,
1822- path : path ,
1823- } ;
1824+ return { prv, pub } ;
1825+ }
1826+
1827+ /**
1828+ * Encrypts a decrypted private key for sharing with a specific user.
1829+ * This is the pure encryption step - no API calls, no decryption.
1830+ *
1831+ * @param decryptedPrv - The already-decrypted private key
1832+ * @param pub - The wallet's public key
1833+ * @param userPubkey - The recipient user's public key
1834+ * @param path - The key path
1835+ * @returns The encrypted keychain for the recipient with all required fields
1836+ */
1837+ encryptPrvForUser ( decryptedPrv : string , pub : string , userPubkey : string , path : string ) : BulkWalletShareKeychain {
1838+ const eckey = makeRandomKey ( ) ;
1839+ const secret = getSharedSecret ( eckey , Buffer . from ( userPubkey , 'hex' ) ) . toString ( 'hex' ) ;
1840+ const newEncryptedPrv = this . bitgo . encrypt ( { password : secret , input : decryptedPrv } ) ;
1841+
1842+ const keychain : BulkWalletShareKeychain = {
1843+ pub,
1844+ encryptedPrv : newEncryptedPrv ,
1845+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
1846+ toPubKey : userPubkey ,
1847+ path : path ,
1848+ } ;
1849+
1850+ assert ( keychain . pub , 'pub must be defined for sharing' ) ;
1851+ assert ( keychain . encryptedPrv , 'encryptedPrv must be defined for sharing' ) ;
1852+ assert ( keychain . fromPubKey , 'fromPubKey must be defined for sharing' ) ;
1853+ assert ( keychain . toPubKey , 'toPubKey must be defined for sharing' ) ;
1854+ assert ( keychain . path , 'path must be defined for sharing' ) ;
1855+
1856+ return keychain ;
1857+ }
1858+
1859+ /**
1860+ * Prepares a keychain for sharing with another user.
1861+ * Fetches the wallet keychain, decrypts it, and encrypts it for the recipient.
1862+ *
1863+ * @param walletPassphrase - The passphrase to decrypt the keychain
1864+ * @param pubkey - The recipient's public key
1865+ * @param path - The key path
1866+ * @returns The encrypted keychain for the recipient
1867+ */
1868+ async prepareSharedKeychain (
1869+ walletPassphrase : string | undefined ,
1870+ pubkey : string ,
1871+ path : string
1872+ ) : Promise < SharedKeyChain > {
1873+ try {
1874+ const decryptedKeychain = await this . getDecryptedKeychainForSharing ( walletPassphrase ) ;
1875+ if ( ! decryptedKeychain ) {
1876+ return { } ;
18241877 }
1878+ return this . encryptPrvForUser ( decryptedKeychain . prv , decryptedKeychain . pub , pubkey , path ) ;
18251879 } catch ( e ) {
18261880 if ( e instanceof MissingEncryptedKeychainError ) {
1827- sharedKeychain = { } ;
18281881 // ignore this error because this looks like a cold wallet
1829- } else {
1830- throw e ;
1882+ return { } ;
18311883 }
1884+ throw e ;
18321885 }
1833-
1834- return sharedKeychain ;
18351886 }
18361887
18371888 /**
0 commit comments