Skip to content

Commit 1ffec78

Browse files
committed
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 1ffec78

2 files changed

Lines changed: 76 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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,3 +1534,65 @@ 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+
beforeEach(() => {
1559+
mockBitgo = {
1560+
decryptAsync: sinon
1561+
.stub()
1562+
.callsFake(async (params: { input: string; password: string }) => sjcl.decrypt(params.password, params.input)),
1563+
} as unknown as BitGoBase;
1564+
1565+
eddsaUtils = new EddsaMPCv2Utils(mockBitgo, {} as unknown as IBaseCoin);
1566+
});
1567+
1568+
it('returns true for MPCv1 SJCL-encrypted keycard with backupYShare + correct passphrase', async () => {
1569+
const encrypted = sjcl.encrypt(PASSPHRASE, JSON.stringify(MPCv1_MATERIAL_BACKUP));
1570+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), true);
1571+
});
1572+
1573+
it('returns true for MPCv1 SJCL-encrypted keycard with userYShare + correct passphrase', async () => {
1574+
const encrypted = sjcl.encrypt(PASSPHRASE, JSON.stringify(MPCv1_MATERIAL_USER));
1575+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), true);
1576+
});
1577+
1578+
it('returns false for MPCv2 CBOR content wrapped in SJCL envelope + correct passphrase', async () => {
1579+
const encrypted = sjcl.encrypt(PASSPHRASE, MPCv2_CBOR_BYTES);
1580+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), false);
1581+
});
1582+
1583+
it('returns false for MPCv2 Argon2id envelope (v2) + correct passphrase (forward-compat)', async () => {
1584+
const fakeV2Envelope = JSON.stringify({ v: 2, m: 65536, t: 3, p: 4, salt: 'AAAA', iv: 'AAAA', ct: 'AAAA' });
1585+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(fakeV2Envelope, PASSPHRASE), false);
1586+
});
1587+
1588+
it('returns false for wrong passphrase — does not throw', async () => {
1589+
const encrypted = sjcl.encrypt(PASSPHRASE, JSON.stringify(MPCv1_MATERIAL_BACKUP));
1590+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, 'wrong-passphrase'), false);
1591+
});
1592+
1593+
it('returns false when neither backupYShare.u nor userYShare.u is present', async () => {
1594+
const partial = { uShare: { seed: 'abc' }, bitgoYShare: { u: 'xyz' } };
1595+
const encrypted = sjcl.encrypt(PASSPHRASE, JSON.stringify(partial));
1596+
assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), false);
1597+
});
1598+
});

0 commit comments

Comments
 (0)