@chipi-stack/chipi-passkey 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/dist/index.js ADDED
@@ -0,0 +1,246 @@
1
+ 'use strict';
2
+
3
+ var browser = require('@simplewebauthn/browser');
4
+
5
+ // src/passkey.ts
6
+ var WALLET_PRF_SALT_STRING = "chipi-wallet-encryption-key-v1";
7
+ var WALLET_CREDENTIAL_KEY = "chipi_wallet_passkey_credential";
8
+ function generateRandomBase64URL(length = 32) {
9
+ const array = new Uint8Array(length);
10
+ crypto.getRandomValues(array);
11
+ return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
12
+ }
13
+ function stringToBase64URL(str) {
14
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
15
+ }
16
+ function arrayBufferToHex(buffer) {
17
+ const bytes = new Uint8Array(buffer);
18
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
19
+ }
20
+ function base64URLToArrayBuffer(base64url) {
21
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
22
+ const paddedBase64 = base64.padEnd(
23
+ base64.length + (4 - base64.length % 4) % 4,
24
+ "="
25
+ );
26
+ const binary = atob(paddedBase64);
27
+ const bytes = new Uint8Array(binary.length);
28
+ for (let i = 0; i < binary.length; i++) {
29
+ bytes[i] = binary.charCodeAt(i);
30
+ }
31
+ return bytes.buffer;
32
+ }
33
+ function getPRFSaltBase64URL() {
34
+ return stringToBase64URL(WALLET_PRF_SALT_STRING);
35
+ }
36
+ function isWebAuthnSupported() {
37
+ if (typeof window === "undefined") return false;
38
+ return browser.browserSupportsWebAuthn();
39
+ }
40
+ function getStoredCredential() {
41
+ if (typeof window === "undefined") return null;
42
+ const stored = localStorage.getItem(WALLET_CREDENTIAL_KEY);
43
+ if (!stored) return null;
44
+ try {
45
+ return JSON.parse(stored);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ function storeCredential(metadata) {
51
+ localStorage.setItem(WALLET_CREDENTIAL_KEY, JSON.stringify(metadata));
52
+ }
53
+ function removeStoredCredential() {
54
+ localStorage.removeItem(WALLET_CREDENTIAL_KEY);
55
+ }
56
+ function hasWalletPasskey() {
57
+ return getStoredCredential() !== null;
58
+ }
59
+ async function createWalletPasskey(userId, userName) {
60
+ if (!isWebAuthnSupported()) {
61
+ throw new Error("WebAuthn is not supported in this browser");
62
+ }
63
+ const challenge = generateRandomBase64URL(32);
64
+ const userIdBase64 = stringToBase64URL(userId);
65
+ const prfSalt = getPRFSaltBase64URL();
66
+ const registrationOptions = {
67
+ challenge,
68
+ rp: {
69
+ name: "Chipi Wallet",
70
+ id: window.location.hostname
71
+ },
72
+ user: {
73
+ id: userIdBase64,
74
+ name: userName,
75
+ displayName: `${userName} - Wallet Key`
76
+ },
77
+ pubKeyCredParams: [
78
+ { alg: -7, type: "public-key" },
79
+ // ES256
80
+ { alg: -257, type: "public-key" }
81
+ // RS256
82
+ ],
83
+ authenticatorSelection: {
84
+ authenticatorAttachment: "platform",
85
+ residentKey: "required",
86
+ userVerification: "required"
87
+ },
88
+ timeout: 6e4,
89
+ attestation: "none",
90
+ extensions: {
91
+ // @ts-expect-error - PRF extension not in types yet
92
+ prf: {
93
+ eval: {
94
+ first: base64URLToArrayBuffer(prfSalt)
95
+ }
96
+ }
97
+ }
98
+ };
99
+ let registration;
100
+ try {
101
+ registration = await browser.startRegistration(registrationOptions);
102
+ } catch (error) {
103
+ if (error instanceof Error) {
104
+ if (error.name === "NotAllowedError") {
105
+ throw new Error("Passkey creation was cancelled");
106
+ }
107
+ if (error.name === "InvalidStateError") {
108
+ throw new Error("A passkey already exists for this account");
109
+ }
110
+ }
111
+ throw error;
112
+ }
113
+ const credentialId = registration.id;
114
+ const prfResults = registration.clientExtensionResults?.prf;
115
+ let encryptKey;
116
+ let prfSupported = false;
117
+ if (prfResults?.results?.first) {
118
+ const prfOutput = prfResults.results.first;
119
+ if (typeof prfOutput === "string") {
120
+ encryptKey = arrayBufferToHex(base64URLToArrayBuffer(prfOutput));
121
+ } else {
122
+ encryptKey = arrayBufferToHex(prfOutput);
123
+ }
124
+ prfSupported = true;
125
+ } else if (prfResults?.enabled) {
126
+ const prfOutput = await getWalletEncryptKey(credentialId);
127
+ if (!prfOutput) {
128
+ throw new Error("Failed to derive encryption key from passkey");
129
+ }
130
+ encryptKey = prfOutput;
131
+ prfSupported = true;
132
+ } else {
133
+ console.warn("PRF extension not supported, using fallback key derivation");
134
+ encryptKey = await deriveKeyFromCredential(credentialId, userId);
135
+ prfSupported = false;
136
+ }
137
+ const transports = registration.response.transports;
138
+ storeCredential({
139
+ credentialId,
140
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
141
+ userId,
142
+ transports
143
+ });
144
+ return {
145
+ encryptKey,
146
+ credentialId,
147
+ prfSupported
148
+ };
149
+ }
150
+ async function getWalletEncryptKey(credentialId) {
151
+ if (!isWebAuthnSupported()) {
152
+ throw new Error("WebAuthn is not supported in this browser");
153
+ }
154
+ const storedCredential = getStoredCredential();
155
+ const targetCredentialId = credentialId || storedCredential?.credentialId;
156
+ if (!targetCredentialId) {
157
+ throw new Error("No wallet passkey found");
158
+ }
159
+ const challenge = generateRandomBase64URL(32);
160
+ const prfSalt = getPRFSaltBase64URL();
161
+ const authenticationOptions = {
162
+ challenge,
163
+ rpId: window.location.hostname,
164
+ allowCredentials: [
165
+ {
166
+ id: targetCredentialId,
167
+ type: "public-key",
168
+ transports: storedCredential?.transports
169
+ }
170
+ ],
171
+ userVerification: "required",
172
+ timeout: 6e4,
173
+ extensions: {
174
+ // @ts-expect-error - PRF extension not in types
175
+ prf: {
176
+ eval: {
177
+ first: base64URLToArrayBuffer(prfSalt)
178
+ }
179
+ }
180
+ }
181
+ };
182
+ let authentication;
183
+ try {
184
+ authentication = await browser.startAuthentication(authenticationOptions);
185
+ } catch (error) {
186
+ if (error instanceof Error && error.name === "NotAllowedError") {
187
+ return null;
188
+ }
189
+ throw error;
190
+ }
191
+ const prfResults = authentication.clientExtensionResults?.prf;
192
+ if (prfResults?.results?.first) {
193
+ const prfOutput = prfResults.results.first;
194
+ if (typeof prfOutput === "string") {
195
+ return arrayBufferToHex(base64URLToArrayBuffer(prfOutput));
196
+ }
197
+ return arrayBufferToHex(prfOutput);
198
+ }
199
+ console.warn("PRF not available, using fallback key derivation");
200
+ if (storedCredential) {
201
+ return deriveKeyFromCredential(targetCredentialId, storedCredential.userId);
202
+ }
203
+ throw new Error("Unable to derive encryption key");
204
+ }
205
+ async function deriveKeyFromCredential(credentialId, userId) {
206
+ const encoder = new TextEncoder();
207
+ const keyMaterial = await crypto.subtle.importKey(
208
+ "raw",
209
+ encoder.encode(credentialId),
210
+ "PBKDF2",
211
+ false,
212
+ ["deriveBits"]
213
+ );
214
+ const salt = encoder.encode(`chipi-wallet-${userId}`);
215
+ const derivedBits = await crypto.subtle.deriveBits(
216
+ {
217
+ name: "PBKDF2",
218
+ salt,
219
+ iterations: 1e5,
220
+ hash: "SHA-256"
221
+ },
222
+ keyMaterial,
223
+ 256
224
+ );
225
+ return arrayBufferToHex(derivedBits);
226
+ }
227
+ async function verifyWalletPasskey() {
228
+ try {
229
+ const encryptKey = await getWalletEncryptKey();
230
+ return encryptKey !== null;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+ var isWebAuthnPRFSupported = isWebAuthnSupported;
236
+
237
+ exports.createWalletPasskey = createWalletPasskey;
238
+ exports.getStoredCredential = getStoredCredential;
239
+ exports.getWalletEncryptKey = getWalletEncryptKey;
240
+ exports.hasWalletPasskey = hasWalletPasskey;
241
+ exports.isWebAuthnPRFSupported = isWebAuthnPRFSupported;
242
+ exports.isWebAuthnSupported = isWebAuthnSupported;
243
+ exports.removeStoredCredential = removeStoredCredential;
244
+ exports.verifyWalletPasskey = verifyWalletPasskey;
245
+ //# sourceMappingURL=index.js.map
246
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/passkey.ts"],"names":["browserSupportsWebAuthn","startRegistration","startAuthentication"],"mappings":"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","file":"index.js","sourcesContent":["/* eslint-disable no-undef */\n/**\n * Wallet Passkey Utilities\n *\n * This module provides WebAuthn passkey functionality with PRF (Pseudo-Random Function)\n * extension support for securely deriving encryption keys for wallet creation.\n *\n * Uses @simplewebauthn/browser for robust browser compatibility.\n */\n\nimport {\n startRegistration,\n startAuthentication,\n browserSupportsWebAuthn,\n} from \"@simplewebauthn/browser\";\nimport type {\n PublicKeyCredentialCreationOptionsJSON,\n PublicKeyCredentialRequestOptionsJSON,\n AuthenticationResponseJSON,\n RegistrationResponseJSON,\n} from \"@simplewebauthn/types\";\n\n// Salt used for PRF - this should be constant for your app\nconst WALLET_PRF_SALT_STRING = \"chipi-wallet-encryption-key-v1\";\n\n// Storage key for credential metadata\nconst WALLET_CREDENTIAL_KEY = \"chipi_wallet_passkey_credential\";\n\nexport interface WalletCredentialMetadata {\n credentialId: string;\n createdAt: string;\n userId: string;\n transports?: AuthenticatorTransport[];\n}\n\n/**\n * Generate a random base64url string for challenges\n */\nfunction generateRandomBase64URL(length: number = 32): string {\n const array = new Uint8Array(length);\n crypto.getRandomValues(array);\n return btoa(String.fromCharCode(...array))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=/g, \"\");\n}\n\n/**\n * Convert string to base64url\n */\nfunction stringToBase64URL(str: string): string {\n return btoa(str).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n}\n\n/**\n * Convert ArrayBuffer to hex string\n */\nfunction arrayBufferToHex(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Convert base64url to ArrayBuffer\n */\nfunction base64URLToArrayBuffer(base64url: string): ArrayBuffer {\n const base64 = base64url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const paddedBase64 = base64.padEnd(\n base64.length + ((4 - (base64.length % 4)) % 4),\n \"=\"\n );\n const binary = atob(paddedBase64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Get PRF salt as base64url for SimpleWebAuthn\n */\nfunction getPRFSaltBase64URL(): string {\n return stringToBase64URL(WALLET_PRF_SALT_STRING);\n}\n\n/**\n * Check if WebAuthn is supported in this browser\n */\nexport function isWebAuthnSupported(): boolean {\n if (typeof window === \"undefined\") return false;\n return browserSupportsWebAuthn();\n}\n\n/**\n * Get stored credential metadata\n */\nexport function getStoredCredential(): WalletCredentialMetadata | null {\n if (typeof window === \"undefined\") return null;\n\n const stored = localStorage.getItem(WALLET_CREDENTIAL_KEY);\n if (!stored) return null;\n\n try {\n return JSON.parse(stored) as WalletCredentialMetadata;\n } catch {\n return null;\n }\n}\n\n/**\n * Store credential metadata\n */\nfunction storeCredential(metadata: WalletCredentialMetadata): void {\n localStorage.setItem(WALLET_CREDENTIAL_KEY, JSON.stringify(metadata));\n}\n\n/**\n * Remove stored credential\n */\nexport function removeStoredCredential(): void {\n localStorage.removeItem(WALLET_CREDENTIAL_KEY);\n}\n\n/**\n * Check if a wallet passkey already exists\n */\nexport function hasWalletPasskey(): boolean {\n return getStoredCredential() !== null;\n}\n\nexport interface CreateWalletPasskeyResult {\n encryptKey: string;\n credentialId: string;\n prfSupported: boolean;\n}\n\n/**\n * Create a new wallet passkey with PRF extension\n */\nexport async function createWalletPasskey(\n userId: string,\n userName: string\n): Promise<CreateWalletPasskeyResult> {\n if (!isWebAuthnSupported()) {\n throw new Error(\"WebAuthn is not supported in this browser\");\n }\n\n const challenge = generateRandomBase64URL(32);\n const userIdBase64 = stringToBase64URL(userId);\n const prfSalt = getPRFSaltBase64URL();\n\n // Build registration options for SimpleWebAuthn\n const registrationOptions: PublicKeyCredentialCreationOptionsJSON = {\n challenge,\n rp: {\n name: \"Chipi Wallet\",\n id: window.location.hostname,\n },\n user: {\n id: userIdBase64,\n name: userName,\n displayName: `${userName} - Wallet Key`,\n },\n pubKeyCredParams: [\n { alg: -7, type: \"public-key\" }, // ES256\n { alg: -257, type: \"public-key\" }, // RS256\n ],\n authenticatorSelection: {\n authenticatorAttachment: \"platform\",\n residentKey: \"required\",\n userVerification: \"required\",\n },\n timeout: 60000,\n attestation: \"none\",\n extensions: {\n // @ts-expect-error - PRF extension not in types yet\n prf: {\n eval: {\n first: base64URLToArrayBuffer(prfSalt),\n },\n },\n },\n };\n\n let registration: RegistrationResponseJSON;\n try {\n registration = await startRegistration(registrationOptions);\n } catch (error) {\n if (error instanceof Error) {\n if (error.name === \"NotAllowedError\") {\n throw new Error(\"Passkey creation was cancelled\");\n }\n if (error.name === \"InvalidStateError\") {\n throw new Error(\"A passkey already exists for this account\");\n }\n }\n throw error;\n }\n\n const credentialId = registration.id;\n\n // Check PRF results\n // @ts-expect-error - PRF extension not in types\n const prfResults = registration.clientExtensionResults?.prf;\n\n let encryptKey: string;\n let prfSupported = false;\n\n if (prfResults?.results?.first) {\n // PRF was evaluated during creation\n const prfOutput = prfResults.results.first;\n if (typeof prfOutput === \"string\") {\n encryptKey = arrayBufferToHex(base64URLToArrayBuffer(prfOutput));\n } else {\n encryptKey = arrayBufferToHex(prfOutput);\n }\n prfSupported = true;\n } else if (prfResults?.enabled) {\n // PRF is supported but needs assertion to get output\n const prfOutput = await getWalletEncryptKey(credentialId);\n if (!prfOutput) {\n throw new Error(\"Failed to derive encryption key from passkey\");\n }\n encryptKey = prfOutput;\n prfSupported = true;\n } else {\n // PRF not supported - use fallback derivation\n console.warn(\"PRF extension not supported, using fallback key derivation\");\n encryptKey = await deriveKeyFromCredential(credentialId, userId);\n prfSupported = false;\n }\n\n // Get transports if available\n const transports = registration.response.transports as\n | AuthenticatorTransport[]\n | undefined;\n\n // Store credential metadata\n storeCredential({\n credentialId,\n createdAt: new Date().toISOString(),\n userId,\n transports,\n });\n\n return {\n encryptKey,\n credentialId,\n prfSupported,\n };\n}\n\n/**\n * Get the encryption key by authenticating with the wallet passkey\n */\nexport async function getWalletEncryptKey(\n credentialId?: string\n): Promise<string | null> {\n if (!isWebAuthnSupported()) {\n throw new Error(\"WebAuthn is not supported in this browser\");\n }\n\n const storedCredential = getStoredCredential();\n const targetCredentialId = credentialId || storedCredential?.credentialId;\n\n if (!targetCredentialId) {\n throw new Error(\"No wallet passkey found\");\n }\n\n const challenge = generateRandomBase64URL(32);\n const prfSalt = getPRFSaltBase64URL();\n\n // Build authentication options\n const authenticationOptions: PublicKeyCredentialRequestOptionsJSON = {\n challenge,\n rpId: window.location.hostname,\n allowCredentials: [\n {\n id: targetCredentialId,\n type: \"public-key\",\n transports: storedCredential?.transports,\n },\n ],\n userVerification: \"required\",\n timeout: 60000,\n extensions: {\n // @ts-expect-error - PRF extension not in types\n prf: {\n eval: {\n first: base64URLToArrayBuffer(prfSalt),\n },\n },\n },\n };\n\n let authentication: AuthenticationResponseJSON;\n try {\n authentication = await startAuthentication(authenticationOptions);\n } catch (error) {\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n // User cancelled\n return null;\n }\n throw error;\n }\n\n // Check PRF results\n // @ts-expect-error - PRF extension not in types\n const prfResults = authentication.clientExtensionResults?.prf;\n\n if (prfResults?.results?.first) {\n const prfOutput = prfResults.results.first;\n if (typeof prfOutput === \"string\") {\n return arrayBufferToHex(base64URLToArrayBuffer(prfOutput));\n }\n return arrayBufferToHex(prfOutput);\n }\n\n // PRF not available, use fallback\n console.warn(\"PRF not available, using fallback key derivation\");\n if (storedCredential) {\n return deriveKeyFromCredential(targetCredentialId, storedCredential.userId);\n }\n\n throw new Error(\"Unable to derive encryption key\");\n}\n\n/**\n * Fallback key derivation when PRF is not supported\n */\nasync function deriveKeyFromCredential(\n credentialId: string,\n userId: string\n): Promise<string> {\n const encoder = new TextEncoder();\n const keyMaterial = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(credentialId),\n \"PBKDF2\",\n false,\n [\"deriveBits\"]\n );\n\n const salt = encoder.encode(`chipi-wallet-${userId}`);\n\n const derivedBits = await crypto.subtle.deriveBits(\n {\n name: \"PBKDF2\",\n salt,\n iterations: 100000,\n hash: \"SHA-256\",\n },\n keyMaterial,\n 256\n );\n\n return arrayBufferToHex(derivedBits);\n}\n\n/**\n * Verify that the stored passkey is still valid\n */\nexport async function verifyWalletPasskey(): Promise<boolean> {\n try {\n const encryptKey = await getWalletEncryptKey();\n return encryptKey !== null;\n } catch {\n return false;\n }\n}\n\n// Re-export for backwards compatibility\nexport const isWebAuthnPRFSupported = isWebAuthnSupported;\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,237 @@
1
+ import { browserSupportsWebAuthn, startRegistration, startAuthentication } from '@simplewebauthn/browser';
2
+
3
+ // src/passkey.ts
4
+ var WALLET_PRF_SALT_STRING = "chipi-wallet-encryption-key-v1";
5
+ var WALLET_CREDENTIAL_KEY = "chipi_wallet_passkey_credential";
6
+ function generateRandomBase64URL(length = 32) {
7
+ const array = new Uint8Array(length);
8
+ crypto.getRandomValues(array);
9
+ return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
10
+ }
11
+ function stringToBase64URL(str) {
12
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
13
+ }
14
+ function arrayBufferToHex(buffer) {
15
+ const bytes = new Uint8Array(buffer);
16
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
17
+ }
18
+ function base64URLToArrayBuffer(base64url) {
19
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
20
+ const paddedBase64 = base64.padEnd(
21
+ base64.length + (4 - base64.length % 4) % 4,
22
+ "="
23
+ );
24
+ const binary = atob(paddedBase64);
25
+ const bytes = new Uint8Array(binary.length);
26
+ for (let i = 0; i < binary.length; i++) {
27
+ bytes[i] = binary.charCodeAt(i);
28
+ }
29
+ return bytes.buffer;
30
+ }
31
+ function getPRFSaltBase64URL() {
32
+ return stringToBase64URL(WALLET_PRF_SALT_STRING);
33
+ }
34
+ function isWebAuthnSupported() {
35
+ if (typeof window === "undefined") return false;
36
+ return browserSupportsWebAuthn();
37
+ }
38
+ function getStoredCredential() {
39
+ if (typeof window === "undefined") return null;
40
+ const stored = localStorage.getItem(WALLET_CREDENTIAL_KEY);
41
+ if (!stored) return null;
42
+ try {
43
+ return JSON.parse(stored);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function storeCredential(metadata) {
49
+ localStorage.setItem(WALLET_CREDENTIAL_KEY, JSON.stringify(metadata));
50
+ }
51
+ function removeStoredCredential() {
52
+ localStorage.removeItem(WALLET_CREDENTIAL_KEY);
53
+ }
54
+ function hasWalletPasskey() {
55
+ return getStoredCredential() !== null;
56
+ }
57
+ async function createWalletPasskey(userId, userName) {
58
+ if (!isWebAuthnSupported()) {
59
+ throw new Error("WebAuthn is not supported in this browser");
60
+ }
61
+ const challenge = generateRandomBase64URL(32);
62
+ const userIdBase64 = stringToBase64URL(userId);
63
+ const prfSalt = getPRFSaltBase64URL();
64
+ const registrationOptions = {
65
+ challenge,
66
+ rp: {
67
+ name: "Chipi Wallet",
68
+ id: window.location.hostname
69
+ },
70
+ user: {
71
+ id: userIdBase64,
72
+ name: userName,
73
+ displayName: `${userName} - Wallet Key`
74
+ },
75
+ pubKeyCredParams: [
76
+ { alg: -7, type: "public-key" },
77
+ // ES256
78
+ { alg: -257, type: "public-key" }
79
+ // RS256
80
+ ],
81
+ authenticatorSelection: {
82
+ authenticatorAttachment: "platform",
83
+ residentKey: "required",
84
+ userVerification: "required"
85
+ },
86
+ timeout: 6e4,
87
+ attestation: "none",
88
+ extensions: {
89
+ // @ts-expect-error - PRF extension not in types yet
90
+ prf: {
91
+ eval: {
92
+ first: base64URLToArrayBuffer(prfSalt)
93
+ }
94
+ }
95
+ }
96
+ };
97
+ let registration;
98
+ try {
99
+ registration = await startRegistration(registrationOptions);
100
+ } catch (error) {
101
+ if (error instanceof Error) {
102
+ if (error.name === "NotAllowedError") {
103
+ throw new Error("Passkey creation was cancelled");
104
+ }
105
+ if (error.name === "InvalidStateError") {
106
+ throw new Error("A passkey already exists for this account");
107
+ }
108
+ }
109
+ throw error;
110
+ }
111
+ const credentialId = registration.id;
112
+ const prfResults = registration.clientExtensionResults?.prf;
113
+ let encryptKey;
114
+ let prfSupported = false;
115
+ if (prfResults?.results?.first) {
116
+ const prfOutput = prfResults.results.first;
117
+ if (typeof prfOutput === "string") {
118
+ encryptKey = arrayBufferToHex(base64URLToArrayBuffer(prfOutput));
119
+ } else {
120
+ encryptKey = arrayBufferToHex(prfOutput);
121
+ }
122
+ prfSupported = true;
123
+ } else if (prfResults?.enabled) {
124
+ const prfOutput = await getWalletEncryptKey(credentialId);
125
+ if (!prfOutput) {
126
+ throw new Error("Failed to derive encryption key from passkey");
127
+ }
128
+ encryptKey = prfOutput;
129
+ prfSupported = true;
130
+ } else {
131
+ console.warn("PRF extension not supported, using fallback key derivation");
132
+ encryptKey = await deriveKeyFromCredential(credentialId, userId);
133
+ prfSupported = false;
134
+ }
135
+ const transports = registration.response.transports;
136
+ storeCredential({
137
+ credentialId,
138
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
139
+ userId,
140
+ transports
141
+ });
142
+ return {
143
+ encryptKey,
144
+ credentialId,
145
+ prfSupported
146
+ };
147
+ }
148
+ async function getWalletEncryptKey(credentialId) {
149
+ if (!isWebAuthnSupported()) {
150
+ throw new Error("WebAuthn is not supported in this browser");
151
+ }
152
+ const storedCredential = getStoredCredential();
153
+ const targetCredentialId = credentialId || storedCredential?.credentialId;
154
+ if (!targetCredentialId) {
155
+ throw new Error("No wallet passkey found");
156
+ }
157
+ const challenge = generateRandomBase64URL(32);
158
+ const prfSalt = getPRFSaltBase64URL();
159
+ const authenticationOptions = {
160
+ challenge,
161
+ rpId: window.location.hostname,
162
+ allowCredentials: [
163
+ {
164
+ id: targetCredentialId,
165
+ type: "public-key",
166
+ transports: storedCredential?.transports
167
+ }
168
+ ],
169
+ userVerification: "required",
170
+ timeout: 6e4,
171
+ extensions: {
172
+ // @ts-expect-error - PRF extension not in types
173
+ prf: {
174
+ eval: {
175
+ first: base64URLToArrayBuffer(prfSalt)
176
+ }
177
+ }
178
+ }
179
+ };
180
+ let authentication;
181
+ try {
182
+ authentication = await startAuthentication(authenticationOptions);
183
+ } catch (error) {
184
+ if (error instanceof Error && error.name === "NotAllowedError") {
185
+ return null;
186
+ }
187
+ throw error;
188
+ }
189
+ const prfResults = authentication.clientExtensionResults?.prf;
190
+ if (prfResults?.results?.first) {
191
+ const prfOutput = prfResults.results.first;
192
+ if (typeof prfOutput === "string") {
193
+ return arrayBufferToHex(base64URLToArrayBuffer(prfOutput));
194
+ }
195
+ return arrayBufferToHex(prfOutput);
196
+ }
197
+ console.warn("PRF not available, using fallback key derivation");
198
+ if (storedCredential) {
199
+ return deriveKeyFromCredential(targetCredentialId, storedCredential.userId);
200
+ }
201
+ throw new Error("Unable to derive encryption key");
202
+ }
203
+ async function deriveKeyFromCredential(credentialId, userId) {
204
+ const encoder = new TextEncoder();
205
+ const keyMaterial = await crypto.subtle.importKey(
206
+ "raw",
207
+ encoder.encode(credentialId),
208
+ "PBKDF2",
209
+ false,
210
+ ["deriveBits"]
211
+ );
212
+ const salt = encoder.encode(`chipi-wallet-${userId}`);
213
+ const derivedBits = await crypto.subtle.deriveBits(
214
+ {
215
+ name: "PBKDF2",
216
+ salt,
217
+ iterations: 1e5,
218
+ hash: "SHA-256"
219
+ },
220
+ keyMaterial,
221
+ 256
222
+ );
223
+ return arrayBufferToHex(derivedBits);
224
+ }
225
+ async function verifyWalletPasskey() {
226
+ try {
227
+ const encryptKey = await getWalletEncryptKey();
228
+ return encryptKey !== null;
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+ var isWebAuthnPRFSupported = isWebAuthnSupported;
234
+
235
+ export { createWalletPasskey, getStoredCredential, getWalletEncryptKey, hasWalletPasskey, isWebAuthnPRFSupported, isWebAuthnSupported, removeStoredCredential, verifyWalletPasskey };
236
+ //# sourceMappingURL=index.mjs.map
237
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/passkey.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/* eslint-disable no-undef */\n/**\n * Wallet Passkey Utilities\n *\n * This module provides WebAuthn passkey functionality with PRF (Pseudo-Random Function)\n * extension support for securely deriving encryption keys for wallet creation.\n *\n * Uses @simplewebauthn/browser for robust browser compatibility.\n */\n\nimport {\n startRegistration,\n startAuthentication,\n browserSupportsWebAuthn,\n} from \"@simplewebauthn/browser\";\nimport type {\n PublicKeyCredentialCreationOptionsJSON,\n PublicKeyCredentialRequestOptionsJSON,\n AuthenticationResponseJSON,\n RegistrationResponseJSON,\n} from \"@simplewebauthn/types\";\n\n// Salt used for PRF - this should be constant for your app\nconst WALLET_PRF_SALT_STRING = \"chipi-wallet-encryption-key-v1\";\n\n// Storage key for credential metadata\nconst WALLET_CREDENTIAL_KEY = \"chipi_wallet_passkey_credential\";\n\nexport interface WalletCredentialMetadata {\n credentialId: string;\n createdAt: string;\n userId: string;\n transports?: AuthenticatorTransport[];\n}\n\n/**\n * Generate a random base64url string for challenges\n */\nfunction generateRandomBase64URL(length: number = 32): string {\n const array = new Uint8Array(length);\n crypto.getRandomValues(array);\n return btoa(String.fromCharCode(...array))\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=/g, \"\");\n}\n\n/**\n * Convert string to base64url\n */\nfunction stringToBase64URL(str: string): string {\n return btoa(str).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n}\n\n/**\n * Convert ArrayBuffer to hex string\n */\nfunction arrayBufferToHex(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Convert base64url to ArrayBuffer\n */\nfunction base64URLToArrayBuffer(base64url: string): ArrayBuffer {\n const base64 = base64url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const paddedBase64 = base64.padEnd(\n base64.length + ((4 - (base64.length % 4)) % 4),\n \"=\"\n );\n const binary = atob(paddedBase64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Get PRF salt as base64url for SimpleWebAuthn\n */\nfunction getPRFSaltBase64URL(): string {\n return stringToBase64URL(WALLET_PRF_SALT_STRING);\n}\n\n/**\n * Check if WebAuthn is supported in this browser\n */\nexport function isWebAuthnSupported(): boolean {\n if (typeof window === \"undefined\") return false;\n return browserSupportsWebAuthn();\n}\n\n/**\n * Get stored credential metadata\n */\nexport function getStoredCredential(): WalletCredentialMetadata | null {\n if (typeof window === \"undefined\") return null;\n\n const stored = localStorage.getItem(WALLET_CREDENTIAL_KEY);\n if (!stored) return null;\n\n try {\n return JSON.parse(stored) as WalletCredentialMetadata;\n } catch {\n return null;\n }\n}\n\n/**\n * Store credential metadata\n */\nfunction storeCredential(metadata: WalletCredentialMetadata): void {\n localStorage.setItem(WALLET_CREDENTIAL_KEY, JSON.stringify(metadata));\n}\n\n/**\n * Remove stored credential\n */\nexport function removeStoredCredential(): void {\n localStorage.removeItem(WALLET_CREDENTIAL_KEY);\n}\n\n/**\n * Check if a wallet passkey already exists\n */\nexport function hasWalletPasskey(): boolean {\n return getStoredCredential() !== null;\n}\n\nexport interface CreateWalletPasskeyResult {\n encryptKey: string;\n credentialId: string;\n prfSupported: boolean;\n}\n\n/**\n * Create a new wallet passkey with PRF extension\n */\nexport async function createWalletPasskey(\n userId: string,\n userName: string\n): Promise<CreateWalletPasskeyResult> {\n if (!isWebAuthnSupported()) {\n throw new Error(\"WebAuthn is not supported in this browser\");\n }\n\n const challenge = generateRandomBase64URL(32);\n const userIdBase64 = stringToBase64URL(userId);\n const prfSalt = getPRFSaltBase64URL();\n\n // Build registration options for SimpleWebAuthn\n const registrationOptions: PublicKeyCredentialCreationOptionsJSON = {\n challenge,\n rp: {\n name: \"Chipi Wallet\",\n id: window.location.hostname,\n },\n user: {\n id: userIdBase64,\n name: userName,\n displayName: `${userName} - Wallet Key`,\n },\n pubKeyCredParams: [\n { alg: -7, type: \"public-key\" }, // ES256\n { alg: -257, type: \"public-key\" }, // RS256\n ],\n authenticatorSelection: {\n authenticatorAttachment: \"platform\",\n residentKey: \"required\",\n userVerification: \"required\",\n },\n timeout: 60000,\n attestation: \"none\",\n extensions: {\n // @ts-expect-error - PRF extension not in types yet\n prf: {\n eval: {\n first: base64URLToArrayBuffer(prfSalt),\n },\n },\n },\n };\n\n let registration: RegistrationResponseJSON;\n try {\n registration = await startRegistration(registrationOptions);\n } catch (error) {\n if (error instanceof Error) {\n if (error.name === \"NotAllowedError\") {\n throw new Error(\"Passkey creation was cancelled\");\n }\n if (error.name === \"InvalidStateError\") {\n throw new Error(\"A passkey already exists for this account\");\n }\n }\n throw error;\n }\n\n const credentialId = registration.id;\n\n // Check PRF results\n // @ts-expect-error - PRF extension not in types\n const prfResults = registration.clientExtensionResults?.prf;\n\n let encryptKey: string;\n let prfSupported = false;\n\n if (prfResults?.results?.first) {\n // PRF was evaluated during creation\n const prfOutput = prfResults.results.first;\n if (typeof prfOutput === \"string\") {\n encryptKey = arrayBufferToHex(base64URLToArrayBuffer(prfOutput));\n } else {\n encryptKey = arrayBufferToHex(prfOutput);\n }\n prfSupported = true;\n } else if (prfResults?.enabled) {\n // PRF is supported but needs assertion to get output\n const prfOutput = await getWalletEncryptKey(credentialId);\n if (!prfOutput) {\n throw new Error(\"Failed to derive encryption key from passkey\");\n }\n encryptKey = prfOutput;\n prfSupported = true;\n } else {\n // PRF not supported - use fallback derivation\n console.warn(\"PRF extension not supported, using fallback key derivation\");\n encryptKey = await deriveKeyFromCredential(credentialId, userId);\n prfSupported = false;\n }\n\n // Get transports if available\n const transports = registration.response.transports as\n | AuthenticatorTransport[]\n | undefined;\n\n // Store credential metadata\n storeCredential({\n credentialId,\n createdAt: new Date().toISOString(),\n userId,\n transports,\n });\n\n return {\n encryptKey,\n credentialId,\n prfSupported,\n };\n}\n\n/**\n * Get the encryption key by authenticating with the wallet passkey\n */\nexport async function getWalletEncryptKey(\n credentialId?: string\n): Promise<string | null> {\n if (!isWebAuthnSupported()) {\n throw new Error(\"WebAuthn is not supported in this browser\");\n }\n\n const storedCredential = getStoredCredential();\n const targetCredentialId = credentialId || storedCredential?.credentialId;\n\n if (!targetCredentialId) {\n throw new Error(\"No wallet passkey found\");\n }\n\n const challenge = generateRandomBase64URL(32);\n const prfSalt = getPRFSaltBase64URL();\n\n // Build authentication options\n const authenticationOptions: PublicKeyCredentialRequestOptionsJSON = {\n challenge,\n rpId: window.location.hostname,\n allowCredentials: [\n {\n id: targetCredentialId,\n type: \"public-key\",\n transports: storedCredential?.transports,\n },\n ],\n userVerification: \"required\",\n timeout: 60000,\n extensions: {\n // @ts-expect-error - PRF extension not in types\n prf: {\n eval: {\n first: base64URLToArrayBuffer(prfSalt),\n },\n },\n },\n };\n\n let authentication: AuthenticationResponseJSON;\n try {\n authentication = await startAuthentication(authenticationOptions);\n } catch (error) {\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n // User cancelled\n return null;\n }\n throw error;\n }\n\n // Check PRF results\n // @ts-expect-error - PRF extension not in types\n const prfResults = authentication.clientExtensionResults?.prf;\n\n if (prfResults?.results?.first) {\n const prfOutput = prfResults.results.first;\n if (typeof prfOutput === \"string\") {\n return arrayBufferToHex(base64URLToArrayBuffer(prfOutput));\n }\n return arrayBufferToHex(prfOutput);\n }\n\n // PRF not available, use fallback\n console.warn(\"PRF not available, using fallback key derivation\");\n if (storedCredential) {\n return deriveKeyFromCredential(targetCredentialId, storedCredential.userId);\n }\n\n throw new Error(\"Unable to derive encryption key\");\n}\n\n/**\n * Fallback key derivation when PRF is not supported\n */\nasync function deriveKeyFromCredential(\n credentialId: string,\n userId: string\n): Promise<string> {\n const encoder = new TextEncoder();\n const keyMaterial = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(credentialId),\n \"PBKDF2\",\n false,\n [\"deriveBits\"]\n );\n\n const salt = encoder.encode(`chipi-wallet-${userId}`);\n\n const derivedBits = await crypto.subtle.deriveBits(\n {\n name: \"PBKDF2\",\n salt,\n iterations: 100000,\n hash: \"SHA-256\",\n },\n keyMaterial,\n 256\n );\n\n return arrayBufferToHex(derivedBits);\n}\n\n/**\n * Verify that the stored passkey is still valid\n */\nexport async function verifyWalletPasskey(): Promise<boolean> {\n try {\n const encryptKey = await getWalletEncryptKey();\n return encryptKey !== null;\n } catch {\n return false;\n }\n}\n\n// Re-export for backwards compatibility\nexport const isWebAuthnPRFSupported = isWebAuthnSupported;\n"]}
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@chipi-stack/chipi-passkey",
3
+ "version": "0.1.0",
4
+ "description": "Chipi Passkey SDK - WebAuthn passkey utilities for secure wallet encryption",
5
+ "homepage": "https://github.com/chipi-pay/chipi-sdk",
6
+ "bugs": {
7
+ "url": "https://github.com/chipi-pay/chipi-sdk/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/chipi-pay/chipi-sdk.git",
12
+ "directory": "sdks/chipi-passkey"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Chipi Pay",
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "types": "./dist/index.d.mts",
20
+ "default": "./dist/index.mjs"
21
+ },
22
+ "require": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "./hooks": {
28
+ "import": {
29
+ "types": "./dist/hooks.d.mts",
30
+ "default": "./dist/hooks.mjs"
31
+ },
32
+ "require": {
33
+ "types": "./dist/hooks.d.ts",
34
+ "default": "./dist/hooks.js"
35
+ }
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "main": "./dist/index.js",
40
+ "module": "./dist/index.mjs",
41
+ "types": "./dist/index.d.ts",
42
+ "files": [
43
+ "dist",
44
+ "hooks"
45
+ ],
46
+ "dependencies": {
47
+ "@simplewebauthn/browser": "^10.0.0",
48
+ "@simplewebauthn/types": "^12.0.0",
49
+ "@chipi-stack/types": "^12.4.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.15.15",
53
+ "@types/react": "^18.3.0",
54
+ "eslint": "^9.33.0",
55
+ "prettier": "^3.0.0",
56
+ "react": "^18.3.0",
57
+ "rimraf": "^5.0.0",
58
+ "tsup": "^8.3.6",
59
+ "typescript": "^5.7.3"
60
+ },
61
+ "peerDependencies": {
62
+ "react": ">=18.0.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=18.17.0"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public"
69
+ },
70
+ "scripts": {
71
+ "build": "tsup",
72
+ "clean": "rimraf ./dist",
73
+ "dev": "tsup --watch",
74
+ "format": "prettier --write src/**/*.ts",
75
+ "format:check": "prettier --check src/**/*.ts",
76
+ "lint": "eslint src",
77
+ "typecheck": "tsc --noEmit"
78
+ }
79
+ }