@tinfoilsh/passkey-kit 0.1.0 → 0.2.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/README.md +138 -81
- package/dist/crypto.d.ts +11 -54
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +146 -97
- package/dist/crypto.js.map +1 -1
- package/dist/errors.d.ts +11 -21
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -27
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -7
- package/dist/index.js.map +1 -1
- package/dist/kit.d.ts +2 -62
- package/dist/kit.d.ts.map +1 -1
- package/dist/kit.js +305 -159
- package/dist/kit.js.map +1 -1
- package/dist/storage.d.ts +15 -29
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +113 -62
- package/dist/storage.js.map +1 -1
- package/dist/support.d.ts +2 -14
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +32 -47
- package/dist/support.js.map +1 -1
- package/dist/types.d.ts +85 -89
- package/dist/types.d.ts.map +1 -1
- package/dist/webauthn.d.ts +10 -33
- package/dist/webauthn.d.ts.map +1 -1
- package/dist/webauthn.js +89 -177
- package/dist/webauthn.js.map +1 -1
- package/dist/wrapped-key-record-codec.d.ts +4 -0
- package/dist/wrapped-key-record-codec.d.ts.map +1 -0
- package/dist/wrapped-key-record-codec.js +89 -0
- package/dist/wrapped-key-record-codec.js.map +1 -0
- package/package.json +4 -2
- package/src/crypto.ts +171 -137
- package/src/errors.ts +29 -33
- package/src/index.ts +26 -41
- package/src/kit.ts +405 -263
- package/src/storage.ts +126 -60
- package/src/support.ts +36 -50
- package/src/types.ts +99 -99
- package/src/webauthn.ts +121 -237
- package/src/wrapped-key-record-codec.ts +93 -0
- package/dist/protocol.d.ts +0 -13
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -13
- package/dist/protocol.js.map +0 -1
- package/src/protocol.ts +0 -15
package/dist/webauthn.js
CHANGED
|
@@ -1,189 +1,101 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { base64UrlToBytes, bufferSourceToArrayBuffer, bytesToBase64Url } from "./codec.js";
|
|
2
|
+
import { invalidInput, PasskeyKeyError } from "./errors.js";
|
|
3
|
+
const CHALLENGE_BYTES = 32;
|
|
4
|
+
const MAX_USER_HANDLE_BYTES = 64;
|
|
5
|
+
const PRF_OUTPUT_BYTES = 32;
|
|
6
|
+
function extensionResults(credential) {
|
|
7
|
+
return credential.getClientExtensionResults();
|
|
8
|
+
}
|
|
9
|
+
function credentialId(credential) {
|
|
10
|
+
const rawId = new Uint8Array(credential.rawId);
|
|
11
|
+
if (rawId.length === 0)
|
|
12
|
+
throw invalidInput("credential ID must not be empty");
|
|
13
|
+
return bytesToBase64Url(rawId);
|
|
14
|
+
}
|
|
15
|
+
function resultFromCredential(credential) {
|
|
16
|
+
const first = extensionResults(credential).prf?.results?.first;
|
|
17
|
+
if (!first) {
|
|
18
|
+
throw new PasskeyKeyError("unsupported", "the authenticator returned no PRF output");
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const prfOutput = new Uint8Array(bufferSourceToArrayBuffer(first));
|
|
21
|
+
if (prfOutput.length !== PRF_OUTPUT_BYTES) {
|
|
22
|
+
throw invalidInput(`PRF output must be ${PRF_OUTPUT_BYTES} bytes`);
|
|
22
23
|
}
|
|
24
|
+
return {
|
|
25
|
+
credentialId: credentialId(credential),
|
|
26
|
+
prfOutput,
|
|
27
|
+
isPlatformAuthenticator: credential.authenticatorAttachment === "platform",
|
|
28
|
+
};
|
|
23
29
|
}
|
|
24
|
-
|
|
25
|
-
|
|
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`);
|
|
30
|
+
export async function createPrfCredential(context, user, signal) {
|
|
31
|
+
if (!(user?.id instanceof Uint8Array) || user.id.length === 0) {
|
|
32
|
+
throw invalidInput("user.id must be a non-empty Uint8Array");
|
|
35
33
|
}
|
|
36
|
-
|
|
37
|
-
|
|
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;
|
|
34
|
+
if (user.id.length > MAX_USER_HANDLE_BYTES) {
|
|
35
|
+
throw invalidInput(`user.id must be at most ${MAX_USER_HANDLE_BYTES} bytes`);
|
|
100
36
|
}
|
|
101
|
-
|
|
102
|
-
|
|
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;
|
|
37
|
+
if (typeof user.name !== "string" || user.name.length === 0) {
|
|
38
|
+
throw invalidInput("user.name must be a non-empty string");
|
|
117
39
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
40
|
+
if (user.displayName !== undefined && (typeof user.displayName !== "string" || !user.displayName)) {
|
|
41
|
+
throw invalidInput("user.displayName must be a non-empty string");
|
|
42
|
+
}
|
|
43
|
+
const credential = (await navigator.credentials.create({
|
|
44
|
+
signal,
|
|
45
|
+
publicKey: {
|
|
46
|
+
challenge: crypto.getRandomValues(new Uint8Array(CHALLENGE_BYTES)),
|
|
47
|
+
rp: { id: context.rpId, name: context.rpName },
|
|
48
|
+
user: {
|
|
49
|
+
id: user.id.slice(),
|
|
50
|
+
name: user.name,
|
|
51
|
+
displayName: user.displayName ?? user.name,
|
|
52
|
+
},
|
|
53
|
+
pubKeyCredParams: [
|
|
54
|
+
{ type: "public-key", alg: -7 },
|
|
55
|
+
{ type: "public-key", alg: -257 },
|
|
56
|
+
],
|
|
57
|
+
authenticatorSelection: {
|
|
58
|
+
authenticatorAttachment: "platform",
|
|
59
|
+
residentKey: "preferred",
|
|
138
60
|
userVerification: "required",
|
|
139
|
-
timeout: ctx.webauthnTimeoutMs,
|
|
140
|
-
extensions: {
|
|
141
|
-
prf: { eval: { first: ctx.prfSalt } },
|
|
142
|
-
},
|
|
143
61
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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;
|
|
62
|
+
timeout: context.timeoutMs,
|
|
63
|
+
extensions: {
|
|
64
|
+
prf: { eval: { first: context.prfInput.slice() } },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
if (!credential) {
|
|
69
|
+
throw new PasskeyKeyError("cancelled", "credential creation was cancelled or no eligible credential was available");
|
|
70
|
+
}
|
|
71
|
+
const extension = extensionResults(credential).prf;
|
|
72
|
+
if (!extension?.enabled) {
|
|
73
|
+
throw new PasskeyKeyError("unsupported", "the authenticator does not support PRF");
|
|
166
74
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
75
|
+
if (extension.results?.first)
|
|
76
|
+
return resultFromCredential(credential);
|
|
77
|
+
return evaluatePrfCredential(context, [credentialId(credential)], signal);
|
|
78
|
+
}
|
|
79
|
+
export async function evaluatePrfCredential(context, credentialIds, signal) {
|
|
80
|
+
const assertion = (await navigator.credentials.get({
|
|
81
|
+
signal,
|
|
82
|
+
publicKey: {
|
|
83
|
+
challenge: crypto.getRandomValues(new Uint8Array(CHALLENGE_BYTES)),
|
|
84
|
+
rpId: context.rpId,
|
|
85
|
+
allowCredentials: credentialIds.map((credentialId) => ({
|
|
86
|
+
type: "public-key",
|
|
87
|
+
id: base64UrlToBytes(credentialId),
|
|
88
|
+
})),
|
|
89
|
+
userVerification: "required",
|
|
90
|
+
timeout: context.timeoutMs,
|
|
91
|
+
extensions: {
|
|
92
|
+
prf: { eval: { first: context.prfInput.slice() } },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
if (!assertion) {
|
|
97
|
+
throw new PasskeyKeyError("cancelled", "credential evaluation was cancelled or no eligible credential was available");
|
|
187
98
|
}
|
|
99
|
+
return resultFromCredential(assertion);
|
|
188
100
|
}
|
|
189
101
|
//# sourceMappingURL=webauthn.js.map
|
package/dist/webauthn.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webauthn.js","sourceRoot":"","sources":["../src/webauthn.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"webauthn.js","sourceRoot":"","sources":["../src/webauthn.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrapped-key-record-codec.d.ts","sourceRoot":"","sources":["../src/wrapped-key-record-codec.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,YAAY,CAAC;AAgB/D,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CAmBrE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CA+C/D"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { bytesToBase64Url } from "./codec.js";
|
|
2
|
+
import { copyAndValidateProfile, decodeCanonicalBase64Url, PROFILE_KEYS, validateWrappedKey, } from "./crypto.js";
|
|
3
|
+
import { invalidInput } from "./errors.js";
|
|
4
|
+
const RECORD_FIELDS = [
|
|
5
|
+
"version",
|
|
6
|
+
"profile",
|
|
7
|
+
"credentialId",
|
|
8
|
+
"kekIvHex",
|
|
9
|
+
"wrappedKeyHex",
|
|
10
|
+
];
|
|
11
|
+
function hasExactFields(value, fields) {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
13
|
+
return false;
|
|
14
|
+
const keys = Object.keys(value).sort();
|
|
15
|
+
const expected = [...fields].sort();
|
|
16
|
+
return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
|
|
17
|
+
}
|
|
18
|
+
export function encodeWrappedKeyRecord(wrappedKey) {
|
|
19
|
+
if (!wrappedKey || typeof wrappedKey !== "object") {
|
|
20
|
+
throw invalidInput("wrapped key is required");
|
|
21
|
+
}
|
|
22
|
+
const profile = copyAndValidateProfile(wrappedKey.profile);
|
|
23
|
+
validateWrappedKey(wrappedKey, profile);
|
|
24
|
+
const record = {
|
|
25
|
+
version: 1,
|
|
26
|
+
profile: {
|
|
27
|
+
version: 1,
|
|
28
|
+
relyingPartyId: profile.relyingPartyId,
|
|
29
|
+
prfSalt: bytesToBase64Url(profile.prfSalt),
|
|
30
|
+
hkdfInfo: bytesToBase64Url(profile.hkdfInfo),
|
|
31
|
+
},
|
|
32
|
+
credentialId: wrappedKey.credentialId,
|
|
33
|
+
kekIvHex: wrappedKey.kekIvHex,
|
|
34
|
+
wrappedKeyHex: wrappedKey.wrappedKeyHex,
|
|
35
|
+
};
|
|
36
|
+
return JSON.stringify(record);
|
|
37
|
+
}
|
|
38
|
+
export function decodeWrappedKeyRecord(json) {
|
|
39
|
+
if (typeof json !== "string")
|
|
40
|
+
throw invalidInput("wrapped key JSON must be a string");
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(json);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw invalidInput("wrapped key JSON is malformed");
|
|
47
|
+
}
|
|
48
|
+
if (!hasExactFields(parsed, RECORD_FIELDS)) {
|
|
49
|
+
throw invalidInput("wrapped key record has unexpected fields");
|
|
50
|
+
}
|
|
51
|
+
if (parsed.version !== 1)
|
|
52
|
+
throw invalidInput("wrapped key record version must be 1");
|
|
53
|
+
if (!hasExactFields(parsed.profile, PROFILE_KEYS)) {
|
|
54
|
+
throw invalidInput("wrapped key profile record has unexpected fields");
|
|
55
|
+
}
|
|
56
|
+
if (parsed.profile.version !== 1) {
|
|
57
|
+
throw invalidInput("wrapped key profile version must be 1");
|
|
58
|
+
}
|
|
59
|
+
if (typeof parsed.profile.relyingPartyId !== "string") {
|
|
60
|
+
throw invalidInput("profile.relyingPartyId must be a string");
|
|
61
|
+
}
|
|
62
|
+
if (typeof parsed.credentialId !== "string") {
|
|
63
|
+
throw invalidInput("credentialId must be a string");
|
|
64
|
+
}
|
|
65
|
+
if (typeof parsed.kekIvHex !== "string") {
|
|
66
|
+
throw invalidInput("kekIvHex must be a string");
|
|
67
|
+
}
|
|
68
|
+
if (typeof parsed.wrappedKeyHex !== "string") {
|
|
69
|
+
throw invalidInput("wrappedKeyHex must be a string");
|
|
70
|
+
}
|
|
71
|
+
const wrappedKey = {
|
|
72
|
+
profile: {
|
|
73
|
+
version: 1,
|
|
74
|
+
relyingPartyId: parsed.profile.relyingPartyId,
|
|
75
|
+
prfSalt: decodeCanonicalBase64Url(parsed.profile.prfSalt, "profile.prfSalt"),
|
|
76
|
+
hkdfInfo: decodeCanonicalBase64Url(parsed.profile.hkdfInfo, "profile.hkdfInfo"),
|
|
77
|
+
},
|
|
78
|
+
credentialId: parsed.credentialId,
|
|
79
|
+
kekIvHex: parsed.kekIvHex,
|
|
80
|
+
wrappedKeyHex: parsed.wrappedKeyHex,
|
|
81
|
+
};
|
|
82
|
+
const profile = copyAndValidateProfile(wrappedKey.profile);
|
|
83
|
+
validateWrappedKey(wrappedKey, profile);
|
|
84
|
+
return {
|
|
85
|
+
...wrappedKey,
|
|
86
|
+
profile,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=wrapped-key-record-codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wrapped-key-record-codec.js","sourceRoot":"","sources":["../src/wrapped-key-record-codec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinfoilsh/passkey-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "WebAuthn PRF passkey SDK for protecting encryption keys with passkey-derived keys.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
-
"src"
|
|
19
|
+
"src",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md"
|
|
20
22
|
],
|
|
21
23
|
"scripts": {
|
|
22
24
|
"clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
|