Skip to content

Commit 5ed104d

Browse files
committed
feat: added rotate keychain method for multi-user-key ofc wallet
added a new method to rotate ofc multi-user-key wallet's keychain TICKET: WP-6902
1 parent 49d8974 commit 5ed104d

File tree

6 files changed

+112
-8
lines changed

6 files changed

+112
-8
lines changed

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

Lines changed: 78 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;
@@ -171,9 +172,7 @@ describe('V2 Keychains', function () {
171172
'expected new password to be a string'
172173
);
173174

174-
(() => keychains.updateSingleKeychainPassword({ oldPassword: '1234', newPassword: 5678 })).should.throw(
175-
'expected new password to be a string'
176-
);
175+
(() => keychains.updateSingleKeychainPassword({ oldPassword: '1234', newPassword: 5678 })).should.throw();
177176

178177
(() => keychains.updateSingleKeychainPassword({ oldPassword: '1234', newPassword: '5678' })).should.throw(
179178
'expected keychain to be an object with an encryptedPrv property'
@@ -827,4 +826,79 @@ describe('V2 Keychains', function () {
827826
const decryptedPrv = bitgo.decrypt({ input: backup.encryptedPrv, password: 't3stSicretly!' });
828827
decryptedPrv.should.startWith('xprv');
829828
});
829+
830+
describe('Rotate OFC multi-user-key keychains', function () {
831+
let ofcBaseCoin;
832+
let ofcKeychains;
833+
const mockOfcKeychain: Keychain = {
834+
id: 'ofcKeychainId',
835+
pub: 'ofcKeychainPub',
836+
encryptedPrv: 'ofcEncryptedPrv',
837+
source: 'user',
838+
coinSpecific: {
839+
ofc: {
840+
features: ['multi-user-key'],
841+
},
842+
},
843+
type: 'tss',
844+
};
845+
let nonOfcBaseCoin;
846+
let nonOfcKeychains;
847+
const mockNonOfcKeychain: Keychain = {
848+
id: 'nonOfcKeychainId',
849+
pub: 'nonOfcKeychainPub',
850+
source: 'user',
851+
type: 'tss',
852+
};
853+
854+
const mockNewKeypair = {
855+
pub: 'newPub',
856+
prv: 'newPrv',
857+
};
858+
859+
let sandbox;
860+
let updateKeychainStub: SinonStub;
861+
let createKeypairStub: SinonStub;
862+
let encryptionStub: SinonStub;
863+
864+
beforeEach(function () {
865+
ofcBaseCoin = bitgo.coin('ofc');
866+
ofcKeychains = ofcBaseCoin.keychains();
867+
868+
nonOfcBaseCoin = bitgo.coin('hteth');
869+
nonOfcKeychains = nonOfcBaseCoin.keychains();
870+
871+
sandbox = sinon.createSandbox();
872+
updateKeychainStub = sandbox.stub().returns({ result: sandbox.stub().resolves() });
873+
sandbox.stub(BitGo.prototype, 'put').returns({ send: updateKeychainStub });
874+
createKeypairStub = sandbox.stub(ofcKeychains, 'create').returns(mockNewKeypair);
875+
encryptionStub = sandbox.stub(BitGo.prototype, 'encrypt').returns('newEncryptedPrv');
876+
});
877+
878+
afterEach(function () {
879+
sandbox.restore();
880+
});
881+
882+
it('should rotate ofc multi-user-key properly', async function () {
883+
nock(bgUrl).get(`/api/v2/ofc/key/${mockOfcKeychain.id}`).query(true).reply(200, mockOfcKeychain);
884+
885+
await ofcKeychains.rotateKeychain({ id: mockOfcKeychain.id, password: '1234' });
886+
sinon.assert.called(createKeypairStub);
887+
sinon.assert.calledWith(encryptionStub, { input: mockNewKeypair.prv, password: '1234' });
888+
sinon.assert.calledWith(updateKeychainStub, {
889+
pub: mockNewKeypair.pub,
890+
encryptedPrv: 'newEncryptedPrv',
891+
reqId: undefined,
892+
});
893+
});
894+
895+
it('should throw when trying to rotate non-ofc keychain', async function () {
896+
nock(bgUrl).get(`/api/v2/hteth/key/${mockNonOfcKeychain.id}`).query(true).reply(200, mockNonOfcKeychain);
897+
898+
await assert.rejects(
899+
async () => await nonOfcKeychains.rotateKeychain({ id: mockNonOfcKeychain.id, password: '1234' }),
900+
(err: Error) => err.message === 'rotateKeychain is only for ofc multi-user-key wallet'
901+
);
902+
});
903+
});
830904
});
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
{
2-
"61f039aad587c2000745c687373e0fa9": "{\"iv\":\"/Gnh+Ip1G+IOhy+Cms+umQ==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"FYnd1xwReTw=\",\"ct\":\"vgnCvdJ1Z9sqeV6urYxNsscwnkB/6eSPsZhzaW4Cuc7RKEY1uWNlleR0Tjtd8nlQuhsA5UXFpOID3lHHHjPDvB+jWtRm08I2F+HNGYuklWG12vIiSrY2KnkYRJkyCghn5Pq3iEimQb9M2kkwj5wf4EtfAiz9jsY=\"}"
3-
}
1+
{}

modules/express/src/clientRoutes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
encryptRsaWithAesGcm,
2222
GetNetworkPartnersResponse,
2323
GShare,
24+
Keychains,
2425
MPCType,
2526
ShareType,
2627
SignShare,

modules/express/test/unit/clientRoutes/changeKeychainPassword.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ describe('Change Wallet Password', function () {
173173
throw new Error(`Response did not match expected codec: ${errors}`);
174174
});
175175
({ result: '200 OK' }).should.be.eql(result);
176-
sinon.assert.calledWith(sendStub, { encryptedPrv: sinon.match.any, pub: undefined });
176+
sinon.assert.calledWith(sendStub, { encryptedPrv: sinon.match.any, pub: 'pub' });
177177
});
178178

179179
it('should throw updating ofc multi-user-key without a valid pub', async function () {

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

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

91+
export interface RotateKeychainOptions {
92+
id: string;
93+
password: string;
94+
reqId?: IRequestTracer;
95+
}
96+
9197
export interface AddKeychainOptions {
9298
pub?: string;
9399
commonPub?: string;
@@ -214,4 +220,5 @@ export interface IKeychains {
214220
recreateMpc(params: RecreateMpcOptions): Promise<KeychainsTriplet>;
215221
createTssBitGoKeyFromOvcShares(ovcOutput: OvcToBitGoJSON, enterprise?: string): Promise<BitGoKeyFromOvcShares>;
216222
createUserKeychain(userPassword: string): Promise<Keychain>;
223+
rotateKeychain(params: RotateKeychainOptions): Promise<Keychain>;
217224
}

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

Lines changed: 24 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,27 @@ export class Keychains implements IKeychains {
517518
prv: newKeychain.prv,
518519
};
519520
}
521+
522+
async rotateKeychain(params: RotateKeychainOptions): Promise<Keychain> {
523+
const keyChain = await this.get({ id: params.id });
524+
if (!Keychains.isMultiUserKey(keyChain)) {
525+
throw new Error(`rotateKeychain is only for ofc multi-user-key wallet`);
526+
}
527+
528+
const { pub, prv } = this.create();
529+
const encryptedPrv = this.bitgo.encrypt({ input: prv, password: params.password });
530+
531+
return this.bitgo
532+
.put(this.baseCoin.url(`/key/${params.id}`))
533+
.send({
534+
encryptedPrv,
535+
pub,
536+
reqId: params.reqId,
537+
})
538+
.result();
539+
}
540+
541+
static isMultiUserKey(keychain: Keychain): boolean {
542+
return (keychain.coinSpecific?.ofc?.['features'] ?? []).includes('multi-user-key');
543+
}
520544
}

0 commit comments

Comments
 (0)