diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 7c130fcf8f..778e9040f7 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -1054,6 +1054,15 @@ export async function handleKeychainChangePassword( throw new ApiResponseError(`Keychain ${req.params.id} not found`, 404); } + // OFC wallet supports multiple keys (one per spender) on a single keychain. + const isMultiUserKey = (keychain.coinSpecific?.[coinName]?.['features'] ?? []).includes('multi-user-key'); + if (isMultiUserKey && !keychain.pub) { + throw new ApiResponseError( + `Unexpected missing field "keychain.pub". Please contact support@bitgo.com for more details`, + 500 + ); + } + const updatedKeychain = coin.keychains().updateSingleKeychainPassword({ keychain, oldPassword, @@ -1062,6 +1071,7 @@ export async function handleKeychainChangePassword( return bitgo.put(coin.url(`/key/${updatedKeychain.id}`)).send({ encryptedPrv: updatedKeychain.encryptedPrv, + pub: keychain.pub, }); } diff --git a/modules/express/src/typedRoutes/api/v2/keychainChangePassword.ts b/modules/express/src/typedRoutes/api/v2/keychainChangePassword.ts index 6aff9f5555..0613d85151 100644 --- a/modules/express/src/typedRoutes/api/v2/keychainChangePassword.ts +++ b/modules/express/src/typedRoutes/api/v2/keychainChangePassword.ts @@ -33,6 +33,7 @@ export const KeychainChangePasswordResponse = { /** Invalid request or not found */ 400: BitgoExpressError, 404: BitgoExpressError, + 500: BitgoExpressError, } as const; /** diff --git a/modules/express/test/unit/clientRoutes/changeKeychainPassword.ts b/modules/express/test/unit/clientRoutes/changeKeychainPassword.ts index b98a027714..ae79965957 100644 --- a/modules/express/test/unit/clientRoutes/changeKeychainPassword.ts +++ b/modules/express/test/unit/clientRoutes/changeKeychainPassword.ts @@ -7,12 +7,19 @@ import { BitGo } from 'bitgo'; import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api'; import { decodeOrElse } from '@bitgo/sdk-core'; import { KeychainChangePasswordResponse } from '../../../src/typedRoutes/api/v2/keychainChangePassword'; +import * as assert from 'assert'; +import { ApiResponseError } from '../../../src/errors'; describe('Change Wallet Password', function () { + const id = '23423423423423'; + const oldPassword = 'oldPasswordString'; + const newPassword = 'newPasswordString'; + + const keychainBaseCoinStub = { + keychains: () => ({ updateSingleKeychainPassword: () => Promise.resolve({ result: 'stubbed' }) }), + }; + it('should change wallet password', async function () { - const keychainBaseCoinStub = { - keychains: () => ({ updateSingleKeychainPassword: () => Promise.resolve({ result: 'stubbed' }) }), - }; const keychainStub = { baseCoin: keychainBaseCoinStub, }; @@ -35,9 +42,6 @@ describe('Change Wallet Password', function () { }); const coin = 'talgo'; - const id = '23423423423423'; - const oldPassword = 'oldPasswordString'; - const newPassword = 'newPasswordString'; const mockRequest = { bitgo: stubBitgo, params: { @@ -62,4 +66,167 @@ describe('Change Wallet Password', function () { }); ({ result: '200 OK' }).should.be.eql(result); }); + + describe('ofc wallet test', function () { + it('should change ofc multi-user-key wallet password', async function () { + const keychainStub = { + baseCoin: keychainBaseCoinStub, + coinSpecific: { + ofc: { + features: ['multi-user-key'], + }, + }, + pub: 'pub', + }; + + const coinStub = { + keychains: () => ({ + get: () => Promise.resolve(keychainStub), + updateSingleKeychainPassword: () => ({ result: 'stubbed' }), + }), + url: () => 'url', + }; + + const sendStub = sinon.stub().resolves({ result: '200 OK' }); + const stubBitgo = sinon.createStubInstance(BitGo as any, { + coin: coinStub, + }); + stubBitgo['put'] = sinon.stub().returns({ + send: sendStub, + }); + + const coin = 'ofc'; + const mockRequest = { + bitgo: stubBitgo, + params: { + coin, + id, + }, + body: { + oldPassword, + newPassword, + }, + decoded: { + oldPassword, + newPassword, + coin, + id, + }, + } as unknown as ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>; + + const result = await handleKeychainChangePassword(mockRequest); + decodeOrElse('KeychainChangePasswordResponse200', KeychainChangePasswordResponse[200], result, (errors) => { + throw new Error(`Response did not match expected codec: ${errors}`); + }); + ({ result: '200 OK' }).should.be.eql(result); + sinon.assert.calledWith(sendStub, { encryptedPrv: sinon.match.any, pub: 'pub' }); + }); + + it('should change ofc non multi-user-key wallet password', async function () { + const keychainStub = { + baseCoin: keychainBaseCoinStub, + coinSpecific: { + ofc: { + features: [], + }, + }, + pub: 'pub', + }; + + const coinStub = { + keychains: () => ({ + get: () => Promise.resolve(keychainStub), + updateSingleKeychainPassword: () => ({ result: 'stubbed' }), + }), + url: () => 'url', + }; + + const sendStub = sinon.stub().resolves({ result: '200 OK' }); + const stubBitgo = sinon.createStubInstance(BitGo as any, { + coin: coinStub, + }); + stubBitgo['put'] = sinon.stub().returns({ + send: sendStub, + }); + + const coin = 'ofc'; + const mockRequest = { + bitgo: stubBitgo, + params: { + coin, + id, + }, + body: { + oldPassword, + newPassword, + }, + decoded: { + oldPassword, + newPassword, + coin, + id, + }, + } as unknown as ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>; + + const result = await handleKeychainChangePassword(mockRequest); + decodeOrElse('KeychainChangePasswordResponse200', KeychainChangePasswordResponse[200], result, (errors) => { + throw new Error(`Response did not match expected codec: ${errors}`); + }); + ({ result: '200 OK' }).should.be.eql(result); + sinon.assert.calledWith(sendStub, { encryptedPrv: sinon.match.any, pub: 'pub' }); + }); + + it('should throw updating ofc multi-user-key without a valid pub', async function () { + const keychainStub = { + baseCoin: keychainBaseCoinStub, + coinSpecific: { + ofc: { + features: ['multi-user-key'], + }, + }, + }; + + const coinStub = { + keychains: () => ({ + get: () => Promise.resolve(keychainStub), + updateSingleKeychainPassword: () => ({ result: 'stubbed' }), + }), + url: () => 'url', + }; + + const sendStub = sinon.stub().resolves({ result: '200 OK' }); + const stubBitgo = sinon.createStubInstance(BitGo as any, { + coin: coinStub, + }); + stubBitgo['put'] = sinon.stub().returns({ + send: sendStub, + }); + + const coin = 'ofc'; + const mockRequest = { + bitgo: stubBitgo, + params: { + coin, + id, + }, + body: { + oldPassword, + newPassword, + }, + decoded: { + oldPassword, + newPassword, + coin, + id, + }, + } as unknown as ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>; + + await assert.rejects( + async () => await handleKeychainChangePassword(mockRequest), + (err: ApiResponseError) => + err.status === 500 && + err.message === `Unexpected missing field "keychain.pub". Please contact support@bitgo.com for more details` + ); + }); + }); });