Skip to content

Commit 419045f

Browse files
committed
feat(express): migrate update keychain passphrase to typed routes
TICKET: WP-5417
1 parent 9919b93 commit 419045f

File tree

5 files changed

+110
-26
lines changed

5 files changed

+110
-26
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,27 +1022,23 @@ async function handleWalletUpdate(req: express.Request): Promise<unknown> {
10221022
* Changes a keychain's passphrase, re-encrypting the key to a new password
10231023
* @param req
10241024
*/
1025-
export async function handleKeychainChangePassword(req: express.Request): Promise<unknown> {
1026-
const { oldPassword, newPassword, otp } = req.body;
1027-
if (!oldPassword || !newPassword) {
1028-
throw new ApiResponseError('Missing 1 or more required fields: [oldPassword, newPassword]', 400);
1029-
}
1025+
export async function handleKeychainChangePassword(
1026+
req: ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>
1027+
): Promise<unknown> {
1028+
const { oldPassword, newPassword, otp, coin: coinName, id } = req.decoded;
10301029
const reqId = new RequestTracer();
10311030

10321031
const bitgo = req.bitgo;
1033-
const coin = bitgo.coin(req.params.coin);
1032+
const coin = bitgo.coin(coinName);
10341033

10351034
if (otp) {
10361035
await bitgo.unlock({ otp });
10371036
}
10381037

10391038
const keychain = await coin.keychains().get({
1040-
id: req.params.id,
1039+
id: id,
10411040
reqId,
10421041
});
1043-
if (!keychain) {
1044-
throw new ApiResponseError(`Keychain ${req.params.id} not found`, 404);
1045-
}
10461042

10471043
const updatedKeychain = coin.keychains().updateSingleKeychainPassword({
10481044
keychain,
@@ -1610,12 +1606,10 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16101606
app.put('/express/api/v2/:coin/wallet/:id', parseBody, prepareBitGo(config), promiseWrapper(handleWalletUpdate));
16111607

16121608
// change wallet passphrase
1613-
app.post(
1614-
'/api/v2/:coin/keychain/:id/changepassword',
1615-
parseBody,
1609+
router.post('express.keychain.changePassword', [
16161610
prepareBitGo(config),
1617-
promiseWrapper(handleKeychainChangePassword)
1618-
);
1611+
typedPromiseWrapper(handleKeychainChangePassword),
1612+
]);
16191613

16201614
router.post('express.v2.wallet.createAddress', [prepareBitGo(config), typedPromiseWrapper(handleV2CreateAddress)]);
16211615

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { PutPendingApproval } from './v1/pendingApproval';
1515
import { PostSignTransaction } from './v1/signTransaction';
1616
import { PostKeychainLocal } from './v2/keychainLocal';
1717
import { GetLightningState } from './v2/lightningState';
18+
import { PostKeychainChangePassword } from './v2/keychainChangePassword';
1819
import { PostLightningInitWallet } from './v2/lightningInitWallet';
1920
import { PostUnlockLightningWallet } from './v2/unlockWallet';
2021
import { PostVerifyCoinAddress } from './v2/verifyAddress';
@@ -64,6 +65,9 @@ export const ExpressApi = apiSpec({
6465
'express.lightning.getState': {
6566
get: GetLightningState,
6667
},
68+
'express.keychain.changePassword': {
69+
post: PostKeychainChangePassword,
70+
},
6771
'express.lightning.initWallet': {
6872
post: PostLightningInitWallet,
6973
},
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Path parameters for changing a keychain's password
7+
*/
8+
export const KeychainChangePasswordParams = {
9+
/** Coin identifier (e.g. btc, tbtc, eth) */
10+
coin: t.string,
11+
/** The keychain id */
12+
id: t.string,
13+
} as const;
14+
15+
/**
16+
* Request body for changing a keychain's password
17+
*/
18+
export const KeychainChangePasswordBody = {
19+
/** The old password used to encrypt the keychain */
20+
oldPassword: t.string,
21+
/** The new password to re-encrypt the keychain */
22+
newPassword: t.string,
23+
/** Optional OTP to unlock the session if required */
24+
otp: optional(t.string),
25+
} as const;
26+
27+
/**
28+
* Response for changing a keychain's password
29+
*/
30+
export const KeychainChangePasswordResponse = {
31+
/** Successful update */
32+
200: t.unknown,
33+
/** Invalid request or not found */
34+
400: BitgoExpressError,
35+
404: BitgoExpressError,
36+
} as const;
37+
38+
/**
39+
* Change a keychain's passphrase, re-encrypting the key to a new password.
40+
*
41+
* @operationId express.v2.keychain.changePassword
42+
*/
43+
export const PostKeychainChangePassword = httpRoute({
44+
path: '/api/v2/{coin}/keychain/{id}/changepassword',
45+
method: 'POST',
46+
request: httpRequest({
47+
params: KeychainChangePasswordParams,
48+
body: KeychainChangePasswordBody,
49+
}),
50+
response: KeychainChangePasswordResponse,
51+
});

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import * as sinon from 'sinon';
2-
32
import 'should-http';
43
import 'should-sinon';
54
import '../../lib/asserts';
6-
7-
import * as express from 'express';
8-
95
import { handleKeychainChangePassword } from '../../../src/clientRoutes';
10-
116
import { BitGo } from 'bitgo';
7+
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
8+
import { decodeOrElse } from '@bitgo/sdk-core';
9+
import { KeychainChangePasswordResponse } from '../../../src/typedRoutes/api/v2/keychainChangePassword';
1210

1311
describe('Change Wallet Password', function () {
1412
it('should change wallet password', async function () {
@@ -36,19 +34,32 @@ describe('Change Wallet Password', function () {
3634
}),
3735
});
3836

37+
const coin = 'talgo';
38+
const id = '23423423423423';
39+
const oldPassword = 'oldPasswordString';
40+
const newPassword = 'newPasswordString';
3941
const mockRequest = {
4042
bitgo: stubBitgo,
4143
params: {
42-
coin: 'talgo',
43-
id: '23423423423423',
44+
coin,
45+
id,
4446
},
4547
body: {
46-
oldPassword: 'oldPasswordString',
47-
newPassword: 'newPasswordString',
48+
oldPassword,
49+
newPassword,
4850
},
49-
};
51+
decoded: {
52+
oldPassword,
53+
newPassword,
54+
coin,
55+
id,
56+
},
57+
} as unknown as ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>;
5058

51-
const result = await handleKeychainChangePassword(mockRequest as express.Request & typeof mockRequest);
59+
const result = await handleKeychainChangePassword(mockRequest);
60+
decodeOrElse('KeychainChangePasswordResponse200', KeychainChangePasswordResponse[200], result, (errors) => {
61+
throw new Error(`Response did not match expected codec: ${errors}`);
62+
});
5263
({ result: '200 OK' }).should.be.eql(result);
5364
});
5465
});

modules/express/test/unit/typedRoutes/decode.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet';
1616
import { OfcSignPayloadBody } from '../../../src/typedRoutes/api/v2/ofcSignPayload';
1717
import { CreateAddressBody, CreateAddressParams } from '../../../src/typedRoutes/api/v2/createAddress';
18+
import {
19+
KeychainChangePasswordBody,
20+
KeychainChangePasswordParams,
21+
} from '../../../src/typedRoutes/api/v2/keychainChangePassword';
1822

1923
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
2024
const result = codec.decode(input);
@@ -243,4 +247,24 @@ describe('io-ts decode tests', function () {
243247
assertDecode(t.type(CreateAddressBody), { eip1559: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 } });
244248
assertDecode(t.type(CreateAddressBody), {});
245249
});
250+
251+
it('express.keychain.changePassword', function () {
252+
// missing id
253+
assert.throws(() => assertDecode(t.type(KeychainChangePasswordParams), { coin: 'btc' }));
254+
// invalid coin type
255+
assert.throws(() => assertDecode(t.type(KeychainChangePasswordParams), { coin: 123, id: 'abc' }));
256+
// valid params
257+
assertDecode(t.type(KeychainChangePasswordParams), { coin: 'btc', id: 'abc' });
258+
// missing required fields
259+
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), {}));
260+
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 'a' }));
261+
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), { newPassword: 'b' }));
262+
// invalid types
263+
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 1, newPassword: 'b' }));
264+
assert.throws(() => assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 'a', newPassword: 2 }));
265+
// valid minimal
266+
assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 'a', newPassword: 'b' });
267+
// valid with optional otp
268+
assertDecode(t.type(KeychainChangePasswordBody), { oldPassword: 'a', newPassword: 'b', otp: '123456' });
269+
});
246270
});

0 commit comments

Comments
 (0)