Skip to content

Commit 3fc3f3b

Browse files
feat(mbe): hide recovery apis under non recovery mode for mbe
1 parent cb12fae commit 3fc3f3b

File tree

10 files changed

+185
-1
lines changed

10 files changed

+185
-1
lines changed

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: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
logFile: '',
24+
env: 'test',
25+
disableEnvCheck: true,
26+
authVersion: 2,
27+
enclavedExpressUrl: enclavedExpressUrl,
28+
enclavedExpressCert: 'dummy-cert',
29+
tlsMode: TlsMode.DISABLED,
30+
mtlsRequestCert: false,
31+
allowSelfSigned: true,
32+
recoveryMode: false,
33+
};
34+
35+
beforeEach(() => {
36+
nock.disableNetConnect();
37+
nock.enableNetConnect('127.0.0.1');
38+
39+
// Create mock BitGo instance with base functionality
40+
mockBitgo = {
41+
coin: sinon.stub(),
42+
_coinFactory: {},
43+
_useAms: false,
44+
initCoinFactory: sinon.stub(),
45+
registerToken: sinon.stub(),
46+
getValidate: sinon.stub(),
47+
validateAddress: sinon.stub(),
48+
verifyAddress: sinon.stub(),
49+
verifyPassword: sinon.stub(),
50+
encrypt: sinon.stub(),
51+
decrypt: sinon.stub(),
52+
lock: sinon.stub(),
53+
unlock: sinon.stub(),
54+
getSharingKey: sinon.stub(),
55+
ping: sinon.stub(),
56+
authenticate: sinon.stub(),
57+
authenticateWithAccessToken: sinon.stub(),
58+
logout: sinon.stub(),
59+
me: sinon.stub(),
60+
session: sinon.stub(),
61+
getUser: sinon.stub(),
62+
users: sinon.stub(),
63+
getWallet: sinon.stub(),
64+
getWallets: sinon.stub(),
65+
addWallet: sinon.stub(),
66+
removeWallet: sinon.stub(),
67+
getAsUser: sinon.stub(),
68+
register: sinon.stub(),
69+
} as unknown as BitGoAPI;
70+
71+
// Setup middleware stubs before creating app
72+
sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => {
73+
(req as BitGoRequest<MasterExpressConfig>).bitgo = mockBitgo;
74+
(req as BitGoRequest<MasterExpressConfig>).config = config;
75+
next();
76+
});
77+
78+
// Create app after middleware is stubbed
79+
const app = expressApp(config);
80+
agent = request.agent(app);
81+
});
82+
83+
afterEach(() => {
84+
nock.cleanAll();
85+
sinon.restore();
86+
});
87+
88+
describe('Recovery', () => {
89+
const coin = 'tbtc';
90+
91+
beforeEach(() => {
92+
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
93+
(req as BitGoRequest<MasterExpressConfig>).params = { coin };
94+
(req as BitGoRequest<MasterExpressConfig>).enclavedExpressClient =
95+
new EnclavedExpressClient(config, coin);
96+
next();
97+
return undefined;
98+
});
99+
});
100+
101+
it('should fail to run recovery if not in recovery mode', async () => {
102+
const coin = 'tbtc';
103+
const userPub = 'xpub_user';
104+
const backupPub = 'xpub_backup';
105+
const bitgoPub = 'xpub_bitgo';
106+
const recoveryDestination = 'tb1qprdy6jwxrrr2qrwgd2tzl8z99hqp29jn6f3sguxulqm448myj6jsy2nwsu';
107+
const response = await agent
108+
.post(`/api/${coin}/wallet/recovery`)
109+
.set('Authorization', `Bearer ${accessToken}`)
110+
.send({
111+
multiSigRecoveryParams: {
112+
userPub,
113+
backupPub,
114+
bitgoPub,
115+
walletContractAddress: '',
116+
},
117+
recoveryDestinationAddress: recoveryDestination,
118+
coin,
119+
apiKey: 'key',
120+
coinSpecificParams: {
121+
evmRecoveryOptions: {
122+
gasPrice: 20000000000,
123+
gasLimit: 500000,
124+
},
125+
},
126+
});
127+
response.status.should.equal(500);
128+
response.body.should.have.property('error');
129+
response.body.should.have.property('details');
130+
response.body.details.should.containEql(
131+
'Recovery operations are not enabled. The server must be in recovery mode to perform this action.',
132+
);
133+
});
134+
});
135+
136+
describe('Recovery Consolidation', () => {
137+
it('should fail to run recovery consolidation if not in recovery mode', async () => {
138+
const response = await agent
139+
.post(`/api/trx/wallet/recoveryconsolidations`)
140+
.set('Authorization', `Bearer ${accessToken}`)
141+
.send({
142+
multisigType: 'onchain',
143+
userPub: 'user-xpub',
144+
backupPub: 'backup-xpub',
145+
bitgoPub: 'bitgo-xpub',
146+
tokenContractAddress: 'tron-token',
147+
startingScanIndex: 1,
148+
endingScanIndex: 3,
149+
});
150+
151+
response.status.should.equal(500);
152+
});
153+
});
154+
});

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ describe('Configuration', () => {
7575
}
7676
});
7777

78+
it('should read the recovery mode from the env', () => {
79+
process.env.RECOVERY_MODE = 'true';
80+
const cfg = initConfig();
81+
cfg.recoveryMode!.should.be.true();
82+
});
83+
7884
it('should read port from environment variable', () => {
7985
process.env.ENCLAVED_EXPRESS_PORT = '4000';
8086
process.env.KMS_URL = 'http://localhost:3000';

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)