Skip to content
22 changes: 10 additions & 12 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,24 +1022,24 @@ async function handleWalletUpdate(req: express.Request): Promise<unknown> {
* Changes a keychain's passphrase, re-encrypting the key to a new password
* @param req
*/
export async function handleKeychainChangePassword(req: express.Request): Promise<unknown> {
const { oldPassword, newPassword, otp } = req.body;
if (!oldPassword || !newPassword) {
throw new ApiResponseError('Missing 1 or more required fields: [oldPassword, newPassword]', 400);
}
export async function handleKeychainChangePassword(
req: ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>
): Promise<unknown> {
const { oldPassword, newPassword, otp, coin: coinName, id } = req.decoded;
const reqId = new RequestTracer();

const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
const coin = bitgo.coin(coinName);

if (otp) {
await bitgo.unlock({ otp });
}

const keychain = await coin.keychains().get({
id: req.params.id,
id: id,
reqId,
});

if (!keychain) {
throw new ApiResponseError(`Keychain ${req.params.id} not found`, 404);
}
Expand Down Expand Up @@ -1610,12 +1610,10 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
app.put('/express/api/v2/:coin/wallet/:id', parseBody, prepareBitGo(config), promiseWrapper(handleWalletUpdate));

// change wallet passphrase
app.post(
'/api/v2/:coin/keychain/:id/changepassword',
parseBody,
router.post('express.keychain.changePassword', [
prepareBitGo(config),
promiseWrapper(handleKeychainChangePassword)
);
typedPromiseWrapper(handleKeychainChangePassword),
]);

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

Expand Down
175 changes: 152 additions & 23 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { PutPendingApproval } from './v1/pendingApproval';
import { PostSignTransaction } from './v1/signTransaction';
import { PostKeychainLocal } from './v2/keychainLocal';
import { GetLightningState } from './v2/lightningState';
import { PostKeychainChangePassword } from './v2/keychainChangePassword';
import { PostLightningInitWallet } from './v2/lightningInitWallet';
import { PostUnlockLightningWallet } from './v2/unlockWallet';
import { PostVerifyCoinAddress } from './v2/verifyAddress';
Expand All @@ -30,73 +31,151 @@ import { PostCoinSignTx } from './v2/coinSignTx';
import { PostWalletSignTx } from './v2/walletSignTx';
import { PostWalletTxSignTSS } from './v2/walletTxSignTSS';

export const ExpressApi = apiSpec({
// Too large types can cause the following error
//
// > error TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.
//
// As a workaround, only construct expressApi with a single key and add it to the type union at the end

export const ExpressPingApiSpec = apiSpec({
'express.ping': {
get: GetPing,
},
});

export const ExpressPingExpressApiSpec = apiSpec({
'express.pingExpress': {
get: GetPingExpress,
},
});

export const ExpressLoginApiSpec = apiSpec({
'express.login': {
post: PostLogin,
},
});

export const ExpressDecryptApiSpec = apiSpec({
'express.decrypt': {
post: PostDecrypt,
},
});

export const ExpressEncryptApiSpec = apiSpec({
'express.encrypt': {
post: PostEncrypt,
},
});

export const ExpressVerifyAddressApiSpec = apiSpec({
'express.verifyaddress': {
post: PostVerifyAddress,
},
});

export const ExpressVerifyCoinAddressApiSpec = apiSpec({
'express.verifycoinaddress': {
post: PostVerifyCoinAddress,
},
});

export const ExpressCalculateMinerFeeInfoApiSpec = apiSpec({
'express.calculateminerfeeinfo': {
post: PostCalculateMinerFeeInfo,
},
});

export const ExpressV1WalletAcceptShareApiSpec = apiSpec({
'express.v1.wallet.acceptShare': {
post: PostAcceptShare,
},
});

export const ExpressV1WalletSimpleCreateApiSpec = apiSpec({
'express.v1.wallet.simplecreate': {
post: PostSimpleCreate,
},
});

export const ExpressV1PendingApprovalsApiSpec = apiSpec({
'express.v1.pendingapprovals': {
put: PutPendingApproval,
},
});

export const ExpressV1WalletSignTransactionApiSpec = apiSpec({
'express.v1.wallet.signTransaction': {
post: PostSignTransaction,
},
'express.keychain.local': {
post: PostKeychainLocal,
},
'express.lightning.getState': {
get: GetLightningState,
},
'express.lightning.initWallet': {
post: PostLightningInitWallet,
},
'express.lightning.unlockWallet': {
post: PostUnlockLightningWallet,
},
'express.verifycoinaddress': {
post: PostVerifyCoinAddress,
},
'express.v2.wallet.createAddress': {
post: PostCreateAddress,
},
'express.calculateminerfeeinfo': {
post: PostCalculateMinerFeeInfo,
},
});

export const ExpressV1KeychainDeriveApiSpec = apiSpec({
'express.v1.keychain.derive': {
post: PostDeriveLocalKeyChain,
},
});

export const ExpressV1KeychainLocalApiSpec = apiSpec({
'express.v1.keychain.local': {
post: PostCreateLocalKeyChain,
},
});

export const ExpressV1PendingApprovalConstructTxApiSpec = apiSpec({
'express.v1.pendingapproval.constructTx': {
put: PutConstructPendingApprovalTx,
},
});

export const ExpressV1WalletConsolidateUnspentsApiSpec = apiSpec({
'express.v1.wallet.consolidateunspents': {
put: PutConsolidateUnspents,
},
});

export const ExpressV1WalletFanoutUnspentsApiSpec = apiSpec({
'express.v1.wallet.fanoutunspents': {
put: PutFanoutUnspents,
},
});

export const ExpressV2WalletCreateAddressApiSpec = apiSpec({
'express.v2.wallet.createAddress': {
post: PostCreateAddress,
},
});

export const ExpressKeychainLocalApiSpec = apiSpec({
'express.keychain.local': {
post: PostKeychainLocal,
},
});

export const ExpressKeychainChangePasswordApiSpec = apiSpec({
'express.keychain.changePassword': {
post: PostKeychainChangePassword,
},
});

export const ExpressLightningGetStateApiSpec = apiSpec({
'express.lightning.getState': {
get: GetLightningState,
},
});

export const ExpressLightningInitWalletApiSpec = apiSpec({
'express.lightning.initWallet': {
post: PostLightningInitWallet,
},
});

export const ExpressLightningUnlockWalletApiSpec = apiSpec({
'express.lightning.unlockWallet': {
post: PostUnlockLightningWallet,
},
});

export const ExpressOfcSignPayloadApiSpec = apiSpec({
'express.ofc.signPayload': {
post: PostOfcSignPayload,
},
Expand All @@ -114,7 +193,57 @@ export const ExpressApi = apiSpec({
},
});

export type ExpressApi = typeof ExpressApi;
export type ExpressApi = typeof ExpressPingApiSpec &
typeof ExpressPingExpressApiSpec &
typeof ExpressLoginApiSpec &
typeof ExpressDecryptApiSpec &
typeof ExpressEncryptApiSpec &
typeof ExpressVerifyAddressApiSpec &
typeof ExpressVerifyCoinAddressApiSpec &
typeof ExpressCalculateMinerFeeInfoApiSpec &
typeof ExpressV1WalletAcceptShareApiSpec &
typeof ExpressV1WalletSimpleCreateApiSpec &
typeof ExpressV1PendingApprovalsApiSpec &
typeof ExpressV1WalletSignTransactionApiSpec &
typeof ExpressV1KeychainDeriveApiSpec &
typeof ExpressV1KeychainLocalApiSpec &
typeof ExpressV1PendingApprovalConstructTxApiSpec &
typeof ExpressV1WalletConsolidateUnspentsApiSpec &
typeof ExpressV1WalletFanoutUnspentsApiSpec &
typeof ExpressV2WalletCreateAddressApiSpec &
typeof ExpressKeychainLocalApiSpec &
typeof ExpressKeychainChangePasswordApiSpec &
typeof ExpressLightningGetStateApiSpec &
typeof ExpressLightningInitWalletApiSpec &
typeof ExpressLightningUnlockWalletApiSpec &
typeof ExpressOfcSignPayloadApiSpec;

export const ExpressApi: ExpressApi = {
...ExpressPingApiSpec,
...ExpressPingExpressApiSpec,
...ExpressLoginApiSpec,
...ExpressDecryptApiSpec,
...ExpressEncryptApiSpec,
...ExpressVerifyAddressApiSpec,
...ExpressVerifyCoinAddressApiSpec,
...ExpressCalculateMinerFeeInfoApiSpec,
...ExpressV1WalletAcceptShareApiSpec,
...ExpressV1WalletSimpleCreateApiSpec,
...ExpressV1PendingApprovalsApiSpec,
...ExpressV1WalletSignTransactionApiSpec,
...ExpressV1KeychainDeriveApiSpec,
...ExpressV1KeychainLocalApiSpec,
...ExpressV1PendingApprovalConstructTxApiSpec,
...ExpressV1WalletConsolidateUnspentsApiSpec,
...ExpressV1WalletFanoutUnspentsApiSpec,
...ExpressV2WalletCreateAddressApiSpec,
...ExpressKeychainLocalApiSpec,
...ExpressKeychainChangePasswordApiSpec,
...ExpressLightningGetStateApiSpec,
...ExpressLightningInitWalletApiSpec,
...ExpressLightningUnlockWalletApiSpec,
...ExpressOfcSignPayloadApiSpec,
};

type ExtractDecoded<T> = T extends t.Type<any, infer O, any> ? O : never;
type FlattenDecoded<T> = T extends Record<string, unknown>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';

/**
* Path parameters for changing a keychain's password
*/
export const KeychainChangePasswordParams = {
/** Coin identifier (e.g. btc, tbtc, eth) */
coin: t.string,
/** The keychain id */
id: t.string,
} as const;

/**
* Request body for changing a keychain's password
*/
export const KeychainChangePasswordBody = {
/** The old password used to encrypt the keychain */
oldPassword: t.string,
/** The new password to re-encrypt the keychain */
newPassword: t.string,
/** Optional OTP to unlock the session if required */
otp: optional(t.string),
} as const;

/**
* Response for changing a keychain's password
*/
export const KeychainChangePasswordResponse = {
/** Successful update */
200: t.unknown,
/** Invalid request or not found */
400: BitgoExpressError,
404: BitgoExpressError,
} as const;

/**
* Change a keychain's passphrase, re-encrypting the key to a new password.
*
* @operationId express.v2.keychain.changePassword
*/
export const PostKeychainChangePassword = httpRoute({
path: '/api/v2/{coin}/keychain/{id}/changepassword',
method: 'POST',
request: httpRequest({
params: KeychainChangePasswordParams,
body: KeychainChangePasswordBody,
}),
response: KeychainChangePasswordResponse,
});
33 changes: 22 additions & 11 deletions modules/express/test/unit/clientRoutes/changeKeychainPassword.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import * as sinon from 'sinon';

import 'should-http';
import 'should-sinon';
import '../../lib/asserts';

import * as express from 'express';

import { handleKeychainChangePassword } from '../../../src/clientRoutes';

import { BitGo } from 'bitgo';
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
import { decodeOrElse } from '@bitgo/sdk-core';
import { KeychainChangePasswordResponse } from '../../../src/typedRoutes/api/v2/keychainChangePassword';

describe('Change Wallet Password', function () {
it('should change wallet password', async function () {
Expand Down Expand Up @@ -36,19 +34,32 @@ describe('Change Wallet Password', function () {
}),
});

const coin = 'talgo';
const id = '23423423423423';
const oldPassword = 'oldPasswordString';
const newPassword = 'newPasswordString';
const mockRequest = {
bitgo: stubBitgo,
params: {
coin: 'talgo',
id: '23423423423423',
coin,
id,
},
body: {
oldPassword: 'oldPasswordString',
newPassword: 'newPasswordString',
oldPassword,
newPassword,
},
};
decoded: {
oldPassword,
newPassword,
coin,
id,
},
} as unknown as ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>;

const result = await handleKeychainChangePassword(mockRequest as express.Request & typeof mockRequest);
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);
});
});
Loading