Skip to content

Commit b91c894

Browse files
Marzooqabitgobot
authored andcommitted
feat(sdk-core): add isEddsaMpcV1SigningMaterial format detector
Export `isEddsaMpcV1SigningMaterial` from eddsaMPCv2.ts. The function decrypts an SJCL-encrypted keycard and checks for the structural shape of MPCv1 SigningMaterial (UShare.seed + at least one YShare.u). Returns false on any error so callers can safely branch to the MPCv2 path. Ticket: WCI-395 Session-Id: 5a65f0b2-1638-4b8c-b090-10d1a78ca491 Task-Id: f0c8184f-d80d-4652-af77-119a855e5029
1 parent 8bc40d5 commit b91c894

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
4848
private static readonly MPS_DSG_SIGNING_ROUND1_STATE = 'MPS_DSG_SIGNING_ROUND1_STATE';
4949
private static readonly MPS_DSG_SIGNING_ROUND2_STATE = 'MPS_DSG_SIGNING_ROUND2_STATE';
5050

51+
async isEddsaMpcV1SigningMaterial(encryptedKeyShare: string, walletPassphrase: string): Promise<boolean> {
52+
try {
53+
const prv = await this.bitgo.decryptAsync({ input: encryptedKeyShare, password: walletPassphrase });
54+
const signingMaterial = JSON.parse(prv);
55+
return (
56+
typeof signingMaterial?.uShare?.seed === 'string' &&
57+
typeof signingMaterial?.bitgoYShare?.u === 'string' &&
58+
(typeof signingMaterial?.backupYShare?.u === 'string' || typeof signingMaterial?.userYShare?.u === 'string')
59+
);
60+
} catch {
61+
return false;
62+
}
63+
}
64+
5165
/** @inheritdoc */
5266
async createKeychains(params: {
5367
passphrase: string;

modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,3 +1534,79 @@ function bytesToWord(bytes?: Uint8Array | number[]): number {
15341534

15351535
return bytes.reduce((num, byte) => num * 0x100 + byte, 0);
15361536
}
1537+
1538+
describe('EddsaMPCv2Utils.isEddsaMpcV1SigningMaterial', () => {
1539+
const PASSPHRASE = 'test-passphrase';
1540+
1541+
const MPCv1_MATERIAL_BACKUP = {
1542+
uShare: { i: 1, t: 2, n: 3, y: 'aabbcc', seed: 'deadbeef01234567', chaincode: '00' },
1543+
bitgoYShare: { i: 3, j: 1, y: 'aabbcc', u: 'bitgo-u-value', chaincode: '00' },
1544+
backupYShare: { i: 2, j: 1, y: 'aabbcc', u: 'backup-u-value', chaincode: '00' },
1545+
};
1546+
1547+
const MPCv1_MATERIAL_USER = {
1548+
uShare: { i: 2, t: 2, n: 3, y: 'aabbcc', seed: 'deadbeef01234567', chaincode: '00' },
1549+
bitgoYShare: { i: 3, j: 2, y: 'aabbcc', u: 'bitgo-u-value', chaincode: '00' },
1550+
userYShare: { i: 1, j: 2, y: 'aabbcc', u: 'user-u-value', chaincode: '00' },
1551+
};
1552+
1553+
const MPCv2_CBOR_BYTES = Buffer.from([0xd9, 0x01, 0x04, 0xa3, 0x61, 0x78, 0x18, 0x00]).toString('base64');
1554+
1555+
let eddsaUtils: EddsaMPCv2Utils;
1556+
let mockBitgo: BitGoBase;
1557+
1558+
function encryptSjcl(plaintext: string, password: string): string {
1559+
const salt = randomBytes(8);
1560+
const iv = randomBytes(16);
1561+
return sjcl.encrypt(password, plaintext, {
1562+
salt: [bytesToWord(salt.subarray(0, 4)), bytesToWord(salt.subarray(4, 8))],
1563+
iv: [
1564+
bytesToWord(iv.subarray(0, 4)),
1565+
bytesToWord(iv.subarray(4, 8)),
1566+
bytesToWord(iv.subarray(8, 12)),
1567+
bytesToWord(iv.subarray(12, 16)),
1568+
],
1569+
});
1570+
}
1571+
1572+
beforeEach(() => {
1573+
mockBitgo = {
1574+
decryptAsync: sinon
1575+
.stub()
1576+
.callsFake(async (params: { input: string; password: string }) => sjcl.decrypt(params.password, params.input)),
1577+
} as unknown as BitGoBase;
1578+
1579+
eddsaUtils = new EddsaMPCv2Utils(mockBitgo, {} as unknown as IBaseCoin);
1580+
});
1581+
1582+
it('returns true for MPCv1 SJCL-encrypted keycard with backupYShare + correct passphrase', async () => {
1583+
const encrypted = encryptSjcl(JSON.stringify(MPCv1_MATERIAL_BACKUP), PASSPHRASE);
1584+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), true);
1585+
});
1586+
1587+
it('returns true for MPCv1 SJCL-encrypted keycard with userYShare + correct passphrase', async () => {
1588+
const encrypted = encryptSjcl(JSON.stringify(MPCv1_MATERIAL_USER), PASSPHRASE);
1589+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), true);
1590+
});
1591+
1592+
it('returns false for MPCv2 CBOR content wrapped in SJCL envelope + correct passphrase', async () => {
1593+
const encrypted = encryptSjcl(MPCv2_CBOR_BYTES, PASSPHRASE);
1594+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), false);
1595+
});
1596+
1597+
it('returns false for MPCv2 Argon2id envelope (v2) + correct passphrase (forward-compat)', async () => {
1598+
const fakeV2Envelope = JSON.stringify({ v: 2, m: 65536, t: 3, p: 4, salt: 'AAAA', iv: 'AAAA', ct: 'AAAA' });
1599+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(fakeV2Envelope, PASSPHRASE), false);
1600+
});
1601+
1602+
it('returns false for wrong passphrase — does not throw', async () => {
1603+
const encrypted = encryptSjcl(JSON.stringify(MPCv1_MATERIAL_BACKUP), PASSPHRASE);
1604+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, 'wrong-passphrase'), false);
1605+
});
1606+
1607+
it('returns false when neither backupYShare.u nor userYShare.u is present', async () => {
1608+
const partial = { uShare: { seed: 'abc' }, bitgoYShare: { u: 'xyz' } };
1609+
const encrypted = encryptSjcl(JSON.stringify(partial), PASSPHRASE);
1610+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), false);
1611+
});
1612+
});

0 commit comments

Comments
 (0)