Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected] for more details`,
500
);
}

const updatedKeychain = coin.keychains().updateSingleKeychainPassword({
keychain,
oldPassword,
Expand All @@ -1062,6 +1071,7 @@ export async function handleKeychainChangePassword(

return bitgo.put(coin.url(`/key/${updatedKeychain.id}`)).send({
encryptedPrv: updatedKeychain.encryptedPrv,
pub: keychain.pub,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const KeychainChangePasswordResponse = {
/** Invalid request or not found */
400: BitgoExpressError,
404: BitgoExpressError,
500: BitgoExpressError,
} as const;

/**
Expand Down
179 changes: 173 additions & 6 deletions modules/express/test/unit/clientRoutes/changeKeychainPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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: {
Expand All @@ -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 [email protected] for more details`
);
});
});
});