Skip to content
Closed
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
8 changes: 8 additions & 0 deletions modules/passkey-crypto/.mocharc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require: 'tsx'
timeout: '20000'
reporter: 'min'
reporter-option:
- 'cdn=true'
- 'json=false'
exit: true
spec: ['test/unit/**/*.ts']
42 changes: 42 additions & 0 deletions modules/passkey-crypto/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@bitgo/passkey-crypto",
"version": "1.0.0",
"description": "Pure cryptographic primitives for BitGo passkey (WebAuthn PRF) key derivation",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "yarn tsc --build --incremental --verbose .",
"fmt": "prettier --write .",
"check-fmt": "prettier --check '**/*.{ts,js,json}'",
"clean": "rm -r ./dist",
"lint": "eslint --quiet .",
"prepare": "npm run build",
"test": "npm run unit-test",
"unit-test": "mocha 'test/unit/**/*.ts'"
},
"author": "BitGo SDK Team <sdkteam@bitgo.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/BitGo/BitGoJS.git",
"directory": "modules/passkey-crypto"
},
"lint-staged": {
"*.{js,ts}": [
"yarn prettier --write",
"yarn eslint --fix"
]
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/node": "^18.0.0",
"mocha": "^10.0.0",
"ts-node": "^10.0.0",
"typescript": "~5.4.5"
}
}
15 changes: 15 additions & 0 deletions modules/passkey-crypto/src/deriveEnterpriseSalt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createHmac } from 'crypto';

/**
* Derives an enterprise-scoped salt to prevent cross-enterprise key reuse.
*
* Computes HMAC-SHA256(baseSalt, enterpriseId) as a hex string.
* The baseSalt must always come from the server — never generate it client-side.
*
* @param baseSalt - Server-provided base salt
* @param enterpriseId - Enterprise identifier
* @returns Hex-encoded HMAC-SHA256 digest
*/
export function deriveEnterpriseSalt(baseSalt: string, enterpriseId: string): string {
return createHmac('sha256', baseSalt).update(enterpriseId).digest('hex');
}
12 changes: 12 additions & 0 deletions modules/passkey-crypto/src/derivePassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Derives a wallet passphrase from a WebAuthn PRF result.
*
* The PRF output (ArrayBuffer) is hex-encoded and used directly as the
* walletPassphrase for SJCL-based encryption (bitgo.encrypt).
*
* @param prfResult - Raw PRF output from WebAuthn credential assertion
* @returns Lowercase hex string to use as walletPassphrase
*/
export function derivePassword(prfResult: ArrayBuffer): string {
return Buffer.from(prfResult).toString('hex');
}
2 changes: 2 additions & 0 deletions modules/passkey-crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { derivePassword } from './derivePassword';
export { deriveEnterpriseSalt } from './deriveEnterpriseSalt';
39 changes: 39 additions & 0 deletions modules/passkey-crypto/test/unit/deriveEnterpriseSalt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as assert from 'assert';
import { createHmac } from 'crypto';
import { deriveEnterpriseSalt } from '../../src';

describe('deriveEnterpriseSalt', function () {
const BASE_SALT = 'server-provided-base-salt';
const ENTERPRISE_ID = 'ent-abc123';

it('returns a hex string', function () {
const result = deriveEnterpriseSalt(BASE_SALT, ENTERPRISE_ID);
assert.match(result, /^[0-9a-f]+$/);
});

it('returns a 64-character string (SHA-256 = 32 bytes = 64 hex chars)', function () {
const result = deriveEnterpriseSalt(BASE_SALT, ENTERPRISE_ID);
assert.strictEqual(result.length, 64);
});

it('matches a known HMAC-SHA256 test vector', function () {
const expected = createHmac('sha256', BASE_SALT).update(ENTERPRISE_ID).digest('hex');
assert.strictEqual(deriveEnterpriseSalt(BASE_SALT, ENTERPRISE_ID), expected);
});

it('is deterministic — same inputs produce same output', function () {
assert.strictEqual(deriveEnterpriseSalt(BASE_SALT, ENTERPRISE_ID), deriveEnterpriseSalt(BASE_SALT, ENTERPRISE_ID));
});

it('produces different output for different enterprise IDs', function () {
const a = deriveEnterpriseSalt(BASE_SALT, 'ent-aaa');
const b = deriveEnterpriseSalt(BASE_SALT, 'ent-bbb');
assert.notStrictEqual(a, b);
});

it('produces different output for different base salts', function () {
const a = deriveEnterpriseSalt('salt-one', ENTERPRISE_ID);
const b = deriveEnterpriseSalt('salt-two', ENTERPRISE_ID);
assert.notStrictEqual(a, b);
});
});
30 changes: 30 additions & 0 deletions modules/passkey-crypto/test/unit/derivePassword.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as assert from 'assert';
import { derivePassword } from '../../src';

describe('derivePassword', function () {
it('converts an ArrayBuffer of zeros to a hex string of zeros', function () {
const input = new ArrayBuffer(4);
assert.strictEqual(derivePassword(input), '00000000');
});

it('converts known bytes to expected hex', function () {
const input = new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer;
assert.strictEqual(derivePassword(input), 'deadbeef');
});

it('returns a lowercase hex string', function () {
const input = new Uint8Array([0xab, 0xcd]).buffer;
const result = derivePassword(input);
assert.strictEqual(result, result.toLowerCase());
});

it('returns a string of length 2x the input byte length', function () {
const input = new ArrayBuffer(32);
assert.strictEqual(derivePassword(input).length, 64);
});

it('produces the same output for the same input (deterministic)', function () {
const input = new Uint8Array([1, 2, 3, 4, 5]).buffer;
assert.strictEqual(derivePassword(input), derivePassword(input));
});
});
12 changes: 12 additions & 0 deletions modules/passkey-crypto/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./",
"strictPropertyInitialization": false,
"esModuleInterop": true,
"typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"]
},
"include": ["src/**/*", "test/**/*"],
"exclude": ["node_modules"]
}
109 changes: 106 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,13 @@
resolved "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.33.1.tgz"
integrity sha512-UnLHDY6KMmC+UXf3Ufyh+onE19xzEXjT4VZ504Acmk4PXxqyvG4cCPprlKUFnGUX7f0z8Or9MAOHXBx41uHBcg==

"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
dependencies:
"@jridgewell/trace-mapping" "0.3.9"

"@csstools/postcss-cascade-layers@^1.1.1":
version "1.1.1"
resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz"
Expand Down Expand Up @@ -3182,7 +3189,7 @@
"@jridgewell/sourcemap-codec" "^1.5.0"
"@jridgewell/trace-mapping" "^0.3.24"

"@jridgewell/resolve-uri@^3.1.0":
"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
Expand All @@ -3195,11 +3202,19 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.25"

"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==

"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"

"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28":
version "0.3.30"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz"
Expand Down Expand Up @@ -5842,6 +5857,26 @@
resolved "https://registry.npmjs.org/@tronweb3/google-protobuf/-/google-protobuf-3.21.4.tgz"
integrity sha512-joxgV4esCdyZ921AprMIG1T7HjkypquhbJ5qJti/priCBJhRE1z9GOxIEMvayxSVSRbMGIoJNE0Knrg3vpwM1w==

"@tsconfig/node10@^1.0.7":
version "1.0.12"
resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4"
integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==

"@tsconfig/node12@^1.0.7":
version "1.0.11"
resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==

"@tsconfig/node14@^1.0.0":
version "1.0.3"
resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==

"@tsconfig/node16@^1.0.2":
version "1.0.4"
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==

"@tufjs/canonical-json@1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz"
Expand Down Expand Up @@ -6290,6 +6325,13 @@
resolved "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz"
integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==

"@types/node@^18.0.0":
version "18.19.130"
resolved "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59"
integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==
dependencies:
undici-types "~5.26.4"

"@types/node@^18.0.4":
version "18.19.123"
resolved "https://registry.npmjs.org/@types/node/-/node-18.19.123.tgz"
Expand Down Expand Up @@ -7024,6 +7066,13 @@ acorn-walk@^8.0.0, acorn-walk@^8.0.2:
dependencies:
acorn "^8.11.0"

acorn-walk@^8.1.1:
version "8.3.5"
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496"
integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==
dependencies:
acorn "^8.11.0"

acorn@7.1.1:
version "7.1.1"
resolved "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz"
Expand All @@ -7044,6 +7093,11 @@ acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0:
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==

acorn@^8.4.1:
version "8.16.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==

add-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz"
Expand Down Expand Up @@ -7245,6 +7299,11 @@ are-we-there-yet@^3.0.0:
delegates "^1.0.0"
readable-stream "^3.6.0"

arg@^4.1.0:
version "4.1.3"
resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==

argparse@^1.0.10, argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz"
Expand Down Expand Up @@ -9494,6 +9553,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"

create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==

cross-env@^7.0.3:
version "7.0.3"
resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz"
Expand Down Expand Up @@ -14817,6 +14881,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
dependencies:
semver "^6.0.0"

make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==

make-fetch-happen@15.0.2, make-fetch-happen@^15.0.0, make-fetch-happen@^15.0.2:
version "15.0.2"
resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.2.tgz"
Expand Down Expand Up @@ -15354,7 +15423,7 @@ mocha@10.6.0:
yargs-parser "^20.2.9"
yargs-unparser "^2.0.0"

mocha@^10.2.0:
mocha@^10.0.0, mocha@^10.2.0:
version "10.8.2"
resolved "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz"
integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==
Expand Down Expand Up @@ -20124,6 +20193,25 @@ ts-loader@^9.1.2:
semver "^7.3.4"
source-map "^0.7.4"

ts-node@^10.0.0:
version "10.9.2"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"

ts-results@^3.2.1:
version "3.3.0"
resolved "https://registry.npmjs.org/ts-results/-/ts-results-3.3.0.tgz"
Expand Down Expand Up @@ -20421,6 +20509,11 @@ typescript@^4.2.4:
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==

typescript@~5.4.5:
version "5.4.5"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==

"ua-parser-js@>0.7.30 <0.8.0", ua-parser-js@^0.7.30, ua-parser-js@^1.0.35:
version "0.7.41"
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452"
Expand Down Expand Up @@ -20723,6 +20816,11 @@ uuid@^8.3.2:
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==

v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==

v8-compile-cache@^2.0.3:
version "2.4.0"
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz"
Expand Down Expand Up @@ -21544,6 +21642,11 @@ yeoman-generator@^5.6.1:
sort-keys "^4.2.0"
text-table "^0.2.0"

yn@3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
Expand Down
Loading