diff --git a/modules/passkey-crypto/package.json b/modules/passkey-crypto/package.json index d87804a456..1ae148013f 100644 --- a/modules/passkey-crypto/package.json +++ b/modules/passkey-crypto/package.json @@ -35,7 +35,8 @@ }, "dependencies": { "@bitgo/public-types": "6.1.0", - "@bitgo/sdk-core": "^36.44.0" + "@bitgo/sdk-core": "^36.44.0", + "@bitgo/sjcl": "^1.1.0" }, "devDependencies": { "@types/node": "^18.0.0" diff --git a/modules/passkey-crypto/src/attachPasskeyToWallet.ts b/modules/passkey-crypto/src/attachPasskeyToWallet.ts index 575828f7b9..13e3227ce8 100644 --- a/modules/passkey-crypto/src/attachPasskeyToWallet.ts +++ b/modules/passkey-crypto/src/attachPasskeyToWallet.ts @@ -37,8 +37,8 @@ export async function attachPasskeyToWallet(params: { const keychain = await wallet.getEncryptedUserKeychain(); const keychainId = keychain.id; - // Derive enterprise-scoped salt - const enterpriseSalt = deriveEnterpriseSalt(device.prfSalt, enterpriseId); + // Derive enterprise-scoped salt (already base64url-encoded) + const prfSalt = deriveEnterpriseSalt(device.prfSalt, enterpriseId); // Decrypt private key with existing passphrase const privateKey = bitgo.decrypt({ password: existingPassphrase, input: keychain.encryptedPrv }); @@ -53,7 +53,7 @@ export async function attachPasskeyToWallet(params: { publicKey: { allowCredentials: [{ type: 'public-key', id: credentialIdBuffer }], } as PublicKeyCredentialRequestOptions, - evalByCredential: { [device.credentialId]: enterpriseSalt }, + evalByCredential: { [device.credentialId]: prfSalt }, }); if (!authResult.prfResult) { @@ -64,20 +64,12 @@ export async function attachPasskeyToWallet(params: { const prfPassword = derivePassword(authResult.prfResult); const encryptedPrv = bitgo.encrypt({ password: prfPassword, input: privateKey }); - // Convert enterpriseSalt from hex to base64url (URL-safe, no padding) - // as required by the server's prfSalt validation. - const prfSaltBase64url = Buffer.from(enterpriseSalt, 'hex') - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); - // PUT webauthnInfo to keychain endpoint const updatedKeychain = await bitgo .put(bitgo.url(`/${coin}/key/${keychainId}`, 2)) .send({ webauthnInfo: { - prfSalt: prfSaltBase64url, + prfSalt, otpDeviceId: device.id, encryptedPrv, }, diff --git a/modules/passkey-crypto/src/deriveEnterpriseSalt.ts b/modules/passkey-crypto/src/deriveEnterpriseSalt.ts index caef1d40fe..11d57ba152 100644 --- a/modules/passkey-crypto/src/deriveEnterpriseSalt.ts +++ b/modules/passkey-crypto/src/deriveEnterpriseSalt.ts @@ -1,4 +1,4 @@ -import { createHmac } from 'crypto'; +import * as sjcl from '@bitgo/sjcl'; /** * Derives an enterprise-scoped PRF salt to prevent cross-enterprise key reuse. @@ -8,9 +8,11 @@ import { createHmac } from 'crypto'; * * @param baseSalt - Server-provided base64url-encoded PRF salt * @param enterpriseId - Enterprise identifier - * @returns Hex-encoded HMAC-SHA256 digest + * @returns Base64url-encoded HMAC-SHA256 digest */ export function deriveEnterpriseSalt(baseSalt: string, enterpriseId: string): string { - const keyBytes = Buffer.from(baseSalt.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); - return createHmac('sha256', keyBytes).update(enterpriseId).digest('hex'); + const keyBits = sjcl.codec.base64url.toBits(baseSalt); + const dataBits = sjcl.codec.utf8String.toBits(enterpriseId); + const resultBits = new sjcl.misc.hmac(keyBits, sjcl.hash.sha256).mac(dataBits); + return sjcl.codec.base64url.fromBits(resultBits); } diff --git a/modules/passkey-crypto/test/unit/deriveEnterpriseSalt.test.ts b/modules/passkey-crypto/test/unit/deriveEnterpriseSalt.test.ts index d9eaab111f..f92f7b4401 100644 --- a/modules/passkey-crypto/test/unit/deriveEnterpriseSalt.test.ts +++ b/modules/passkey-crypto/test/unit/deriveEnterpriseSalt.test.ts @@ -5,7 +5,7 @@ import { deriveEnterpriseSalt } from '../../src'; const REAL_FIXTURE = { basePrfSalt: 'ZqJ64M2dL65zn2-Jxd58SMN2ILc9QjbCFxUTGHd_LC8', enterpriseId: '69c2aea1a3d7bc07f7f775c0ca86b0ec', - expectedDerivedSalt: 'a226ac3aace4bb2b84cfff34e37fb7217620852bb72d5e0dfdad4c2c8473994f', + expectedDerivedSalt: 'oiasOqzkuyuEz_8043-3IXYghSu3LV4N_a1MLIRzmU8', }; describe('deriveEnterpriseSalt', function () { @@ -37,10 +37,10 @@ describe('deriveEnterpriseSalt', function () { assert.notStrictEqual(saltA, saltB); }); - it('returns a non-empty hex string', function () { + it('returns a non-empty base64url string', function () { const result = deriveEnterpriseSalt(REAL_FIXTURE.basePrfSalt, REAL_FIXTURE.enterpriseId); assert.strictEqual(typeof result, 'string'); assert.ok(result.length > 0); - assert.match(result, /^[0-9a-f]{64}$/); + assert.match(result, /^[A-Za-z0-9_-]+$/); }); });