Skip to content

Commit

Permalink
Merge pull request #3998 from BitGo/CE-2928
Browse files Browse the repository at this point in the history
chore: add ecdh keychain creation on login
  • Loading branch information
bitgoAaron authored Oct 18, 2023
2 parents 8423644 + 1a4e0e9 commit 4c8026a
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 29 deletions.
90 changes: 84 additions & 6 deletions modules/bitgo/test/unit/bitgo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
//

import * as crypto from 'crypto';
import * as should from 'should';
import * as nock from 'nock';
import * as should from 'should';

import * as BitGoJS from '../../src/index';
import { common } from '@bitgo/sdk-core';
const rp = require('request-promise');
import * as _ from 'lodash';
import { bip32, ECPair } from '@bitgo/utxo-lib';
import * as _ from 'lodash';
import * as BitGoJS from '../../src/index';
const rp = require('request-promise');

import { TestBitGo } from '@bitgo/sdk-test';
import { BitGo } from '../../src/bitgo';
Expand Down Expand Up @@ -129,7 +129,12 @@ describe('BitGo Prototype Methods', function () {
bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri: 'https://microservices.uri' } as any);
const scope = nock(BitGoJS.Environments[bitgo.getEnv()].uri)
.post('/api/auth/v1/session')
.reply(200, { user: '[email protected]', access_token: 'token12356' });
.reply(200, {
user: {
username: '[email protected]',
},
access_token: 'token12356',
});

await bitgo.authenticate(authenticateRequest);
scope.isDone().should.be.true();
Expand All @@ -139,7 +144,12 @@ describe('BitGo Prototype Methods', function () {
bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
const scope = nock(BitGoJS.Environments[bitgo.getEnv()].uri)
.post('/api/auth/v1/session')
.reply(200, { user: '[email protected]', access_token: 'token12356' });
.reply(200, {
user: {
username: '[email protected]',
},
access_token: 'token12356',
});

await bitgo.authenticate(authenticateRequest);
scope.isDone().should.be.true();
Expand Down Expand Up @@ -609,4 +619,72 @@ describe('BitGo Prototype Methods', function () {
);
});
});

describe('authenticate', function () {
afterEach(function ensureNoPendingMocks() {
nock.pendingMocks().should.be.empty();
});

it('should get the ecdhKeychain if ensureEcdhKeychain is set and user already has ecdhKeychain', async function () {
nock('https://bitgo.fakeurl')
.post('/api/auth/v1/session')
.reply(200, {
access_token: 'access_token',
user: { username: '[email protected]' },
});
nock('https://bitgo.fakeurl')
.get('/api/v1/user/settings')
.reply(200, {
settings: {
ecdhKeychain: 'some-existing-xpub',
},
});

const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
const response = await bitgo.authenticate({
username: '[email protected]',
password: 'password123',
otp: '000000',
ensureEcdhKeychain: true,
});

should.exist(response.user.ecdhKeychain);
response.user.ecdhKeychain.should.equal('some-existing-xpub');
});
it('should create the ecdhKeychain if ensureEcdhKeychain is set and the user does not already have ecdhKeychain', async function () {
nock('https://bitgo.fakeurl')
.post('/api/auth/v1/session')
.reply(200, {
access_token: 'access_token',
user: { username: '[email protected]' },
});
/**
* This is {} because want to make sure the user has no ecdhXpub set before we set it
*/
nock('https://bitgo.fakeurl').get('/api/v1/user/settings').reply(200, {
settings: {},
});
nock('https://bitgo.fakeurl').post('/api/v1/keychain').reply(200, {
xpub: 'some-xpub',
});
nock('https://bitgo.fakeurl')
.put('/api/v2/user/settings')
.reply(200, {
settings: {
ecdhKeychain: 'some-xpub',
},
});

const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
const response = await bitgo.authenticate({
username: '[email protected]',
password: 'password123',
otp: '000000',
ensureEcdhKeychain: true,
});

should.exist(response.user.ecdhKeychain);
response.user.ecdhKeychain.should.equal('some-xpub');
});
});
});
120 changes: 97 additions & 23 deletions modules/sdk-api/src/bitgoAPI.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
import * as _ from 'lodash';
import { bip32, ECPairInterface } from '@bitgo/utxo-lib';
import * as secp256k1 from 'secp256k1';
import * as bs58 from 'bs58';
import * as bitcoinMessage from 'bitcoinjs-message';
import {
handleResponseError,
handleResponseResult,
serializeRequestData,
setRequestQueryString,
toBitgoRequest,
verifyResponse,
} from './api';
import debugLib from 'debug';
import * as superagent from 'superagent';
import * as urlLib from 'url';
import { createHmac } from 'crypto';
import * as utxolib from '@bitgo/utxo-lib';
import {
AliasEnvironments,
BaseCoin,
bitcoin,
BitGoBase,
BitGoRequest,
CoinConstructor,
Expand All @@ -38,10 +21,30 @@ import {
sanitizeLegacyPath,
} from '@bitgo/sdk-core';
import * as sjcl from '@bitgo/sjcl';
import * as utxolib from '@bitgo/utxo-lib';
import { bip32, ECPairInterface } from '@bitgo/utxo-lib';
import * as bitcoinMessage from 'bitcoinjs-message';
import { isBrowser, isWebWorker } from 'browser-or-node';
import * as bs58 from 'bs58';
import { createHmac } from 'crypto';
import debugLib from 'debug';
import * as _ from 'lodash';
import * as secp256k1 from 'secp256k1';
import * as superagent from 'superagent';
import * as urlLib from 'url';
import {
handleResponseError,
handleResponseResult,
serializeRequestData,
setRequestQueryString,
toBitgoRequest,
verifyResponse,
} from './api';
import { decrypt, encrypt } from './encrypt';
import {
AccessTokenOptions,
AddAccessTokenResponse,
AddAccessTokenOptions,
AddAccessTokenResponse,
AuthenticateOptions,
AuthenticateWithAuthCodeOptions,
BitGoAPIOptions,
Expand All @@ -57,6 +60,7 @@ import {
GetEcdhSecretOptions,
GetUserOptions,
ListWebhookNotificationsOptions,
LoginResponse,
PingOptions,
ProcessedAuthenticationOptions,
ReconstitutedSecret,
Expand All @@ -79,8 +83,6 @@ import {
} from './types';
import shamir = require('secrets.js-grempe');
import pjson = require('../package.json');
import { decrypt, encrypt } from './encrypt';
import { isBrowser, isWebWorker } from 'browser-or-node';
const debug = debugLib('bitgo:api');

const Blockchain = require('./v1/blockchain');
Expand Down Expand Up @@ -748,10 +750,77 @@ export class BitGoAPI implements BitGoBase {
this._token = accessToken;
}

/**
* Creates a new ECDH keychain for the user.
* @param {string} loginPassword - The user's login password.
* @returns {Promise<any>} - A promise that resolves with the new ECDH keychain data.
* @throws {Error} - Throws an error if there is an issue creating the keychain.
*/
public async createUserEcdhKeychain(loginPassword: string): Promise<any> {
const keyData = this.keychains().create();
const hdNode = bitcoin.HDNode.fromBase58(keyData.xprv);

/**
* Add the new ECDH keychain to the user's account.
* @type {Promise<any>} - A promise that resolves with the new ECDH keychain.
*/
return await this.keychains().add({
source: 'ecdh',
xpub: hdNode.neutered().toBase58(),
encryptedXprv: this.encrypt({
password: loginPassword,
input: hdNode.toBase58(),
}),
});
}

/**
* Updates the user's settings with the provided parameters.
* @param {Object} params - The parameters to update the user's settings with.
* @returns {Promise<any>}
* @throws {Error} - Throws an error if there is an issue updating the user's settings.
*/
private async updateUserSettings(params: any): Promise<any> {
return this.put(this.url('/user/settings', 2)).send(params).result();
}

/**
* Ensures that the user's ECDH keychain is created for wallet sharing and TSS wallets.
* If the keychain does not exist, it will be created and the user's settings will be updated.
* @param {string} loginPassword - The user's login password.
* @returns {Promise<any>} - A promise that resolves with the user's settings ensuring we have the ecdhKeychain in there.
* @throws {Error} - Throws an error if there is an issue creating the keychain or updating the user's settings.
*/
private async ensureUserEcdhKeychainIsCreated(loginPassword: string): Promise<any> {
/**
* Get the user's current settings.
*/
const userSettings = await this.get(this.url('/user/settings')).result();
/**
* If the user's ECDH keychain does not exist, create a new keychain and update the user's settings.
*/
if (!userSettings.settings.ecdhKeychain) {
const newKeychain = await this.createUserEcdhKeychain(loginPassword);
await this.updateUserSettings({
settings: {
ecdhKeychain: newKeychain.xpub,
},
});
/**
* Update the user's settings object with the new ECDH keychain.
*/
userSettings.settings.ecdhKeychain = newKeychain.xpub;
}
/**
* Return the user's ECDH keychain settings.
*/
return userSettings.settings;
}

/**
* Login to the bitgo platform.
*/
async authenticate(params: AuthenticateOptions): Promise<any> {
async authenticate(params: AuthenticateOptions): Promise<LoginResponse | any> {
try {
if (!_.isObject(params)) {
throw new Error('required object params');
Expand Down Expand Up @@ -805,7 +874,12 @@ export class BitGoAPI implements BitGoBase {
response.body.access_token = this._token;
}

return handleResponseResult<any>()(response);
const userSettings = params.ensureEcdhKeychain ? await this.ensureUserEcdhKeychainIsCreated(password) : undefined;
if (userSettings?.ecdhKeychain) {
response.body.user.ecdhKeychain = userSettings.ecdhKeychain;
}

return handleResponseResult<LoginResponse>()(response);
} catch (e) {
handleResponseError(e);
}
Expand Down
25 changes: 25 additions & 0 deletions modules/sdk-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ export interface AuthenticateOptions {
forceSMS?: boolean;
extensible?: boolean;
forceV1Auth?: boolean;
/**
* Whether or not to ensure that the user's ECDH keychain is created.
* @type {boolean}
* @default false
* @description If set to true, the user's ECDH keychain will be created if it does not already exist.
* The ecdh keychain is a user level keychain that enables the sharing of secret material,
* primarily for wallet sharing, as well as the signing of less private material such as various cryptographic challenges.
* It is highly recommended that this is always set to avoid any issues when using a BitGo wallet
*/
ensureEcdhKeychain?: boolean;
}

export interface ProcessedAuthenticationOptions {
Expand Down Expand Up @@ -275,3 +285,18 @@ export interface RegisterPushTokenOptions {
pushToken: unknown;
operatingSystem: unknown;
}

export interface LoginResponse {
// The API session route does not return this. It's annotated by the SDK
access_token?: string;
derivationPath: string;
encryptedECDHXprv?: string;
encryptedToken?: string;
// Unit timestamp of expiration
expires_at: number;
// seconds in which the token will expire from issuance
expires_in: number;
scope: string[];
token_type: string;
user: unknown;
}

0 comments on commit 4c8026a

Please sign in to comment.