Skip to content

Commit 71f892a

Browse files
authored
Merge pull request #7594 from BitGo/WP-6902
feat: added rotate keychain method for multi-user-key ofc wallet
2 parents 9d42a20 + 49f0701 commit 71f892a

File tree

3 files changed

+159
-4
lines changed

3 files changed

+159
-4
lines changed

modules/bitgo/test/v2/unit/keychains.ts

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import nock = require('nock');
99
import should = require('should');
1010
import * as sinon from 'sinon';
1111

12-
import { common, decodeOrElse, ECDSAUtils, EDDSAUtils, Keychains, OvcShare } from '@bitgo/sdk-core';
12+
import { common, decodeOrElse, ECDSAUtils, EDDSAUtils, Keychain, Keychains, OvcShare } from '@bitgo/sdk-core';
1313
import { TestBitGo } from '@bitgo/sdk-test';
1414
import { BitGo } from '../../../src/bitgo';
15+
import { SinonStub } from 'sinon';
1516

1617
describe('V2 Keychains', function () {
1718
let bitgo;
@@ -174,9 +175,7 @@ describe('V2 Keychains', function () {
174175
'expected new password to be a string'
175176
);
176177

177-
(() => keychains.updateSingleKeychainPassword({ oldPassword: '1234', newPassword: 5678 })).should.throw(
178-
'expected new password to be a string'
179-
);
178+
(() => keychains.updateSingleKeychainPassword({ oldPassword: '1234', newPassword: 5678 })).should.throw();
180179

181180
(() => keychains.updateSingleKeychainPassword({ oldPassword: '1234', newPassword: '5678' })).should.throw(
182181
'expected keychain to be an object with an encryptedPrv property'
@@ -830,4 +829,92 @@ describe('V2 Keychains', function () {
830829
const decryptedPrv = bitgo.decrypt({ input: backup.encryptedPrv, password: 't3stSicretly!' });
831830
decryptedPrv.should.startWith('xprv');
832831
});
832+
833+
describe('Rotate OFC multi-user-key keychains', function () {
834+
let ofcBaseCoin;
835+
let ofcKeychains;
836+
const mockOfcKeychain: Keychain = {
837+
id: 'ofcKeychainId',
838+
pub: 'ofcKeychainPub',
839+
encryptedPrv: 'ofcEncryptedPrv',
840+
source: 'user',
841+
coinSpecific: {
842+
ofc: {
843+
features: ['multi-user-key'],
844+
},
845+
},
846+
type: 'tss',
847+
};
848+
let nonOfcBaseCoin;
849+
let nonOfcKeychains;
850+
const mockNonOfcKeychain: Keychain = {
851+
id: 'nonOfcKeychainId',
852+
pub: 'nonOfcKeychainPub',
853+
source: 'user',
854+
type: 'tss',
855+
};
856+
857+
const mockNewKeypair = {
858+
pub: 'newPub',
859+
prv: 'newPrv',
860+
};
861+
862+
let sandbox;
863+
let updateKeychainStub: SinonStub;
864+
let createKeypairStub: SinonStub;
865+
let encryptionStub: SinonStub;
866+
867+
beforeEach(function () {
868+
ofcBaseCoin = bitgo.coin('ofc');
869+
ofcKeychains = ofcBaseCoin.keychains();
870+
871+
nonOfcBaseCoin = bitgo.coin('hteth');
872+
nonOfcKeychains = nonOfcBaseCoin.keychains();
873+
874+
sandbox = sinon.createSandbox();
875+
updateKeychainStub = sandbox.stub().returns({ result: sandbox.stub().resolves() });
876+
sandbox.stub(BitGo.prototype, 'put').returns({ send: updateKeychainStub });
877+
createKeypairStub = sandbox.stub(ofcKeychains, 'create').returns(mockNewKeypair);
878+
encryptionStub = sandbox.stub(BitGo.prototype, 'encrypt').returns('newEncryptedPrv');
879+
});
880+
881+
afterEach(function () {
882+
sandbox.restore();
883+
});
884+
885+
it('should rotate ofc multi-user-key properly', async function () {
886+
nock(bgUrl).get(`/api/v2/ofc/key/${mockOfcKeychain.id}`).query(true).reply(200, mockOfcKeychain);
887+
888+
await ofcKeychains.rotateKeychain({ id: mockOfcKeychain.id, password: '1234' });
889+
sinon.assert.called(createKeypairStub);
890+
sinon.assert.calledWith(encryptionStub, { input: mockNewKeypair.prv, password: '1234' });
891+
sinon.assert.calledWith(updateKeychainStub, {
892+
pub: mockNewKeypair.pub,
893+
encryptedPrv: 'newEncryptedPrv',
894+
reqId: undefined,
895+
});
896+
});
897+
898+
it('should allow user to supply pub and encryptedPrv directly', async function () {
899+
nock(bgUrl).get(`/api/v2/ofc/key/${mockOfcKeychain.id}`).query(true).reply(200, mockOfcKeychain);
900+
901+
await ofcKeychains.rotateKeychain({ id: mockOfcKeychain.id, pub: 'pub', encryptedPrv: 'encryptedPrv' });
902+
sinon.assert.notCalled(createKeypairStub);
903+
sinon.assert.notCalled(encryptionStub);
904+
sinon.assert.calledWith(updateKeychainStub, {
905+
pub: 'pub',
906+
encryptedPrv: 'encryptedPrv',
907+
reqId: undefined,
908+
});
909+
});
910+
911+
it('should throw when trying to rotate non-ofc keychain', async function () {
912+
nock(bgUrl).get(`/api/v2/hteth/key/${mockNonOfcKeychain.id}`).query(true).reply(200, mockNonOfcKeychain);
913+
914+
await assert.rejects(
915+
async () => await nonOfcKeychains.rotateKeychain({ id: mockNonOfcKeychain.id, password: '1234' }),
916+
(err: Error) => err.message === 'rotateKeychain is only permitted for ofc multi-user-key wallet'
917+
);
918+
});
919+
});
833920
});

modules/sdk-core/src/bitgo/keychain/iKeychains.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,25 @@ export interface UpdateSingleKeychainPasswordOptions {
8888
newPassword?: string;
8989
}
9090

91+
/**
92+
* Parameters for the rotateKeychain method for ofc multi-user-key
93+
* @property {string} id - the public id of the keychain
94+
* @property {string} password - the user password use to encrypt/decrypt the private key
95+
* @property {IRequestTracer} reqId - optional reqId
96+
*/
97+
export type RotateKeychainOptions =
98+
| {
99+
id: string;
100+
password: string;
101+
reqId?: IRequestTracer;
102+
}
103+
| {
104+
id: string;
105+
pub: string;
106+
encryptedPrv: string;
107+
reqId?: IRequestTracer;
108+
};
109+
91110
export interface AddKeychainOptions {
92111
pub?: string;
93112
commonPub?: string;
@@ -214,4 +233,5 @@ export interface IKeychains {
214233
recreateMpc(params: RecreateMpcOptions): Promise<KeychainsTriplet>;
215234
createTssBitGoKeyFromOvcShares(ovcOutput: OvcToBitGoJSON, enterprise?: string): Promise<BitGoKeyFromOvcShares>;
216235
createUserKeychain(userPassword: string): Promise<Keychain>;
236+
rotateKeychain(params: RotateKeychainOptions): Promise<Keychain>;
217237
}

modules/sdk-core/src/bitgo/keychain/keychains.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ListKeychainOptions,
2020
ListKeychainsResult,
2121
RecreateMpcOptions,
22+
RotateKeychainOptions,
2223
UpdatePasswordOptions,
2324
UpdateSingleKeychainPasswordOptions,
2425
} from './iKeychains';
@@ -517,4 +518,51 @@ export class Keychains implements IKeychains {
517518
prv: newKeychain.prv,
518519
};
519520
}
521+
522+
/**
523+
* Rotate an ofc multi-user-keychain by recreating a new keypair, encrypt it using the user password and sent it to WP
524+
* This is only meant to be called by ofc multi-user-wallet's key, and will throw if otherwise.
525+
*
526+
* Requires 2fa auth before calling. Call the bitgo.unlock() SDK method to unlock the session first.
527+
* @param params parameters for the rotate keychain method
528+
* @return rotatedKeychain
529+
*/
530+
async rotateKeychain(params: RotateKeychainOptions): Promise<Keychain> {
531+
const keyChain = await this.get({ id: params.id });
532+
if (!Keychains.isMultiUserKey(keyChain)) {
533+
throw new Error(`rotateKeychain is only permitted for ofc multi-user-key wallet`);
534+
}
535+
536+
let pub: string, encryptedPrv: string;
537+
if ('encryptedPrv' in params) {
538+
// client passed in pub and encryptedPrv directly
539+
pub = params.pub;
540+
encryptedPrv = params.encryptedPrv;
541+
} else {
542+
// bitgo generate pub and prv and encrypt it
543+
const { pub: keyPub, prv: keyPrv } = this.create();
544+
if (!keyPub) {
545+
throw Error('Expected a public key to be generated');
546+
}
547+
pub = keyPub;
548+
encryptedPrv = this.bitgo.encrypt({ input: keyPrv, password: params.password });
549+
}
550+
551+
return this.bitgo
552+
.put(this.baseCoin.url(`/key/${params.id}`))
553+
.send({
554+
encryptedPrv,
555+
pub,
556+
reqId: params.reqId,
557+
})
558+
.result();
559+
}
560+
561+
/**
562+
* Static helper method to determine if a keychain is a ofc multi-user-key
563+
* @param keychain
564+
*/
565+
static isMultiUserKey(keychain: Keychain): boolean {
566+
return (keychain.coinSpecific?.ofc?.['features'] ?? []).includes('multi-user-key');
567+
}
520568
}

0 commit comments

Comments
 (0)