Skip to content
Open
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
3 changes: 2 additions & 1 deletion modules/passkey-crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
16 changes: 4 additions & 12 deletions modules/passkey-crypto/src/attachPasskeyToWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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) {
Expand All @@ -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,
},
Expand Down
10 changes: 6 additions & 4 deletions modules/passkey-crypto/src/deriveEnterpriseSalt.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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_-]+$/);
});
});
Loading