Skip to content
Draft
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
2 changes: 1 addition & 1 deletion modules/abstract-utxo/test/unit/webauthn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
});
8 changes: 7 additions & 1 deletion modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
encryptRsaWithAesGcm,
GetNetworkPartnersResponse,
GShare,
IncorrectPasswordError,
MPCType,
multisigTypes,
ShareType,
Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion modules/express/src/retryPromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function retryPromise<T>(
if (err.code === 'ECONNREFUSED') {
onError(err, tryCount);
} else {
throw new Error(err);
throw err;
}
}

Expand Down
6 changes: 3 additions & 3 deletions modules/express/test/unit/clientRoutes/signPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions modules/express/test/unit/retryPromise.ts
Original file line number Diff line number Diff line change
@@ -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
);
});
});
4 changes: 2 additions & 2 deletions modules/express/test/unit/typedRoutes/coinSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
4 changes: 2 additions & 2 deletions modules/express/test/unit/typedRoutes/generateShareTSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down
6 changes: 5 additions & 1 deletion modules/sdk-core/src/bitgo/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}

Expand Down
152 changes: 62 additions & 90 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
);
}

/**
Expand Down Expand Up @@ -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
);
}

/**
Expand Down Expand Up @@ -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
);
}

/**
Expand Down Expand Up @@ -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
);
}

/**
Expand All @@ -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,
});
}

/**
Expand Down Expand Up @@ -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;
Expand Down
Loading