From 1d396cca6df814ef93c4294c52e9aece75721216 Mon Sep 17 00:00:00 2001 From: Marzooqa Kather Date: Mon, 25 May 2026 15:57:55 +0000 Subject: [PATCH] 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 --- .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 14 +++++ .../unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 62 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 3754f353a2..8dc3175f48 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -48,6 +48,20 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { private static readonly MPS_DSG_SIGNING_ROUND1_STATE = 'MPS_DSG_SIGNING_ROUND1_STATE'; private static readonly MPS_DSG_SIGNING_ROUND2_STATE = 'MPS_DSG_SIGNING_ROUND2_STATE'; + async isEddsaMpcV1SigningMaterial(encryptedKeyShare: string, walletPassphrase: string): Promise { + try { + const prv = await this.bitgo.decryptAsync({ input: encryptedKeyShare, password: walletPassphrase }); + const signingMaterial = JSON.parse(prv); + return ( + typeof signingMaterial?.uShare?.seed === 'string' && + typeof signingMaterial?.bitgoYShare?.u === 'string' && + (typeof signingMaterial?.backupYShare?.u === 'string' || typeof signingMaterial?.userYShare?.u === 'string') + ); + } catch { + return false; + } + } + /** @inheritdoc */ async createKeychains(params: { passphrase: string; diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 52ac0c69e7..77784805b8 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -1534,3 +1534,65 @@ function bytesToWord(bytes?: Uint8Array | number[]): number { return bytes.reduce((num, byte) => num * 0x100 + byte, 0); } + +describe('EddsaMPCv2Utils.isEddsaMpcV1SigningMaterial', () => { + const PASSPHRASE = 'test-passphrase'; + + const MPCv1_MATERIAL_BACKUP = { + uShare: { i: 1, t: 2, n: 3, y: 'aabbcc', seed: 'deadbeef01234567', chaincode: '00' }, + bitgoYShare: { i: 3, j: 1, y: 'aabbcc', u: 'bitgo-u-value', chaincode: '00' }, + backupYShare: { i: 2, j: 1, y: 'aabbcc', u: 'backup-u-value', chaincode: '00' }, + }; + + const MPCv1_MATERIAL_USER = { + uShare: { i: 2, t: 2, n: 3, y: 'aabbcc', seed: 'deadbeef01234567', chaincode: '00' }, + bitgoYShare: { i: 3, j: 2, y: 'aabbcc', u: 'bitgo-u-value', chaincode: '00' }, + userYShare: { i: 1, j: 2, y: 'aabbcc', u: 'user-u-value', chaincode: '00' }, + }; + + const MPCv2_CBOR_BYTES = Buffer.from([0xd9, 0x01, 0x04, 0xa3, 0x61, 0x78, 0x18, 0x00]).toString('base64'); + + let eddsaUtils: EddsaMPCv2Utils; + let mockBitgo: BitGoBase; + + beforeEach(() => { + mockBitgo = { + decryptAsync: sinon + .stub() + .callsFake(async (params: { input: string; password: string }) => sjcl.decrypt(params.password, params.input)), + } as unknown as BitGoBase; + + eddsaUtils = new EddsaMPCv2Utils(mockBitgo, {} as unknown as IBaseCoin); + }); + + it('returns true for MPCv1 SJCL-encrypted keycard with backupYShare + correct passphrase', async () => { + const encrypted = sjcl.encrypt(PASSPHRASE, JSON.stringify(MPCv1_MATERIAL_BACKUP)); + assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), true); + }); + + it('returns true for MPCv1 SJCL-encrypted keycard with userYShare + correct passphrase', async () => { + const encrypted = sjcl.encrypt(PASSPHRASE, JSON.stringify(MPCv1_MATERIAL_USER)); + assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), true); + }); + + it('returns false for MPCv2 CBOR content wrapped in SJCL envelope + correct passphrase', async () => { + const encrypted = sjcl.encrypt(PASSPHRASE, MPCv2_CBOR_BYTES); + assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), false); + }); + + it('returns false for MPCv2 Argon2id envelope (v2) + correct passphrase (forward-compat)', async () => { + const fakeV2Envelope = JSON.stringify({ v: 2, m: 65536, t: 3, p: 4, salt: 'AAAA', iv: 'AAAA', ct: 'AAAA' }); + assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(fakeV2Envelope, PASSPHRASE), false); + }); + + it('returns false for wrong passphrase — does not throw', async () => { + const encrypted = sjcl.encrypt(PASSPHRASE, JSON.stringify(MPCv1_MATERIAL_BACKUP)); + assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, 'wrong-passphrase'), false); + }); + + it('returns false when neither backupYShare.u nor userYShare.u is present', async () => { + const partial = { uShare: { seed: 'abc' }, bitgoYShare: { u: 'xyz' } }; + const encrypted = sjcl.encrypt(PASSPHRASE, JSON.stringify(partial)); + assert.strictEqual(await eddsaUtils.isEddsaMpcV1SigningMaterial(encrypted, PASSPHRASE), false); + }); +});