Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions modules/bitgo/test/v2/unit/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
ManageUnspentsOptions,
MessageStandardType,
MessageTypes,
NeedUserSignupError,
PopulatedIntent,
PrebuildTransactionOptions,
PrebuildTransactionWithIntentOptions,
Expand Down Expand Up @@ -2144,6 +2145,85 @@ describe('V2 Wallet:', function () {
getKeyNock.isDone().should.be.True();
});

it('should throw NeedUserSignupError when recipient has no ECDH pubkey (spend permission)', async function () {
const userId = '456';
const email = 'nopubkey@sdktest.com';
const permissions = 'view,spend';
const walletPassphrase = 'bitgo1234';
const pub = 'Zo1ggzTUKMY5bYnDvT5mtVeZxzf2FaLTbKkmvGUhUQk';
const encXprv = await bitgo.encrypt({ input: 'xprv1', password: walletPassphrase });

// Server returns empty pubkey — user has not completed ECDH keychain setup
const getSharingKeyNock = nock(bgUrl)
.post('/api/v1/user/sharingkey', { email })
.reply(200, { userId, pubkey: '', path: 'm/999999/1/1' });

// Key fetch must succeed so decryption can proceed before the pubkey check
const getKeyNock = nock(bgUrl).get(`/api/v2/tbtc/key/${wallet.keyIds()[0]}`).reply(200, {
id: wallet.keyIds()[0],
pub,
source: 'user',
encryptedPrv: encXprv,
coinSpecific: {},
});

await wallet
.shareWallet({ email, permissions, walletPassphrase })
.should.be.rejectedWith(NeedUserSignupError);

getSharingKeyNock.isDone().should.be.True();
getKeyNock.isDone().should.be.True();
});

it('should throw NeedUserSignupError when recipient pubkey is missing (spend permission)', async function () {
const userId = '456';
const email = 'nopubkey@sdktest.com';
const permissions = 'view,spend';
const walletPassphrase = 'bitgo1234';
const pub = 'Zo1ggzTUKMY5bYnDvT5mtVeZxzf2FaLTbKkmvGUhUQk';
const encXprv = await bitgo.encrypt({ input: 'xprv1', password: walletPassphrase });

// Server returns no pubkey field at all
const getSharingKeyNock = nock(bgUrl)
.post('/api/v1/user/sharingkey', { email })
.reply(200, { userId, path: 'm/999999/1/1' });

const getKeyNock = nock(bgUrl).get(`/api/v2/tbtc/key/${wallet.keyIds()[0]}`).reply(200, {
id: wallet.keyIds()[0],
pub,
source: 'user',
encryptedPrv: encXprv,
coinSpecific: {},
});

await wallet
.shareWallet({ email, permissions, walletPassphrase })
.should.be.rejectedWith(NeedUserSignupError);

getSharingKeyNock.isDone().should.be.True();
getKeyNock.isDone().should.be.True();
});

it('should not throw NeedUserSignupError when recipient has no pubkey but permission is view-only', async function () {
const userId = '456';
const email = 'nopubkey@sdktest.com';
const permissions = 'view';

// No pubkey — but view-only shares do not need keychain, so should succeed
const getSharingKeyNock = nock(bgUrl)
.post('/api/v1/user/sharingkey', { email })
.reply(200, { userId, pubkey: '', path: 'm/999999/1/1' });

const createShareNock = nock(bgUrl)
.post(`/api/v2/tbtc/wallet/${wallet.id()}/share`)
.reply(200, {});

await wallet.shareWallet({ email, permissions });

getSharingKeyNock.isDone().should.be.True();
createShareNock.isDone().should.be.True();
});

describe('OFC multi-key-user-Key Wallet Sharing', function () {
const userId = '123';
const email = 'shareto@sdktest.com';
Expand Down
13 changes: 11 additions & 2 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2005,13 +2005,21 @@ export class Wallet implements IWallet {
walletPassphrase: string | undefined,
pubkey: string,
path: string,
encryptionVersion?: EncryptionVersion
encryptionVersion?: EncryptionVersion,
recipientEmail?: string
): Promise<SharedKeyChain> {
try {
const decryptedKeychain = await this.getDecryptedKeychainForSharing(walletPassphrase);
if (!decryptedKeychain) {
return {};
}
if (!pubkey) {
const recipient = recipientEmail ?? 'The recipient';
throw new NeedUserSignupError(
`${recipient} does not have an ECDH keychain set up. ` +
`They must log into the BitGo web application at least once before receiving a wallet share.`
);
}
return await this.encryptPrvForUser(
decryptedKeychain.prv,
decryptedKeychain.pub,
Expand Down Expand Up @@ -2068,7 +2076,8 @@ export class Wallet implements IWallet {
params.walletPassphrase,
sharing.pubkey,
sharing.path,
params.encryptionVersion
params.encryptionVersion,
params.email
);
}

Expand Down
Loading