diff --git a/modules/abstract-utxo/test/unit/webauthn.ts b/modules/abstract-utxo/test/unit/webauthn.ts index 3dd4f8e50f..5a38247357 100644 --- a/modules/abstract-utxo/test/unit/webauthn.ts +++ b/modules/abstract-utxo/test/unit/webauthn.ts @@ -45,7 +45,7 @@ describe('webauthn passphrase decryption', function () { it('should throw when all passphrases are wrong', async function () { await assert.rejects(() => wallet.getUserPrv({ keychain, walletPassphrase: 'wrong' }), { - message: 'failed to decrypt user keychain', + message: 'unable to decrypt keychain with the given wallet passphrase', }); }); }); diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index dd5fdc99a2..598c0f6ee7 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -27,6 +27,7 @@ import { encryptRsaWithAesGcm, GetNetworkPartnersResponse, GShare, + IncorrectPasswordError, MPCType, multisigTypes, ShareType, @@ -433,7 +434,11 @@ async function decryptPrivKey(bg: BitGo, encryptedPrivKey: string, walletPw: str try { return await bg.decrypt({ password: walletPw, input: encryptedPrivKey }); } catch (e) { - throw new Error(`Error when trying to decrypt private key: ${e}`); + const detail = e instanceof Error ? e.message : String(e); + if (/not valid JSON|unknown envelope version|salt must be|iv must be/i.test(detail)) { + throw new Error(`Error when trying to decrypt private key: ${detail}`); + } + throw new IncorrectPasswordError(); } } @@ -1702,6 +1707,7 @@ function handleRequestHandlerError(res: express.Response, error: unknown) { name: err.name || 'BitGoExpressError', bitgoJsVersion: version, bitgoExpressVersion: pjson.version, + ...(err.code ? { code: err.code } : {}), }); const status = err.status || 500; if (!(status >= 200 && status < 300)) { diff --git a/modules/express/src/retryPromise.ts b/modules/express/src/retryPromise.ts index 9e8cad6285..9d3861d80a 100644 --- a/modules/express/src/retryPromise.ts +++ b/modules/express/src/retryPromise.ts @@ -33,7 +33,7 @@ export async function retryPromise( if (err.code === 'ECONNREFUSED') { onError(err, tryCount); } else { - throw new Error(err); + throw err; } } diff --git a/modules/express/test/unit/clientRoutes/signPayload.ts b/modules/express/test/unit/clientRoutes/signPayload.ts index 009694d5bd..92808081e7 100644 --- a/modules/express/test/unit/clientRoutes/signPayload.ts +++ b/modules/express/test/unit/clientRoutes/signPayload.ts @@ -358,7 +358,7 @@ describe('With the handler to sign an arbitrary payload in external signing mode } as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>; await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith( - 'Error when trying to decrypt private key: Error: decrypt: ciphertext is not valid JSON' + 'Error when trying to decrypt private key: decrypt: ciphertext is not valid JSON' ); readFileStub.restore(); @@ -387,7 +387,7 @@ describe('With the handler to sign an arbitrary payload in external signing mode } as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>; await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith( - 'Error when trying to decrypt private key: Error: incorrect password' + 'unable to decrypt keychain with the given wallet passphrase' ); readFileStub.restore(); @@ -415,7 +415,7 @@ describe('With the handler to sign an arbitrary payload in external signing mode } as unknown as ExpressApiRouteRequest<'express.v2.ofc.extSignPayload', 'post'>; await handleV2OFCSignPayloadInExtSigningMode(req).should.be.rejectedWith( - 'Error when trying to decrypt private key: Error: incorrect password' + 'unable to decrypt keychain with the given wallet passphrase' ); readFileStub.restore(); diff --git a/modules/express/test/unit/retryPromise.ts b/modules/express/test/unit/retryPromise.ts new file mode 100644 index 0000000000..317fb3b688 --- /dev/null +++ b/modules/express/test/unit/retryPromise.ts @@ -0,0 +1,22 @@ +/** + * @prettier + */ +import * as assert from 'assert'; +import { retryPromise } from '../../src/retryPromise'; + +describe('retryPromise', function () { + it('rethrows the original non-ECONNREFUSED error without wrapping', async function () { + const original = Object.assign(new Error('Internal Server Error'), { status: 500 }); + await assert.rejects( + () => + retryPromise( + async () => { + throw original; + }, + () => undefined, + { retryLimit: 1 } + ), + (err: Error) => err === original + ); + }); +}); diff --git a/modules/express/test/unit/typedRoutes/coinSign.ts b/modules/express/test/unit/typedRoutes/coinSign.ts index 3ab2dd9e56..ed87e12adc 100644 --- a/modules/express/test/unit/typedRoutes/coinSign.ts +++ b/modules/express/test/unit/typedRoutes/coinSign.ts @@ -581,8 +581,8 @@ describe('CoinSign codec tests (External Signer Mode)', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Verify error response - runtime errors return 500 - assert.strictEqual(result.status, 500); + // Verify error response - wrong passphrase returns 401 + assert.strictEqual(result.status, 401); assert.ok(result.body); }); diff --git a/modules/express/test/unit/typedRoutes/generateShareTSS.ts b/modules/express/test/unit/typedRoutes/generateShareTSS.ts index b8b1f464e3..2fcf73c278 100644 --- a/modules/express/test/unit/typedRoutes/generateShareTSS.ts +++ b/modules/express/test/unit/typedRoutes/generateShareTSS.ts @@ -935,8 +935,8 @@ describe('GenerateShareTSS codec tests (External Signer Mode)', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Verify error response - runtime errors return 500 - assert.strictEqual(result.status, 500); + // Verify error response - wrong passphrase returns 401 + assert.strictEqual(result.status, 401); assert.ok(result.body); }); diff --git a/modules/express/test/unit/typedRoutes/ofcExtSignPayload.ts b/modules/express/test/unit/typedRoutes/ofcExtSignPayload.ts index f59570bfc1..1538d52968 100644 --- a/modules/express/test/unit/typedRoutes/ofcExtSignPayload.ts +++ b/modules/express/test/unit/typedRoutes/ofcExtSignPayload.ts @@ -266,7 +266,7 @@ describe('OfcExtSignPayload External Signer Mode Tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - assert.strictEqual(result.status, 500); + assert.strictEqual(result.status, 401); result.body.should.have.property('error'); }); diff --git a/modules/sdk-core/src/bitgo/errors.ts b/modules/sdk-core/src/bitgo/errors.ts index b6d92b71ad..caf6e2f402 100644 --- a/modules/sdk-core/src/bitgo/errors.ts +++ b/modules/sdk-core/src/bitgo/errors.ts @@ -168,8 +168,12 @@ export class MissingEncryptedKeychainError extends Error { } export class IncorrectPasswordError extends Error { + public code = 'wallet_passphrase_incorrect'; + public status = 401; + public constructor(message?: string) { - super(message || 'Incorrect password'); + super(message || 'unable to decrypt keychain with the given wallet passphrase'); + this.name = 'IncorrectPasswordError'; } } diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 91a2ba63da..1d7480d06b 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -2562,7 +2562,7 @@ export class Wallet implements IWallet { } userPrv = await decryptKeychainPrivateKey(this.bitgo, userKeychain, params.walletPassphrase); if (!userPrv) { - throw new Error('failed to decrypt user keychain'); + throw new IncorrectPasswordError(); } } return userPrv; @@ -4812,18 +4812,13 @@ export class Wallet implements IWallet { const reqId = params.reqId || undefined; await this.tssUtils.deleteSignatureShares(txRequestId, reqId); - try { - const signedTxRequest = await this.tssUtils.signEddsaTssUsingExternalSigner( - txRequestId, - params.customCommitmentGeneratingFunction, - params.customRShareGeneratingFunction, - params.customGShareGeneratingFunction, - reqId - ); - return signedTxRequest; - } catch (e) { - throw new Error('failed to sign transaction ' + e); - } + return this.tssUtils.signEddsaTssUsingExternalSigner( + txRequestId, + params.customCommitmentGeneratingFunction, + params.customRShareGeneratingFunction, + params.customGShareGeneratingFunction, + reqId + ); } /** @@ -4919,23 +4914,18 @@ export class Wallet implements IWallet { throw new Error('Generator function for S share required to sign transactions with External Signer.'); } - try { - assert(this.tssUtils, 'tssUtils must be defined'); - const signedTxRequest = await this.tssUtils.signEcdsaTssUsingExternalSigner( - { - txRequest: txRequestId, - reqId: params.reqId || new RequestTracer(), - }, - RequestType.tx, - params.customPaillierModulusGeneratingFunction, - params.customKShareGeneratingFunction, - params.customMuDeltaShareGeneratingFunction, - params.customSShareGeneratingFunction - ); - return signedTxRequest; - } catch (e) { - throw new Error('failed to sign transaction ' + e); - } + assert(this.tssUtils, 'tssUtils must be defined'); + return this.tssUtils.signEcdsaTssUsingExternalSigner( + { + txRequest: txRequestId, + reqId: params.reqId || new RequestTracer(), + }, + RequestType.tx, + params.customPaillierModulusGeneratingFunction, + params.customKShareGeneratingFunction, + params.customMuDeltaShareGeneratingFunction, + params.customSShareGeneratingFunction + ); } /** @@ -4974,21 +4964,16 @@ export class Wallet implements IWallet { ); } - try { - assert(this.tssUtils, 'tssUtils must be defined'); - const signedTxRequest = await this.tssUtils.signEddsaMPCv2TssUsingExternalSigner( - { - txRequest: txRequestId, - reqId: params.reqId || new RequestTracer(), - }, - params.customEddsaMPCv2SigningRound1GenerationFunction, - params.customEddsaMPCv2SigningRound2GenerationFunction, - params.customEddsaMPCv2SigningRound3GenerationFunction - ); - return signedTxRequest; - } catch (e) { - throw new Error('failed to sign transaction ' + e); - } + assert(this.tssUtils, 'tssUtils must be defined'); + return this.tssUtils.signEddsaMPCv2TssUsingExternalSigner( + { + txRequest: txRequestId, + reqId: params.reqId || new RequestTracer(), + }, + params.customEddsaMPCv2SigningRound1GenerationFunction, + params.customEddsaMPCv2SigningRound2GenerationFunction, + params.customEddsaMPCv2SigningRound3GenerationFunction + ); } /** @@ -5021,21 +5006,16 @@ export class Wallet implements IWallet { throw new Error('Generator function for MPCv2 Round 3 share required to sign transactions with External Signer.'); } - try { - assert(this.tssUtils, 'tssUtils must be defined'); - const signedTxRequest = await this.tssUtils.signEcdsaMPCv2TssUsingExternalSigner( - { - txRequest: txRequestId, - reqId: params.reqId || new RequestTracer(), - }, - params.customMPCv2SigningRound1GenerationFunction, - params.customMPCv2SigningRound2GenerationFunction, - params.customMPCv2SigningRound3GenerationFunction - ); - return signedTxRequest; - } catch (e) { - throw new Error('failed to sign transaction ' + e); - } + assert(this.tssUtils, 'tssUtils must be defined'); + return this.tssUtils.signEcdsaMPCv2TssUsingExternalSigner( + { + txRequest: txRequestId, + reqId: params.reqId || new RequestTracer(), + }, + params.customMPCv2SigningRound1GenerationFunction, + params.customMPCv2SigningRound2GenerationFunction, + params.customMPCv2SigningRound3GenerationFunction + ); } /** @@ -5056,33 +5036,29 @@ export class Wallet implements IWallet { throw new Error('prv required to sign transactions with TSS'); } - try { - let txRequest: string | TxRequest = params.txPrebuild.txRequestId; - let txParams: TransactionParams | undefined = params.txPrebuild.buildParams; - - // EdDSA MPCv2 re-sign path: buildParams is absent when the UI calls signAndSendTxRequest with - // only txRequestId. Derive txParams from the persisted intent so verifyTransaction receives - // the correct recipients before DSG starts. Other TSS variants are unaffected by the guard. - if (!txParams && this.multisigTypeVersion() === 'MPCv2' && this.baseCoin.getMPCAlgorithm() === 'eddsa') { - txRequest = await getTxRequest( - this.bitgo, - this.id(), - params.txPrebuild.txRequestId, - params.reqId || new RequestTracer() - ); - txParams = txParamsFromIntent(txRequest.intent, this.baseCoin.getChain()); - } + let txRequest: string | TxRequest = params.txPrebuild.txRequestId; + let txParams: TransactionParams | undefined = params.txPrebuild.buildParams; - return await this.tssUtils!.signTxRequest({ - txRequest, - txParams, - prv: params.prv, - reqId: params.reqId || new RequestTracer(), - apiVersion: params.apiVersion, - }); - } catch (e) { - throw new Error('failed to sign transaction ' + e); + // EdDSA MPCv2 re-sign path: buildParams is absent when the UI calls signAndSendTxRequest with + // only txRequestId. Derive txParams from the persisted intent so verifyTransaction receives + // the correct recipients before DSG starts. Other TSS variants are unaffected by the guard. + if (!txParams && this.multisigTypeVersion() === 'MPCv2' && this.baseCoin.getMPCAlgorithm() === 'eddsa') { + txRequest = await getTxRequest( + this.bitgo, + this.id(), + params.txPrebuild.txRequestId, + params.reqId || new RequestTracer() + ); + txParams = txParamsFromIntent(txRequest.intent, this.baseCoin.getChain()); } + + return this.tssUtils!.signTxRequest({ + txRequest, + txParams, + prv: params.prv, + reqId: params.reqId || new RequestTracer(), + apiVersion: params.apiVersion, + }); } /** @@ -5383,11 +5359,7 @@ export class Wallet implements IWallet { // which means that the user is handling the signing in external signing mode if (!customSigningFunction && keychains?.[0]?.encryptedPrv && walletPassphrase) { if (!(await decryptKeychainPrivateKey(this.bitgo, keychains[0], walletPassphrase))) { - const error: Error & { code?: string } = new Error( - `unable to decrypt keychain with the given wallet passphrase` - ); - error.code = 'wallet_passphrase_incorrect'; - throw error; + throw new IncorrectPasswordError(); } } return keychains;