@tinfoilsh/passkey-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +127 -0
- package/dist/codec.d.ts +12 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +61 -0
- package/dist/codec.js.map +1 -0
- package/dist/crypto.d.ts +55 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +105 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/kit.d.ts +63 -0
- package/dist/kit.d.ts.map +1 -0
- package/dist/kit.js +199 -0
- package/dist/kit.js.map +1 -0
- package/dist/protocol.d.ts +13 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +13 -0
- package/dist/protocol.js.map +1 -0
- package/dist/storage.d.ts +31 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +68 -0
- package/dist/storage.js.map +1 -0
- package/dist/support.d.ts +15 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +58 -0
- package/dist/support.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/webauthn.d.ts +38 -0
- package/dist/webauthn.d.ts.map +1 -0
- package/dist/webauthn.js +189 -0
- package/dist/webauthn.js.map +1 -0
- package/package.json +55 -0
- package/src/codec.ts +71 -0
- package/src/crypto.ts +161 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +47 -0
- package/src/kit.ts +333 -0
- package/src/protocol.ts +15 -0
- package/src/storage.ts +69 -0
- package/src/support.ts +61 -0
- package/src/types.ts +113 -0
- package/src/webauthn.ts +258 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn PRF ceremonies: credential creation and assertion with the PRF
|
|
3
|
+
* extension. Pure ceremony logic — persistence of the results is handled
|
|
4
|
+
* by the kit through the hooks on {@link CeremonyContext}.
|
|
5
|
+
*/
|
|
6
|
+
import type { PasskeyKitErrorMessages, PasskeyKitLogger, PasskeyUser, PrfPasskeyResult } from "./types.js";
|
|
7
|
+
export interface CeremonyContext {
|
|
8
|
+
rpId: string;
|
|
9
|
+
rpName: string;
|
|
10
|
+
/** Salt passed to PRF eval.first — the client internally computes
|
|
11
|
+
* SHA-256("WebAuthn PRF" || 0x00 || salt). */
|
|
12
|
+
prfSalt: Uint8Array;
|
|
13
|
+
webauthnTimeoutMs: number;
|
|
14
|
+
stuckTimeoutMs: number;
|
|
15
|
+
errorMessages?: PasskeyKitErrorMessages;
|
|
16
|
+
logger: PasskeyKitLogger;
|
|
17
|
+
/** Invoked after every successful PRF ceremony so the kit can cache state. */
|
|
18
|
+
onPrfResult(result: PrfPasskeyResult, credential: PublicKeyCredential): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a new PRF-capable passkey for the given user.
|
|
22
|
+
*
|
|
23
|
+
* Returns the credential ID and PRF output, or null if the user cancels.
|
|
24
|
+
* Throws {@link PrfNotSupportedError} when the authenticator cannot supply
|
|
25
|
+
* PRF output and {@link PasskeyTimeoutError} when the provider hangs.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createPrfPasskey(ctx: CeremonyContext, user: PasskeyUser): Promise<PrfPasskeyResult | null>;
|
|
28
|
+
/**
|
|
29
|
+
* Authenticate with an existing PRF passkey to derive the PRF output.
|
|
30
|
+
*
|
|
31
|
+
* @param credentialIds - base64url-encoded credential IDs to allow. Pass all
|
|
32
|
+
* known PRF credential IDs so the browser can select the right one.
|
|
33
|
+
* @returns The matched credential ID and PRF output, or null on failure/cancel.
|
|
34
|
+
*/
|
|
35
|
+
export declare function authenticatePrfPasskey(ctx: CeremonyContext, credentialIds: string[], options?: {
|
|
36
|
+
throwOnCancel?: boolean;
|
|
37
|
+
}): Promise<PrfPasskeyResult | null>;
|
|
38
|
+
//# sourceMappingURL=webauthn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webauthn.d.ts","sourceRoot":"","sources":["../src/webauthn.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAYH,OAAO,KAAK,EACV,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf;mDAC+C;IAC/C,OAAO,EAAE,UAAU,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,MAAM,EAAE,gBAAgB,CAAC;IACzB,8EAA8E;IAC9E,WAAW,CAAC,MAAM,EAAE,gBAAgB,EAAE,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC9E;AAwBD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAqGlC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,eAAe,EACpB,aAAa,EAAE,MAAM,EAAE,EACvB,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACxC,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA0ElC"}
|
package/dist/webauthn.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebAuthn PRF ceremonies: credential creation and assertion with the PRF
|
|
3
|
+
* extension. Pure ceremony logic — persistence of the results is handled
|
|
4
|
+
* by the kit through the hooks on {@link CeremonyContext}.
|
|
5
|
+
*/
|
|
6
|
+
import { base64UrlToBytes, bufferSourceToArrayBuffer, bytesToBase64Url, } from "./codec.js";
|
|
7
|
+
import { PasskeyKitError, PasskeyTimeoutError, PrfNotSupportedError, } from "./errors.js";
|
|
8
|
+
const MAX_USER_HANDLE_BYTE_COUNT = 64;
|
|
9
|
+
async function withStuckTimeout(promise, ctx) {
|
|
10
|
+
let timer;
|
|
11
|
+
try {
|
|
12
|
+
return await Promise.race([
|
|
13
|
+
promise,
|
|
14
|
+
new Promise((_, reject) => {
|
|
15
|
+
timer = setTimeout(() => reject(new PasskeyTimeoutError(ctx.errorMessages?.timeout)), ctx.stuckTimeoutMs);
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
if (timer !== undefined)
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a new PRF-capable passkey for the given user.
|
|
26
|
+
*
|
|
27
|
+
* Returns the credential ID and PRF output, or null if the user cancels.
|
|
28
|
+
* Throws {@link PrfNotSupportedError} when the authenticator cannot supply
|
|
29
|
+
* PRF output and {@link PasskeyTimeoutError} when the provider hangs.
|
|
30
|
+
*/
|
|
31
|
+
export async function createPrfPasskey(ctx, user) {
|
|
32
|
+
const userIdBytes = new TextEncoder().encode(user.id);
|
|
33
|
+
if (userIdBytes.byteLength > MAX_USER_HANDLE_BYTE_COUNT) {
|
|
34
|
+
throw new PasskeyKitError(`passkey-kit: user id must be at most ${MAX_USER_HANDLE_BYTE_COUNT} UTF-8 bytes`);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const credential = (await withStuckTimeout(navigator.credentials.create({
|
|
38
|
+
publicKey: {
|
|
39
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
40
|
+
rp: { id: ctx.rpId, name: ctx.rpName },
|
|
41
|
+
user: {
|
|
42
|
+
id: userIdBytes,
|
|
43
|
+
name: user.name,
|
|
44
|
+
displayName: user.displayName || user.name,
|
|
45
|
+
},
|
|
46
|
+
pubKeyCredParams: [
|
|
47
|
+
{ type: "public-key", alg: -7 }, // ES256
|
|
48
|
+
{ type: "public-key", alg: -257 }, // RS256 (broader compat)
|
|
49
|
+
],
|
|
50
|
+
authenticatorSelection: {
|
|
51
|
+
residentKey: "preferred",
|
|
52
|
+
userVerification: "required",
|
|
53
|
+
},
|
|
54
|
+
timeout: ctx.webauthnTimeoutMs,
|
|
55
|
+
extensions: {
|
|
56
|
+
prf: { eval: { first: ctx.prfSalt } },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}), ctx));
|
|
60
|
+
if (!credential) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const extensionResults = credential.getClientExtensionResults();
|
|
64
|
+
const prfResults = extensionResults.prf;
|
|
65
|
+
if (!prfResults?.enabled) {
|
|
66
|
+
ctx.logger.info?.("Authenticator does not support PRF", {
|
|
67
|
+
action: "createPrfPasskey",
|
|
68
|
+
});
|
|
69
|
+
throw new PrfNotSupportedError(ctx.errorMessages?.prfNotSupported);
|
|
70
|
+
}
|
|
71
|
+
const credentialId = bytesToBase64Url(new Uint8Array(credential.rawId));
|
|
72
|
+
// Some authenticators return PRF results during creation, others don't.
|
|
73
|
+
// "Not all authenticators support evaluating the PRFs during credential
|
|
74
|
+
// creation so outputs may, or may not, be provided."
|
|
75
|
+
// — https://w3c.github.io/webauthn/#prf-extension (eval description)
|
|
76
|
+
if (prfResults.results?.first) {
|
|
77
|
+
const result = {
|
|
78
|
+
credentialId,
|
|
79
|
+
prfOutput: bufferSourceToArrayBuffer(prfResults.results.first),
|
|
80
|
+
};
|
|
81
|
+
ctx.onPrfResult(result, credential);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
// PRF enabled but no results during create — do an immediate get()
|
|
85
|
+
ctx.logger.info?.("PRF enabled but no results during creation, doing immediate auth", { action: "createPrfPasskey" });
|
|
86
|
+
// Pass throwOnCancel so a user-cancelled assertion surfaces as a
|
|
87
|
+
// DOMException we can handle below — otherwise a `null` return would
|
|
88
|
+
// be indistinguishable from "provider returned no PRF output" and we'd
|
|
89
|
+
// show the misleading "PRF not supported" error for a plain cancel.
|
|
90
|
+
const postCreateAuth = await authenticatePrfPasskey(ctx, [credentialId], {
|
|
91
|
+
throwOnCancel: true,
|
|
92
|
+
});
|
|
93
|
+
if (!postCreateAuth) {
|
|
94
|
+
// The provider claimed PRF support during creation but didn't deliver
|
|
95
|
+
// a PRF output on the immediately-following assertion. Treat this as
|
|
96
|
+
// a lack of real PRF support rather than a silent failure.
|
|
97
|
+
throw new PrfNotSupportedError(ctx.errorMessages?.prfNotSupported);
|
|
98
|
+
}
|
|
99
|
+
return postCreateAuth;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof PrfNotSupportedError)
|
|
103
|
+
throw error;
|
|
104
|
+
if (error instanceof PasskeyTimeoutError)
|
|
105
|
+
throw error;
|
|
106
|
+
// DOMException with name "NotAllowedError" means the user cancelled
|
|
107
|
+
if (error instanceof DOMException && error.name === "NotAllowedError") {
|
|
108
|
+
ctx.logger.info?.("User cancelled passkey creation", {
|
|
109
|
+
action: "createPrfPasskey",
|
|
110
|
+
});
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
ctx.logger.error?.("Failed to create PRF passkey", error, {
|
|
114
|
+
action: "createPrfPasskey",
|
|
115
|
+
});
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Authenticate with an existing PRF passkey to derive the PRF output.
|
|
121
|
+
*
|
|
122
|
+
* @param credentialIds - base64url-encoded credential IDs to allow. Pass all
|
|
123
|
+
* known PRF credential IDs so the browser can select the right one.
|
|
124
|
+
* @returns The matched credential ID and PRF output, or null on failure/cancel.
|
|
125
|
+
*/
|
|
126
|
+
export async function authenticatePrfPasskey(ctx, credentialIds, options = {}) {
|
|
127
|
+
const { throwOnCancel = false } = options;
|
|
128
|
+
const allowCredentials = credentialIds.map((id) => ({
|
|
129
|
+
id: base64UrlToBytes(id),
|
|
130
|
+
type: "public-key",
|
|
131
|
+
}));
|
|
132
|
+
try {
|
|
133
|
+
const assertion = (await withStuckTimeout(navigator.credentials.get({
|
|
134
|
+
publicKey: {
|
|
135
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
136
|
+
rpId: ctx.rpId,
|
|
137
|
+
allowCredentials,
|
|
138
|
+
userVerification: "required",
|
|
139
|
+
timeout: ctx.webauthnTimeoutMs,
|
|
140
|
+
extensions: {
|
|
141
|
+
prf: { eval: { first: ctx.prfSalt } },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}), ctx));
|
|
145
|
+
if (!assertion) {
|
|
146
|
+
ctx.logger.info?.("passkey assertion returned no credential", {
|
|
147
|
+
action: "authenticatePrfPasskey",
|
|
148
|
+
allowedCredentials: credentialIds.length,
|
|
149
|
+
});
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const extensionResults = assertion.getClientExtensionResults();
|
|
153
|
+
const prfOutput = extensionResults.prf?.results?.first;
|
|
154
|
+
if (!prfOutput) {
|
|
155
|
+
ctx.logger.error?.("PRF output missing from assertion", undefined, {
|
|
156
|
+
action: "authenticatePrfPasskey",
|
|
157
|
+
});
|
|
158
|
+
throw new PrfNotSupportedError(ctx.errorMessages?.prfNotSupported);
|
|
159
|
+
}
|
|
160
|
+
const result = {
|
|
161
|
+
credentialId: bytesToBase64Url(new Uint8Array(assertion.rawId)),
|
|
162
|
+
prfOutput: bufferSourceToArrayBuffer(prfOutput),
|
|
163
|
+
};
|
|
164
|
+
ctx.onPrfResult(result, assertion);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof PasskeyTimeoutError)
|
|
169
|
+
throw error;
|
|
170
|
+
if (error instanceof DOMException && error.name === "NotAllowedError") {
|
|
171
|
+
// NotAllowedError covers both a user cancel and the case where the
|
|
172
|
+
// provider has no usable credential for any of the allowed ids
|
|
173
|
+
// (e.g. the passkey was created in a different browser/profile and
|
|
174
|
+
// never persisted on this device).
|
|
175
|
+
ctx.logger.info?.("passkey authentication not allowed (cancelled or no usable credential)", {
|
|
176
|
+
action: "authenticatePrfPasskey",
|
|
177
|
+
allowedCredentials: credentialIds.length,
|
|
178
|
+
});
|
|
179
|
+
if (throwOnCancel)
|
|
180
|
+
throw error;
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
ctx.logger.error?.("Failed to authenticate with PRF passkey", error, {
|
|
184
|
+
action: "authenticatePrfPasskey",
|
|
185
|
+
});
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=webauthn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webauthn.js","sourceRoot":"","sources":["../src/webauthn.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tinfoilsh/passkey-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WebAuthn PRF passkey SDK for protecting encryption keys with passkey-derived keys.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
|
|
23
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
26
|
+
"prepare": "npm run build",
|
|
27
|
+
"prepublishOnly": "npm run test && npm run typecheck"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/tinfoilsh/tinfoil-passkey-kit.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/tinfoilsh/tinfoil-passkey-kit#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/tinfoilsh/tinfoil-passkey-kit/issues"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"passkey",
|
|
39
|
+
"webauthn",
|
|
40
|
+
"prf",
|
|
41
|
+
"webcrypto",
|
|
42
|
+
"encryption"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"happy-dom": "^20.0.11",
|
|
52
|
+
"typescript": "^5",
|
|
53
|
+
"vitest": "^4.0.16"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/codec.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Dependency-free binary codecs shared across the SDK. */
|
|
2
|
+
|
|
3
|
+
export function bytesToHex(bytes: Uint8Array): string {
|
|
4
|
+
let out = ''
|
|
5
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
6
|
+
out += bytes[i].toString(16).padStart(2, '0')
|
|
7
|
+
}
|
|
8
|
+
return out
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
12
|
+
if (hex.length % 2 !== 0) {
|
|
13
|
+
throw new Error('passkey-kit: odd-length hex input')
|
|
14
|
+
}
|
|
15
|
+
if (!/^[0-9a-fA-F]*$/.test(hex)) {
|
|
16
|
+
throw new Error('passkey-kit: invalid hex character')
|
|
17
|
+
}
|
|
18
|
+
const out = new Uint8Array(hex.length / 2)
|
|
19
|
+
for (let i = 0; i < out.length; i++) {
|
|
20
|
+
out[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16)
|
|
21
|
+
}
|
|
22
|
+
return out
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
26
|
+
const CHUNK_SIZE = 0x8000
|
|
27
|
+
const chunks: string[] = []
|
|
28
|
+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
|
|
29
|
+
const chunk = bytes.subarray(i, i + CHUNK_SIZE)
|
|
30
|
+
chunks.push(String.fromCharCode.apply(null, Array.from(chunk)))
|
|
31
|
+
}
|
|
32
|
+
return btoa(chunks.join(''))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function base64ToBytes(base64: string): Uint8Array {
|
|
36
|
+
const binary = atob(base64)
|
|
37
|
+
const bytes = new Uint8Array(binary.length)
|
|
38
|
+
for (let i = 0; i < binary.length; i++) {
|
|
39
|
+
bytes[i] = binary.charCodeAt(i)
|
|
40
|
+
}
|
|
41
|
+
return bytes
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function bytesToBase64Url(bytes: Uint8Array): string {
|
|
45
|
+
return bytesToBase64(bytes)
|
|
46
|
+
.replace(/\+/g, '-')
|
|
47
|
+
.replace(/\//g, '_')
|
|
48
|
+
.replace(/=+$/, '')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function base64UrlToBytes(base64url: string): Uint8Array {
|
|
52
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/')
|
|
53
|
+
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4)
|
|
54
|
+
return base64ToBytes(padded)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Convert a BufferSource (ArrayBuffer or view) to a standalone ArrayBuffer copy. */
|
|
58
|
+
export function bufferSourceToArrayBuffer(source: BufferSource): ArrayBuffer {
|
|
59
|
+
if (source instanceof ArrayBuffer) {
|
|
60
|
+
return source.slice(0)
|
|
61
|
+
}
|
|
62
|
+
return source.buffer.slice(
|
|
63
|
+
source.byteOffset,
|
|
64
|
+
source.byteOffset + source.byteLength,
|
|
65
|
+
) as ArrayBuffer
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Normalize a string (UTF-8 encoded) or byte input to bytes. */
|
|
69
|
+
export function toBytes(input: string | Uint8Array): Uint8Array {
|
|
70
|
+
return typeof input === 'string' ? new TextEncoder().encode(input) : input
|
|
71
|
+
}
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure WebCrypto primitives: PRF output → KEK derivation (HKDF-SHA-256)
|
|
3
|
+
* and CEK wrap/unwrap under that KEK (AES-256-GCM).
|
|
4
|
+
*
|
|
5
|
+
* References:
|
|
6
|
+
* - W3C WebAuthn Level 3, §10.1.4 (PRF extension): https://w3c.github.io/webauthn/#prf-extension
|
|
7
|
+
* - RFC 5869 (HKDF): https://tools.ietf.org/html/rfc5869
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { bytesToHex, hexToBytes, toBytes } from "./codec.js";
|
|
11
|
+
import { PasskeyKitError } from "./errors.js";
|
|
12
|
+
import { TINFOIL_HKDF_INFO_V1, TINFOIL_KEY_ID_INFO_V1 } from "./protocol.js";
|
|
13
|
+
import type { WrappedCek } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export const CEK_BYTES = 32;
|
|
16
|
+
const AES_GCM_IV_BYTES = 12;
|
|
17
|
+
const DEFAULT_KEY_ID_BYTES = 16;
|
|
18
|
+
|
|
19
|
+
/** Generate a fresh random 32-byte CEK suitable for {@link wrapCek}. */
|
|
20
|
+
export function generateCek(): Uint8Array {
|
|
21
|
+
return crypto.getRandomValues(new Uint8Array(CEK_BYTES));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Type guard for a well-formed CEK: a Uint8Array of exactly
|
|
26
|
+
* {@link CEK_BYTES} bytes. Useful for validating deserialized input
|
|
27
|
+
* before wrapping.
|
|
28
|
+
*/
|
|
29
|
+
export function isValidCek(cek: unknown): cek is Uint8Array {
|
|
30
|
+
return cek instanceof Uint8Array && cek.length === CEK_BYTES;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Derive an AES-256-GCM Key Encryption Key (KEK) from PRF output using HKDF.
|
|
35
|
+
*
|
|
36
|
+
* Raw PRF output is treated as Input Keying Material (IKM), not used
|
|
37
|
+
* directly as a key. HKDF with a purpose-binding info string produces the
|
|
38
|
+
* final non-extractable CryptoKey. An empty HKDF salt is used, which is
|
|
39
|
+
* fine for high-entropy IKM (RFC 5869 §3.1).
|
|
40
|
+
*
|
|
41
|
+
* `hkdfInfo` defaults to the Tinfoil v1 protocol constant so standalone
|
|
42
|
+
* callers derive the same interoperable KEK as a default-configured kit.
|
|
43
|
+
*/
|
|
44
|
+
export async function deriveKeyEncryptionKey(
|
|
45
|
+
prfOutput: ArrayBuffer | Uint8Array,
|
|
46
|
+
hkdfInfo: string | Uint8Array = TINFOIL_HKDF_INFO_V1,
|
|
47
|
+
): Promise<CryptoKey> {
|
|
48
|
+
const masterKey = await crypto.subtle.importKey(
|
|
49
|
+
"raw",
|
|
50
|
+
prfOutput as BufferSource,
|
|
51
|
+
"HKDF",
|
|
52
|
+
false, // non-extractable
|
|
53
|
+
["deriveKey"],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return crypto.subtle.deriveKey(
|
|
57
|
+
{
|
|
58
|
+
name: "HKDF",
|
|
59
|
+
hash: "SHA-256",
|
|
60
|
+
salt: new Uint8Array(),
|
|
61
|
+
info: toBytes(hkdfInfo) as BufferSource,
|
|
62
|
+
},
|
|
63
|
+
masterKey,
|
|
64
|
+
{ name: "AES-GCM", length: 256 },
|
|
65
|
+
false, // non-extractable
|
|
66
|
+
["encrypt", "decrypt"],
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Wrap a raw 32-byte CEK under a passkey-derived KEK using AES-256-GCM
|
|
72
|
+
* with a fresh random IV. The returned hex fields are safe to persist
|
|
73
|
+
* server-side; only the matching passkey can recover the CEK.
|
|
74
|
+
*/
|
|
75
|
+
export async function wrapCek(opts: {
|
|
76
|
+
credentialId: string;
|
|
77
|
+
kek: CryptoKey;
|
|
78
|
+
cek: Uint8Array;
|
|
79
|
+
}): Promise<WrappedCek> {
|
|
80
|
+
if (opts.cek.length !== CEK_BYTES) {
|
|
81
|
+
throw new PasskeyKitError(
|
|
82
|
+
`passkey-kit: CEK must be ${CEK_BYTES} bytes, got ${opts.cek.length}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
|
|
86
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
87
|
+
{ name: "AES-GCM", iv: iv as BufferSource },
|
|
88
|
+
opts.kek,
|
|
89
|
+
opts.cek as BufferSource,
|
|
90
|
+
);
|
|
91
|
+
return {
|
|
92
|
+
credentialId: opts.credentialId,
|
|
93
|
+
kekIvHex: bytesToHex(iv),
|
|
94
|
+
wrappedKeyHex: bytesToHex(new Uint8Array(ciphertext)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Inverse of {@link wrapCek}: recover the raw CEK bytes given the same KEK.
|
|
100
|
+
* Throws on tamper (GCM auth failure) or any shape mismatch.
|
|
101
|
+
*/
|
|
102
|
+
export async function unwrapCek(
|
|
103
|
+
kek: CryptoKey,
|
|
104
|
+
wrapped: Pick<WrappedCek, "kekIvHex" | "wrappedKeyHex">,
|
|
105
|
+
): Promise<Uint8Array> {
|
|
106
|
+
if (!wrapped.kekIvHex || !wrapped.wrappedKeyHex) {
|
|
107
|
+
throw new PasskeyKitError("passkey-kit: missing iv or wrapped key");
|
|
108
|
+
}
|
|
109
|
+
const iv = hexToBytes(wrapped.kekIvHex);
|
|
110
|
+
if (iv.length !== AES_GCM_IV_BYTES) {
|
|
111
|
+
throw new PasskeyKitError("passkey-kit: iv length mismatch");
|
|
112
|
+
}
|
|
113
|
+
const ciphertext = hexToBytes(wrapped.wrappedKeyHex);
|
|
114
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
115
|
+
{ name: "AES-GCM", iv: iv as BufferSource },
|
|
116
|
+
kek,
|
|
117
|
+
ciphertext as BufferSource,
|
|
118
|
+
);
|
|
119
|
+
const cek = new Uint8Array(plaintext);
|
|
120
|
+
if (cek.length !== CEK_BYTES) {
|
|
121
|
+
throw new PasskeyKitError(
|
|
122
|
+
`passkey-kit: unwrapped CEK has wrong length ${cek.length}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return cek;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Derive a stable public identifier for a CEK via HKDF-SHA-256 with an
|
|
130
|
+
* empty salt and a purpose-binding info string. The result identifies the key
|
|
131
|
+
* without revealing it (one-way derivation).
|
|
132
|
+
*/
|
|
133
|
+
export async function deriveKeyId(
|
|
134
|
+
cek: Uint8Array,
|
|
135
|
+
opts: { info?: string | Uint8Array; lengthBytes?: number } = {},
|
|
136
|
+
): Promise<Uint8Array> {
|
|
137
|
+
if (cek.length !== CEK_BYTES) {
|
|
138
|
+
throw new PasskeyKitError(
|
|
139
|
+
`passkey-kit: CEK must be ${CEK_BYTES} bytes, got ${cek.length}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const lengthBytes = opts.lengthBytes ?? DEFAULT_KEY_ID_BYTES;
|
|
143
|
+
const ikm = await crypto.subtle.importKey(
|
|
144
|
+
"raw",
|
|
145
|
+
cek as BufferSource,
|
|
146
|
+
"HKDF",
|
|
147
|
+
false,
|
|
148
|
+
["deriveBits"],
|
|
149
|
+
);
|
|
150
|
+
const bits = await crypto.subtle.deriveBits(
|
|
151
|
+
{
|
|
152
|
+
name: "HKDF",
|
|
153
|
+
hash: "SHA-256",
|
|
154
|
+
salt: new Uint8Array(0) as BufferSource,
|
|
155
|
+
info: toBytes(opts.info ?? TINFOIL_KEY_ID_INFO_V1) as BufferSource,
|
|
156
|
+
},
|
|
157
|
+
ikm,
|
|
158
|
+
lengthBytes * 8,
|
|
159
|
+
);
|
|
160
|
+
return new Uint8Array(bits);
|
|
161
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors thrown by the SDK. Callers should branch on `instanceof`
|
|
3
|
+
* (never on message strings) to drive recovery flows.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Base class for every error the SDK throws on its own behalf. */
|
|
7
|
+
export class PasskeyKitError extends Error {
|
|
8
|
+
constructor(message: string) {
|
|
9
|
+
super(message)
|
|
10
|
+
this.name = 'PasskeyKitError'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PROVIDER_SUGGESTION =
|
|
15
|
+
"Try using iCloud Keychain, Chrome's built-in passkey manager, or the Passwords app in your device settings."
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The authenticator created a credential but does not support the WebAuthn
|
|
19
|
+
* PRF extension, so no key material can be derived from it.
|
|
20
|
+
*/
|
|
21
|
+
export class PrfNotSupportedError extends PasskeyKitError {
|
|
22
|
+
constructor(
|
|
23
|
+
message = `Your passkey provider doesn't support the security features required by this app. ${PROVIDER_SUGGESTION}`,
|
|
24
|
+
) {
|
|
25
|
+
super(message)
|
|
26
|
+
this.name = 'PrfNotSupportedError'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The passkey provider never resolved the WebAuthn promise within the
|
|
32
|
+
* SDK's hard timeout (some password-manager browser extensions hang).
|
|
33
|
+
*/
|
|
34
|
+
export class PasskeyTimeoutError extends PasskeyKitError {
|
|
35
|
+
constructor(
|
|
36
|
+
message = `Your passkey provider took too long to respond. This can happen with some browser extension password managers. ${PROVIDER_SUGGESTION}`,
|
|
37
|
+
) {
|
|
38
|
+
super(message)
|
|
39
|
+
this.name = 'PasskeyTimeoutError'
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export {
|
|
2
|
+
base64ToBytes,
|
|
3
|
+
base64UrlToBytes,
|
|
4
|
+
bufferSourceToArrayBuffer,
|
|
5
|
+
bytesToBase64,
|
|
6
|
+
bytesToBase64Url,
|
|
7
|
+
bytesToHex,
|
|
8
|
+
hexToBytes,
|
|
9
|
+
} from "./codec.js";
|
|
10
|
+
export {
|
|
11
|
+
CEK_BYTES,
|
|
12
|
+
deriveKeyEncryptionKey,
|
|
13
|
+
deriveKeyId,
|
|
14
|
+
generateCek,
|
|
15
|
+
isValidCek,
|
|
16
|
+
unwrapCek,
|
|
17
|
+
wrapCek,
|
|
18
|
+
} from "./crypto.js";
|
|
19
|
+
export {
|
|
20
|
+
PasskeyKitError,
|
|
21
|
+
PasskeyTimeoutError,
|
|
22
|
+
PrfNotSupportedError,
|
|
23
|
+
} from "./errors.js";
|
|
24
|
+
export { createPasskeyKit } from "./kit.js";
|
|
25
|
+
export type { PasskeyKit } from "./kit.js";
|
|
26
|
+
export {
|
|
27
|
+
TINFOIL_HKDF_INFO_V1,
|
|
28
|
+
TINFOIL_KEY_ID_INFO_V1,
|
|
29
|
+
TINFOIL_PRF_SALT_INPUT_V1,
|
|
30
|
+
} from "./protocol.js";
|
|
31
|
+
export {
|
|
32
|
+
browserLocalStorageAdapter,
|
|
33
|
+
createMemoryStorageAdapter,
|
|
34
|
+
} from "./storage.js";
|
|
35
|
+
export type { StorageAdapter } from "./storage.js";
|
|
36
|
+
export { detectPrfSupport } from "./support.js";
|
|
37
|
+
export type {
|
|
38
|
+
EnrollResult,
|
|
39
|
+
PasskeyKitConfig,
|
|
40
|
+
PasskeyKitErrorMessages,
|
|
41
|
+
PasskeyKitLogger,
|
|
42
|
+
PasskeyKitStorageKeys,
|
|
43
|
+
PasskeyUser,
|
|
44
|
+
PrfPasskeyResult,
|
|
45
|
+
UnlockResult,
|
|
46
|
+
WrappedCek,
|
|
47
|
+
} from "./types.js";
|