diff --git a/src/__tests__/api/master/musigRecovery.test.ts b/src/__tests__/api/master/musigRecovery.test.ts index c3f90c95..6856f13f 100644 --- a/src/__tests__/api/master/musigRecovery.test.ts +++ b/src/__tests__/api/master/musigRecovery.test.ts @@ -31,6 +31,7 @@ describe('POST /api/:coin/wallet/recovery', () => { enclavedExpressCert: 'dummy-cert', tlsMode: TlsMode.DISABLED, allowSelfSigned: true, + recoveryMode: true, }; const app = expressApp(config); diff --git a/src/__tests__/api/master/nonRecovery.test.ts b/src/__tests__/api/master/nonRecovery.test.ts new file mode 100644 index 00000000..6567d960 --- /dev/null +++ b/src/__tests__/api/master/nonRecovery.test.ts @@ -0,0 +1,124 @@ +import 'should'; +import * as request from 'supertest'; +import nock from 'nock'; +import { app as expressApp } from '../../../masterExpressApp'; +import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; +import sinon from 'sinon'; +import * as middleware from '../../../shared/middleware'; +import * as masterMiddleware from '../../../api/master/middleware/middleware'; +import { BitGoRequest } from '../../../types/request'; +import { BitGoAPI } from '@bitgo-beta/sdk-api'; +import { EnclavedExpressClient } from '../../../api/master/clients/enclavedExpressClient'; + +describe('Non Recovery Tests', () => { + let agent: request.SuperAgentTest; + let mockBitgo: BitGoAPI; + const enclavedExpressUrl = 'http://enclaved.invalid'; + const accessToken = 'test-token'; + const config: MasterExpressConfig = { + appMode: AppMode.MASTER_EXPRESS, + port: 0, + bind: 'localhost', + timeout: 60000, + env: 'test', + disableEnvCheck: true, + authVersion: 2, + enclavedExpressUrl: enclavedExpressUrl, + enclavedExpressCert: 'dummy-cert', + tlsMode: TlsMode.DISABLED, + httpLoggerFile: '', + allowSelfSigned: true, + recoveryMode: false, + }; + + beforeEach(() => { + nock.disableNetConnect(); + nock.enableNetConnect('127.0.0.1'); + + // Create mock BitGo instance with base functionality + mockBitgo = new BitGoAPI({ env: 'test' }); + + // Setup middleware stubs before creating app + sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { + (req as BitGoRequest).bitgo = mockBitgo; + (req as BitGoRequest).config = config; + next(); + }); + + // Create app after middleware is stubbed + const app = expressApp(config); + agent = request.agent(app); + }); + + afterEach(() => { + nock.cleanAll(); + sinon.restore(); + }); + + describe('Recovery', () => { + const coin = 'tbtc'; + + beforeEach(() => { + sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => { + (req as BitGoRequest).params = { coin }; + (req as BitGoRequest).enclavedExpressClient = + new EnclavedExpressClient(config, coin); + next(); + return undefined; + }); + }); + + it('should fail to run mbe recovery if not in recovery mode', async () => { + const coin = 'tbtc'; + const userPub = 'xpub_user'; + const backupPub = 'xpub_backup'; + const bitgoPub = 'xpub_bitgo'; + const recoveryDestination = 'tb1qprdy6jwxrrr2qrwgd2tzl8z99hqp29jn6f3sguxulqm448myj6jsy2nwsu'; + const response = await agent + .post(`/api/${coin}/wallet/recovery`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + multiSigRecoveryParams: { + userPub, + backupPub, + bitgoPub, + walletContractAddress: '', + }, + recoveryDestinationAddress: recoveryDestination, + coin, + apiKey: 'key', + coinSpecificParams: { + evmRecoveryOptions: { + gasPrice: 20000000000, + gasLimit: 500000, + }, + }, + }); + response.status.should.equal(500); + response.body.should.have.property('error'); + response.body.should.have.property('details'); + response.body.details.should.containEql( + 'Recovery operations are not enabled. The server must be in recovery mode to perform this action.', + ); + }); + }); + + describe('Recovery Consolidation', () => { + it('should fail to run mbe recovery consolidation if not in recovery mode', async () => { + const response = await agent + .post(`/api/trx/wallet/recoveryconsolidations`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + multisigType: 'onchain', + userPub: 'user-xpub', + backupPub: 'backup-xpub', + bitgoPub: 'bitgo-xpub', + tokenContractAddress: 'tron-token', + startingScanIndex: 1, + endingScanIndex: 3, + }); + + response.status.should.equal(500); + }); + }); +}); diff --git a/src/__tests__/api/master/recoveryConsolidationsWallet.test.ts b/src/__tests__/api/master/recoveryConsolidationsWallet.test.ts index 0bbccab3..f5e9d24e 100644 --- a/src/__tests__/api/master/recoveryConsolidationsWallet.test.ts +++ b/src/__tests__/api/master/recoveryConsolidationsWallet.test.ts @@ -38,6 +38,7 @@ describe('POST /api/:coin/wallet/recoveryconsolidations', () => { enclavedExpressCert: 'test-cert', tlsMode: TlsMode.DISABLED, allowSelfSigned: true, + recoveryMode: true, }; const app = expressApp(config); agent = request.agent(app); diff --git a/src/__tests__/api/master/recoveryWallet.test.ts b/src/__tests__/api/master/recoveryWallet.test.ts index 2a2d5368..5fb3f6be 100644 --- a/src/__tests__/api/master/recoveryWallet.test.ts +++ b/src/__tests__/api/master/recoveryWallet.test.ts @@ -30,6 +30,7 @@ describe('Recovery Tests', () => { enclavedExpressCert: 'dummy-cert', tlsMode: TlsMode.DISABLED, allowSelfSigned: true, + recoveryMode: true, }; beforeEach(() => { diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index d058791c..4bcfd2a7 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -90,6 +90,15 @@ describe('Configuration', () => { } }); + it('should read the recovery mode from the env', () => { + process.env.KMS_URL = 'http://localhost:3000'; + process.env.TLS_KEY = mockTlsKey; + process.env.TLS_CERT = mockTlsCert; + process.env.RECOVERY_MODE = 'true'; + const cfg = initConfig(); + cfg.recoveryMode!.should.be.true(); + }); + it('should read TLS mode from environment variables', () => { process.env.KMS_URL = 'http://localhost:3000'; process.env.TLS_KEY = mockTlsKey; diff --git a/src/api/master/handlerUtils.ts b/src/api/master/handlerUtils.ts index 69c64a17..77a4fe89 100644 --- a/src/api/master/handlerUtils.ts +++ b/src/api/master/handlerUtils.ts @@ -2,6 +2,7 @@ import { BitGoAPI } from '@bitgo-beta/sdk-api'; import { CustomSigningFunction, RequestTracer } from '@bitgo-beta/sdk-core'; import { EnclavedExpressClient } from './clients/enclavedExpressClient'; import coinFactory from '../../shared/coinFactory'; +import { MasterExpressConfig } from '../../shared/types'; /** * Fetch wallet and signing keychain, with validation for source and pubkey. @@ -73,3 +74,11 @@ export function makeCustomSigningFunction({ }); }; } + +export function checkRecoveryMode(config: MasterExpressConfig) { + if (!config.recoveryMode) { + throw new Error( + 'Recovery operations are not enabled. The server must be in recovery mode to perform this action.', + ); + } +} diff --git a/src/api/master/handlers/recoveryConsolidationsWallet.ts b/src/api/master/handlers/recoveryConsolidationsWallet.ts index 9f472979..69153917 100644 --- a/src/api/master/handlers/recoveryConsolidationsWallet.ts +++ b/src/api/master/handlers/recoveryConsolidationsWallet.ts @@ -20,6 +20,8 @@ import type { Ada, Tada } from '@bitgo-beta/sdk-coin-ada'; import type { Dot, Tdot } from '@bitgo-beta/sdk-coin-dot'; import type { Tao, Ttao } from '@bitgo-beta/sdk-coin-tao'; import coinFactory from '../../../shared/coinFactory'; +import { checkRecoveryMode } from '../handlerUtils'; +import { MasterExpressConfig } from '../../../shared/types'; type RecoveryConsolidationParams = | ConsolidationRecoveryOptions @@ -77,6 +79,8 @@ export async function recoveryConsolidateWallets( export async function handleRecoveryConsolidationsOnPrem( req: MasterApiSpecRouteRequest<'v1.wallet.recoveryConsolidations', 'post'>, ) { + checkRecoveryMode(req.config as MasterExpressConfig); + const bitgo = req.bitgo; const coin = req.decoded.coin; const enclavedExpressClient = req.enclavedExpressClient; diff --git a/src/api/master/handlers/recoveryWallet.ts b/src/api/master/handlers/recoveryWallet.ts index ed9d9e87..c18fc4c6 100644 --- a/src/api/master/handlers/recoveryWallet.ts +++ b/src/api/master/handlers/recoveryWallet.ts @@ -28,10 +28,11 @@ import { UtxoRecoveryOptions, } from '../routers/masterApiSpec'; import { recoverEddsaWallets } from './recoverEddsaWallets'; -import { EnvironmentName } from '../../../shared/types'; +import { EnvironmentName, MasterExpressConfig } from '../../../shared/types'; import logger from '../../../logger'; import { CoinFamily } from '@bitgo-beta/statics'; import { ValidationError } from '../../../shared/errors'; +import { checkRecoveryMode } from '../handlerUtils'; interface RecoveryParams { userKey: string; @@ -186,6 +187,8 @@ async function handleUtxoLikeRecovery( export async function handleRecoveryWalletOnPrem( req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>, ) { + checkRecoveryMode(req.config as MasterExpressConfig); + const bitgo = req.bitgo; const coin = req.decoded.coin; const enclavedExpressClient = req.enclavedExpressClient; diff --git a/src/initConfig.ts b/src/initConfig.ts index cb24c18e..4329911d 100644 --- a/src/initConfig.ts +++ b/src/initConfig.ts @@ -101,6 +101,7 @@ function enclavedEnvConfig(): Partial { tlsMode: determineTlsMode(), mtlsAllowedClientFingerprints: readEnvVar('MTLS_ALLOWED_CLIENT_FINGERPRINTS')?.split(','), allowSelfSigned: readEnvVar('ALLOW_SELF_SIGNED') === 'true', + recoveryMode: readEnvVar('RECOVERY_MODE') === 'true', }; } @@ -130,6 +131,7 @@ function mergeEnclavedConfigs(...configs: Partial[]): EnclavedCo tlsMode: get('tlsMode'), mtlsAllowedClientFingerprints: get('mtlsAllowedClientFingerprints'), allowSelfSigned: get('allowSelfSigned'), + recoveryMode: get('recoveryMode'), }; } @@ -241,6 +243,7 @@ function masterExpressEnvConfig(): Partial { tlsMode, mtlsAllowedClientFingerprints: readEnvVar('MTLS_ALLOWED_CLIENT_FINGERPRINTS')?.split(','), allowSelfSigned, + recoveryMode: readEnvVar('RECOVERY_MODE') === 'true', }; } @@ -278,6 +281,7 @@ function mergeMasterExpressConfigs( tlsMode: get('tlsMode'), mtlsAllowedClientFingerprints: get('mtlsAllowedClientFingerprints'), allowSelfSigned: get('allowSelfSigned'), + recoveryMode: get('recoveryMode'), }; } diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 05f7c65a..99471702 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -20,6 +20,7 @@ export interface BaseConfig { keepAliveTimeout?: number; headersTimeout?: number; httpLoggerFile: string; + recoveryMode?: boolean; } // Enclaved mode specific configuration