Skip to content

Commit 4d04e20

Browse files
authored
Merge pull request #85 from BitGo/WP-5318/hide-recovery-apis-mbe
feat(mbe): Hide recovery APIs for mbe
2 parents cb12fae + 788e3f8 commit 4d04e20

10 files changed

Lines changed: 158 additions & 1 deletion

File tree

src/__tests__/api/master/musigRecovery.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe('POST /api/:coin/wallet/recovery', () => {
3131
enclavedExpressCert: 'dummy-cert',
3232
tlsMode: TlsMode.DISABLED,
3333
allowSelfSigned: true,
34+
recoveryMode: true,
3435
};
3536

3637
const app = expressApp(config);
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import 'should';
2+
import * as request from 'supertest';
3+
import nock from 'nock';
4+
import { app as expressApp } from '../../../masterExpressApp';
5+
import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
6+
import sinon from 'sinon';
7+
import * as middleware from '../../../shared/middleware';
8+
import * as masterMiddleware from '../../../api/master/middleware/middleware';
9+
import { BitGoRequest } from '../../../types/request';
10+
import { BitGoAPI } from '@bitgo-beta/sdk-api';
11+
import { EnclavedExpressClient } from '../../../api/master/clients/enclavedExpressClient';
12+
13+
describe('Non Recovery Tests', () => {
14+
let agent: request.SuperAgentTest;
15+
let mockBitgo: BitGoAPI;
16+
const enclavedExpressUrl = 'http://enclaved.invalid';
17+
const accessToken = 'test-token';
18+
const config: MasterExpressConfig = {
19+
appMode: AppMode.MASTER_EXPRESS,
20+
port: 0,
21+
bind: 'localhost',
22+
timeout: 60000,
23+
env: 'test',
24+
disableEnvCheck: true,
25+
authVersion: 2,
26+
enclavedExpressUrl: enclavedExpressUrl,
27+
enclavedExpressCert: 'dummy-cert',
28+
tlsMode: TlsMode.DISABLED,
29+
httpLoggerFile: '',
30+
allowSelfSigned: true,
31+
recoveryMode: false,
32+
};
33+
34+
beforeEach(() => {
35+
nock.disableNetConnect();
36+
nock.enableNetConnect('127.0.0.1');
37+
38+
// Create mock BitGo instance with base functionality
39+
mockBitgo = new BitGoAPI({ env: 'test' });
40+
41+
// Setup middleware stubs before creating app
42+
sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => {
43+
(req as BitGoRequest<MasterExpressConfig>).bitgo = mockBitgo;
44+
(req as BitGoRequest<MasterExpressConfig>).config = config;
45+
next();
46+
});
47+
48+
// Create app after middleware is stubbed
49+
const app = expressApp(config);
50+
agent = request.agent(app);
51+
});
52+
53+
afterEach(() => {
54+
nock.cleanAll();
55+
sinon.restore();
56+
});
57+
58+
describe('Recovery', () => {
59+
const coin = 'tbtc';
60+
61+
beforeEach(() => {
62+
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
63+
(req as BitGoRequest<MasterExpressConfig>).params = { coin };
64+
(req as BitGoRequest<MasterExpressConfig>).enclavedExpressClient =
65+
new EnclavedExpressClient(config, coin);
66+
next();
67+
return undefined;
68+
});
69+
});
70+
71+
it('should fail to run mbe recovery if not in recovery mode', async () => {
72+
const coin = 'tbtc';
73+
const userPub = 'xpub_user';
74+
const backupPub = 'xpub_backup';
75+
const bitgoPub = 'xpub_bitgo';
76+
const recoveryDestination = 'tb1qprdy6jwxrrr2qrwgd2tzl8z99hqp29jn6f3sguxulqm448myj6jsy2nwsu';
77+
const response = await agent
78+
.post(`/api/${coin}/wallet/recovery`)
79+
.set('Authorization', `Bearer ${accessToken}`)
80+
.send({
81+
multiSigRecoveryParams: {
82+
userPub,
83+
backupPub,
84+
bitgoPub,
85+
walletContractAddress: '',
86+
},
87+
recoveryDestinationAddress: recoveryDestination,
88+
coin,
89+
apiKey: 'key',
90+
coinSpecificParams: {
91+
evmRecoveryOptions: {
92+
gasPrice: 20000000000,
93+
gasLimit: 500000,
94+
},
95+
},
96+
});
97+
response.status.should.equal(500);
98+
response.body.should.have.property('error');
99+
response.body.should.have.property('details');
100+
response.body.details.should.containEql(
101+
'Recovery operations are not enabled. The server must be in recovery mode to perform this action.',
102+
);
103+
});
104+
});
105+
106+
describe('Recovery Consolidation', () => {
107+
it('should fail to run mbe recovery consolidation if not in recovery mode', async () => {
108+
const response = await agent
109+
.post(`/api/trx/wallet/recoveryconsolidations`)
110+
.set('Authorization', `Bearer ${accessToken}`)
111+
.send({
112+
multisigType: 'onchain',
113+
userPub: 'user-xpub',
114+
backupPub: 'backup-xpub',
115+
bitgoPub: 'bitgo-xpub',
116+
tokenContractAddress: 'tron-token',
117+
startingScanIndex: 1,
118+
endingScanIndex: 3,
119+
});
120+
121+
response.status.should.equal(500);
122+
});
123+
});
124+
});

src/__tests__/api/master/recoveryConsolidationsWallet.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe('POST /api/:coin/wallet/recoveryconsolidations', () => {
3838
enclavedExpressCert: 'test-cert',
3939
tlsMode: TlsMode.DISABLED,
4040
allowSelfSigned: true,
41+
recoveryMode: true,
4142
};
4243
const app = expressApp(config);
4344
agent = request.agent(app);

src/__tests__/api/master/recoveryWallet.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('Recovery Tests', () => {
3030
enclavedExpressCert: 'dummy-cert',
3131
tlsMode: TlsMode.DISABLED,
3232
allowSelfSigned: true,
33+
recoveryMode: true,
3334
};
3435

3536
beforeEach(() => {

src/__tests__/config.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ describe('Configuration', () => {
9090
}
9191
});
9292

93+
it('should read the recovery mode from the env', () => {
94+
process.env.KMS_URL = 'http://localhost:3000';
95+
process.env.TLS_KEY = mockTlsKey;
96+
process.env.TLS_CERT = mockTlsCert;
97+
process.env.RECOVERY_MODE = 'true';
98+
const cfg = initConfig();
99+
cfg.recoveryMode!.should.be.true();
100+
});
101+
93102
it('should read TLS mode from environment variables', () => {
94103
process.env.KMS_URL = 'http://localhost:3000';
95104
process.env.TLS_KEY = mockTlsKey;

src/api/master/handlerUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BitGoAPI } from '@bitgo-beta/sdk-api';
22
import { CustomSigningFunction, RequestTracer } from '@bitgo-beta/sdk-core';
33
import { EnclavedExpressClient } from './clients/enclavedExpressClient';
44
import coinFactory from '../../shared/coinFactory';
5+
import { MasterExpressConfig } from '../../shared/types';
56

67
/**
78
* Fetch wallet and signing keychain, with validation for source and pubkey.
@@ -73,3 +74,11 @@ export function makeCustomSigningFunction({
7374
});
7475
};
7576
}
77+
78+
export function checkRecoveryMode(config: MasterExpressConfig) {
79+
if (!config.recoveryMode) {
80+
throw new Error(
81+
'Recovery operations are not enabled. The server must be in recovery mode to perform this action.',
82+
);
83+
}
84+
}

src/api/master/handlers/recoveryConsolidationsWallet.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import type { Ada, Tada } from '@bitgo-beta/sdk-coin-ada';
2020
import type { Dot, Tdot } from '@bitgo-beta/sdk-coin-dot';
2121
import type { Tao, Ttao } from '@bitgo-beta/sdk-coin-tao';
2222
import coinFactory from '../../../shared/coinFactory';
23+
import { checkRecoveryMode } from '../handlerUtils';
24+
import { MasterExpressConfig } from '../../../shared/types';
2325

2426
type RecoveryConsolidationParams =
2527
| ConsolidationRecoveryOptions
@@ -77,6 +79,8 @@ export async function recoveryConsolidateWallets(
7779
export async function handleRecoveryConsolidationsOnPrem(
7880
req: MasterApiSpecRouteRequest<'v1.wallet.recoveryConsolidations', 'post'>,
7981
) {
82+
checkRecoveryMode(req.config as MasterExpressConfig);
83+
8084
const bitgo = req.bitgo;
8185
const coin = req.decoded.coin;
8286
const enclavedExpressClient = req.enclavedExpressClient;

src/api/master/handlers/recoveryWallet.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ import {
2828
UtxoRecoveryOptions,
2929
} from '../routers/masterApiSpec';
3030
import { recoverEddsaWallets } from './recoverEddsaWallets';
31-
import { EnvironmentName } from '../../../shared/types';
31+
import { EnvironmentName, MasterExpressConfig } from '../../../shared/types';
3232
import logger from '../../../logger';
3333
import { CoinFamily } from '@bitgo-beta/statics';
3434
import { ValidationError } from '../../../shared/errors';
35+
import { checkRecoveryMode } from '../handlerUtils';
3536

3637
interface RecoveryParams {
3738
userKey: string;
@@ -186,6 +187,8 @@ async function handleUtxoLikeRecovery(
186187
export async function handleRecoveryWalletOnPrem(
187188
req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>,
188189
) {
190+
checkRecoveryMode(req.config as MasterExpressConfig);
191+
189192
const bitgo = req.bitgo;
190193
const coin = req.decoded.coin;
191194
const enclavedExpressClient = req.enclavedExpressClient;

src/initConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ function enclavedEnvConfig(): Partial<EnclavedConfig> {
101101
tlsMode: determineTlsMode(),
102102
mtlsAllowedClientFingerprints: readEnvVar('MTLS_ALLOWED_CLIENT_FINGERPRINTS')?.split(','),
103103
allowSelfSigned: readEnvVar('ALLOW_SELF_SIGNED') === 'true',
104+
recoveryMode: readEnvVar('RECOVERY_MODE') === 'true',
104105
};
105106
}
106107

@@ -130,6 +131,7 @@ function mergeEnclavedConfigs(...configs: Partial<EnclavedConfig>[]): EnclavedCo
130131
tlsMode: get('tlsMode'),
131132
mtlsAllowedClientFingerprints: get('mtlsAllowedClientFingerprints'),
132133
allowSelfSigned: get('allowSelfSigned'),
134+
recoveryMode: get('recoveryMode'),
133135
};
134136
}
135137

@@ -241,6 +243,7 @@ function masterExpressEnvConfig(): Partial<MasterExpressConfig> {
241243
tlsMode,
242244
mtlsAllowedClientFingerprints: readEnvVar('MTLS_ALLOWED_CLIENT_FINGERPRINTS')?.split(','),
243245
allowSelfSigned,
246+
recoveryMode: readEnvVar('RECOVERY_MODE') === 'true',
244247
};
245248
}
246249

@@ -278,6 +281,7 @@ function mergeMasterExpressConfigs(
278281
tlsMode: get('tlsMode'),
279282
mtlsAllowedClientFingerprints: get('mtlsAllowedClientFingerprints'),
280283
allowSelfSigned: get('allowSelfSigned'),
284+
recoveryMode: get('recoveryMode'),
281285
};
282286
}
283287

src/shared/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface BaseConfig {
2020
keepAliveTimeout?: number;
2121
headersTimeout?: number;
2222
httpLoggerFile: string;
23+
recoveryMode?: boolean;
2324
}
2425

2526
// Enclaved mode specific configuration

0 commit comments

Comments
 (0)