Skip to content

Commit

Permalink
feat(sdk-core): add utility function for get sharing keys API
Browse files Browse the repository at this point in the history
Utility function to be used by both bulk and non-bulk version of the API

Ticket: CSI-107
  • Loading branch information
SoumalyaBh committed Sep 20, 2024
1 parent a10dac8 commit c2234f2
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 84 deletions.
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
'CP-',
'CR-',
'CS-',
'CSI-',
'DES-',
'DO-',
'DOS-',
Expand Down
1 change: 1 addition & 0 deletions modules/bitgo/test/v2/unit/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,7 @@ describe('V2 Wallets:', function () {

sinon.stub(wallet, 'getEncryptedUserKeychain').resolves({
encryptedPrv: keychainTest.encryptedPrv,
pub,
} as KeychainWithEncryptedPrv);

sinon.stub(moduleBitgo, 'getSharedSecret').resolves('fakeSharedSecret');
Expand Down
6 changes: 6 additions & 0 deletions modules/sdk-core/src/bitgo/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ export class InvalidTransactionError extends BitGoJsError {
}
}

export class MissingEncryptedKeychainError extends Error {
public constructor(message?: string) {
super(message || 'No encrypted keychains on this wallet.');
}
}

export class ApiResponseError<ResponseBodyType = any> extends BitGoJsError {
message: string;
status: number;
Expand Down
8 changes: 8 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,14 @@ export interface GetPrvOptions {
walletPassphrase?: string;
}

export interface SharedKeyChain {
pub?: string;
encryptedPrv?: string;
fromPubKey?: string;
toPubKey?: string;
path?: string;
}

export interface CreateShareOptions {
user?: string;
permissions?: string;
Expand Down
166 changes: 82 additions & 84 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { makeRandomKey } from '../bitcoin';
import { BitGoBase } from '../bitgoBase';
import { getSharedSecret } from '../ecdh';
import { AddressGenerationError, MethodNotImplementedError } from '../errors';
import { AddressGenerationError, MethodNotImplementedError, MissingEncryptedKeychainError } from '../errors';
import * as internal from '../internal/internal';
import { drawKeycard } from '../internal';
import { decryptKeychainPrivateKey, Keychain, KeychainWithEncryptedPrv } from '../keychain';
Expand Down Expand Up @@ -94,6 +94,8 @@ import {
WalletSignTypedDataOptions,
WalletType,
CreateBulkWalletShareListResponse,
SharedKeyChain,
BulkWalletShareKeychain,
} from './iWallet';
import { StakingWallet } from '../staking';
import { Lightning } from '../lightning/custodial';
Expand Down Expand Up @@ -1391,7 +1393,7 @@ export class Wallet implements IWallet {
async getEncryptedUserKeychain(): Promise<KeychainWithEncryptedPrv> {
const tryKeyChain = async (index: number): Promise<KeychainWithEncryptedPrv> => {
if (!this._wallet.keys || index >= this._wallet.keys.length) {
throw new Error('No encrypted keychains on this wallet.');
throw new MissingEncryptedKeychainError();
}

const params = { id: this._wallet.keys[index] };
Expand Down Expand Up @@ -1483,7 +1485,7 @@ export class Wallet implements IWallet {
* @returns {Promise<CreateBulkWalletShareListResponse>} A promise that resolves with the response of the bulk wallet share creation.
*/
async createBulkWalletShare(params: BulkWalletShareOptions): Promise<CreateBulkWalletShareListResponse> {
if (!params.keyShareOptions || Object.keys(params.keyShareOptions).length === 0) {
if (params.keyShareOptions.length === 0) {
throw new Error('shareOptions cannot be empty');
}
const bulkCreateShareOptions: BulkCreateShareOption[] = [];
Expand All @@ -1492,54 +1494,33 @@ export class Wallet implements IWallet {
common.validateParams(shareOption, ['userId', 'pubKey', 'path'], []);

const needsKeychain = shareOption.permissions && shareOption.permissions.includes('spend');
let sharedKeychain;

if (needsKeychain) {
try {
const keychain = await this.getEncryptedUserKeychain();

if (keychain.encryptedPrv) {
const userPrv = decryptKeychainPrivateKey(this.bitgo, keychain, params.walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain.');
}

const ecdhkey = makeRandomKey();
assert(shareOption.pubKey, 'pubKey must be defined for sharing');

const secret = getSharedSecret(ecdhkey, Buffer.from(shareOption.pubKey, 'hex')).toString('hex');
const newEncryptedPrv = this.bitgo.encrypt({ password: secret, input: userPrv });

let pub = keychain.pub ?? keychain.commonPub;
if (keychain.commonKeychain) {
pub =
this.baseCoin.getMPCAlgorithm() === 'eddsa'
? EddsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain)
: EcdsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain);
}

sharedKeychain = {
pub,
encryptedPrv: newEncryptedPrv,
fromPubKey: ecdhkey.publicKey.toString('hex'),
toPubKey: shareOption.pubKey,
path: shareOption.path,
};
}
} catch (e) {
if (e.message === 'No encrypted keychains on this wallet.') {
sharedKeychain = {};
// ignore this error because this looks like a cold wallet
} else {
throw e;
}
}
const sharedKeychain = await this.prepareSharedKeychain(
params.walletPassphrase,
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: keychain,
keychain: bulkKeychain,
});
}
}
Expand Down Expand Up @@ -1578,6 +1559,61 @@ export class Wallet implements IWallet {
return this.bitgo.post(url).send({ shareOptions: params }).result();
}

async prepareSharedKeychain(
walletPassphrase: string | undefined,
pubkey: string,
path: string
): Promise<SharedKeyChain> {
let sharedKeychain: SharedKeyChain = {};

try {
const keychain = await this.getEncryptedUserKeychain();

// Decrypt the user key with a passphrase
if (keychain.encryptedPrv) {
if (!walletPassphrase) {
throw new Error('Missing walletPassphrase argument');
}

const userPrv = decryptKeychainPrivateKey(this.bitgo, keychain, walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}

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) {
pub =
this.baseCoin.getMPCAlgorithm() === 'eddsa'
? EddsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain)
: EcdsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain);
}

sharedKeychain = {
pub,
encryptedPrv: newEncryptedPrv,
fromPubKey: eckey.publicKey.toString('hex'),
toPubKey: pubkey,
path: path,
};
}
} catch (e) {
if (e instanceof MissingEncryptedKeychainError) {
sharedKeychain = {};
// ignore this error because this looks like a cold wallet
} else {
throw e;
}
}

return sharedKeychain;
}

/**
* Share this wallet with another BitGo user.
* @param params
Expand Down Expand Up @@ -1605,47 +1641,9 @@ export class Wallet implements IWallet {
const sharing = (await this.bitgo.getSharingKey({ email: params.email.toLowerCase() })) as any;
let sharedKeychain;
if (needsKeychain) {
try {
const keychain = await this.getEncryptedUserKeychain();
// Decrypt the user key with a passphrase
if (keychain.encryptedPrv) {
if (!params.walletPassphrase) {
throw new Error('Missing walletPassphrase argument');
}
const userPrv = decryptKeychainPrivateKey(this.bitgo, keychain, params.walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}
keychain.prv = userPrv;

const eckey = makeRandomKey();
const secret = getSharedSecret(eckey, Buffer.from(sharing.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) {
pub =
this.baseCoin.getMPCAlgorithm() === 'eddsa'
? EddsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain)
: EcdsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain);
}
sharedKeychain = {
pub,
encryptedPrv: newEncryptedPrv,
fromPubKey: eckey.publicKey.toString('hex'),
toPubKey: sharing.pubkey,
path: sharing.path,
};
}
} catch (e) {
if (e.message === 'No encrypted keychains on this wallet.') {
sharedKeychain = {};
// ignore this error because this looks like a cold wallet
} else {
throw e;
}
}
sharedKeychain = await this.prepareSharedKeychain(params.walletPassphrase, sharing.pubkey, sharing.path);
}

const options: CreateShareOptions = {
user: sharing.userId,
permissions: params.permissions,
Expand Down

0 comments on commit c2234f2

Please sign in to comment.