Skip to content
Open
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
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 @@ -27,73 +28,151 @@ import { PutFanoutUnspents } from './v1/fanoutUnspents';
import { PostOfcSignPayload } from './v2/ofcSignPayload';
import { PostWalletRecoverToken } from './v2/walletRecoverToken';

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 @@ -102,7 +181,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