@organigram/passkey-wallet 0.1.6
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/.organigram-docs-cache/passkey-wallet/api-extractor.json +35 -0
- package/.organigram-docs-cache/passkey-wallet/api-model/passkey-wallet.api.json +3166 -0
- package/.organigram-docs-cache/passkey-wallet/entry/index.d.ts +9 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/05732508 +0 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/166b6dbf +0 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/41c20e23 +0 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/node-compile-cache/v24.12.0-arm64-cf738c9d-501/7111ea58 +0 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-1822dfbab9819e9f6a93485fe3da5dbf3745311c +1 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-45a65cfdc7a50c53817c1dad3954ae3d2d2c35fb +1 -0
- package/.organigram-docs-cache/passkey-wallet/tmp/tsx-501/17844-fee5290b05398fe9e3b878deefd22383744f1526 +1 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/createorganigrampasskeywalletinput.yml +17 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/identitypasskeycapabilities.yml +16 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeycapabilities.yml +18 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeyprovider.yml +16 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/organigrampasskeyprovideractions.yml +20 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyprfunavailableerror.yml +28 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyproviderevent.yml +11 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyproviderlistener.yml +11 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyregistrationresult.yml +15 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultdecrypterror.yml +28 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultenvelopedata.yml +18 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeyvaultenvelopeinput.yml +19 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletapiclient.yml +33 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletcapabilities.yml +11 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletlogincapabilities.yml +14 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletregistrationcapabilities.yml +18 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/passkeywalletvaultpayload.yml +18 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet/unlockedpasskeywallet.yml +21 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/passkey-wallet.yml +870 -0
- package/.organigram-docs-cache/passkey-wallet/yaml/toc.yml +43 -0
- package/.turbo/turbo-build.log +6 -0
- package/.turbo/turbo-docs$colon$generate.log +118 -0
- package/.turbo/turbo-lint.log +13 -0
- package/.turbo/turbo-test.log +152 -0
- package/.turbo/turbo-typecheck.log +5 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE.md +21 -0
- package/README.md +427 -0
- package/__mocks__/organigram-js.ts +19 -0
- package/__mocks__/wagmi.ts +1 -0
- package/dist/crypto.d.ts +39 -0
- package/dist/crypto.js +95 -0
- package/dist/eip1193.d.ts +24 -0
- package/dist/eip1193.js +103 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/rainbowkit.d.ts +14 -0
- package/dist/rainbowkit.js +128 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +17 -0
- package/dist/vault.d.ts +10 -0
- package/dist/vault.js +41 -0
- package/dist/wallet.d.ts +15 -0
- package/dist/wallet.js +30 -0
- package/dist/webauthn-client.d.ts +73 -0
- package/dist/webauthn-client.js +234 -0
- package/dist/webauthn-server.d.ts +62 -0
- package/dist/webauthn-server.js +133 -0
- package/jest.config.ts +32 -0
- package/package.json +61 -0
- package/src/crypto.test.ts +69 -0
- package/src/crypto.ts +193 -0
- package/src/eip1193.ts +169 -0
- package/src/errors.ts +16 -0
- package/src/index.ts +4 -0
- package/src/rainbowkit.test.ts +42 -0
- package/src/rainbowkit.ts +171 -0
- package/src/types.ts +82 -0
- package/src/vault.test.ts +23 -0
- package/src/vault.ts +77 -0
- package/src/wallet.ts +61 -0
- package/src/webauthn-client.test.ts +103 -0
- package/src/webauthn-client.ts +430 -0
- package/src/webauthn-server.test.ts +43 -0
- package/src/webauthn-server.ts +258 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
|
|
2
|
+
import { base64UrlToBytes, decryptPasskeyVaultSecret, derivePasskeyVaultKey, encryptPasskeyVaultSecret } from './crypto';
|
|
3
|
+
import { PasskeyPrfUnavailableError } from './errors';
|
|
4
|
+
import { createNewPasskeyWalletVault, createUnlockedPasskeyWallet } from './wallet';
|
|
5
|
+
import { parsePasskeyWalletVaultPayload, serializePasskeyWalletVaultPayload } from './vault';
|
|
6
|
+
export { parsePasskeyWalletVaultPayload, serializePasskeyWalletVaultPayload };
|
|
7
|
+
const readJson = async (response) => {
|
|
8
|
+
const body = (await response.json().catch(() => null));
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
throw new Error(body?.error ?? `Request failed with status ${response.status}`);
|
|
11
|
+
}
|
|
12
|
+
return body;
|
|
13
|
+
};
|
|
14
|
+
const postJson = async (path, body) => {
|
|
15
|
+
const response = await fetch(path, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json'
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify(body)
|
|
21
|
+
});
|
|
22
|
+
return await readJson(response);
|
|
23
|
+
};
|
|
24
|
+
export const createFetchPasskeyWalletApiClient = (basePath = '/api/auth/passkey') => ({
|
|
25
|
+
registerOptions: async (input) => await postJson(`${basePath}/register/options`, input),
|
|
26
|
+
registerVerify: async (input) => await postJson(`${basePath}/register/verify`, input),
|
|
27
|
+
unlockOptions: async () => await postJson(`${basePath}/unlock/options`, {}),
|
|
28
|
+
unlockVerify: async (input) => await postJson(`${basePath}/unlock/verify`, input)
|
|
29
|
+
});
|
|
30
|
+
const hydrateSerializedPrfValue = (value) => {
|
|
31
|
+
if (value == null)
|
|
32
|
+
return value;
|
|
33
|
+
if (typeof value === 'string')
|
|
34
|
+
return base64UrlToBytes(value);
|
|
35
|
+
if (value instanceof ArrayBuffer)
|
|
36
|
+
return new Uint8Array(value);
|
|
37
|
+
if (ArrayBuffer.isView(value)) {
|
|
38
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return new Uint8Array(value);
|
|
42
|
+
if (typeof value === 'object') {
|
|
43
|
+
const entries = Object.entries(value);
|
|
44
|
+
if (entries.every(([key, entryValue]) => /^\d+$/.test(key) && typeof entryValue === 'number')) {
|
|
45
|
+
return new Uint8Array(entries
|
|
46
|
+
.sort(([left], [right]) => Number(left) - Number(right))
|
|
47
|
+
.map(([, entryValue]) => entryValue));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
const hydratePrfValueSet = (valueSet) => {
|
|
53
|
+
if (valueSet == null)
|
|
54
|
+
return valueSet;
|
|
55
|
+
return {
|
|
56
|
+
...valueSet,
|
|
57
|
+
first: hydrateSerializedPrfValue(valueSet.first),
|
|
58
|
+
...(valueSet.second != null
|
|
59
|
+
? {
|
|
60
|
+
second: hydrateSerializedPrfValue(valueSet.second)
|
|
61
|
+
}
|
|
62
|
+
: {})
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
export const hydratePasskeyPrfOptions = (options) => {
|
|
66
|
+
const extensions = options.extensions;
|
|
67
|
+
const prf = extensions?.prf;
|
|
68
|
+
if (prf == null)
|
|
69
|
+
return options;
|
|
70
|
+
return {
|
|
71
|
+
...options,
|
|
72
|
+
extensions: {
|
|
73
|
+
...extensions,
|
|
74
|
+
prf: {
|
|
75
|
+
...prf,
|
|
76
|
+
...(prf.eval != null ? { eval: hydratePrfValueSet(prf.eval) } : {}),
|
|
77
|
+
...(prf.evalByCredential != null
|
|
78
|
+
? {
|
|
79
|
+
evalByCredential: Object.fromEntries(Object.entries(prf.evalByCredential).map(([credentialId, valueSet]) => [
|
|
80
|
+
credentialId,
|
|
81
|
+
hydratePrfValueSet(valueSet)
|
|
82
|
+
]))
|
|
83
|
+
}
|
|
84
|
+
: {})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const getPasskeyPrfOutput = (response) => {
|
|
90
|
+
const first = response.clientExtensionResults?.prf?.results?.first;
|
|
91
|
+
if (first == null || first === '') {
|
|
92
|
+
throw new PasskeyPrfUnavailableError();
|
|
93
|
+
}
|
|
94
|
+
return typeof first === 'string'
|
|
95
|
+
? base64UrlToBytes(first)
|
|
96
|
+
: new Uint8Array(first);
|
|
97
|
+
};
|
|
98
|
+
export async function registerPasskeyCredentialEnvelope({ api, address, vaultPayload, email, name }) {
|
|
99
|
+
const { options } = await api.registerOptions({
|
|
100
|
+
address,
|
|
101
|
+
email,
|
|
102
|
+
name
|
|
103
|
+
});
|
|
104
|
+
const registrationResponse = await startRegistration({
|
|
105
|
+
optionsJSON: hydratePasskeyPrfOptions(options)
|
|
106
|
+
});
|
|
107
|
+
const prfOutput = getPasskeyPrfOutput(registrationResponse);
|
|
108
|
+
const salt = crypto.getRandomValues(new Uint8Array(32));
|
|
109
|
+
const key = await derivePasskeyVaultKey({
|
|
110
|
+
prfOutput,
|
|
111
|
+
salt
|
|
112
|
+
});
|
|
113
|
+
const envelope = await encryptPasskeyVaultSecret({
|
|
114
|
+
plaintext: serializePasskeyWalletVaultPayload(vaultPayload),
|
|
115
|
+
key,
|
|
116
|
+
salt
|
|
117
|
+
});
|
|
118
|
+
return await api.registerVerify({
|
|
119
|
+
response: registrationResponse,
|
|
120
|
+
envelope: {
|
|
121
|
+
address,
|
|
122
|
+
encryptedMnemonic: envelope.ciphertext,
|
|
123
|
+
salt: envelope.salt,
|
|
124
|
+
nonce: envelope.nonce,
|
|
125
|
+
algorithm: envelope.algorithm,
|
|
126
|
+
keyVersion: envelope.keyVersion
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export const registerPasskeyWallet = async ({ api, capabilities }) => {
|
|
131
|
+
const { address, vaultPayload } = await createNewPasskeyWalletVault({
|
|
132
|
+
capabilities
|
|
133
|
+
});
|
|
134
|
+
const credential = await registerPasskeyCredentialEnvelope({
|
|
135
|
+
api,
|
|
136
|
+
address,
|
|
137
|
+
vaultPayload,
|
|
138
|
+
email: capabilities.identity?.email ?? null,
|
|
139
|
+
name: capabilities.name ?? capabilities.identity?.email ?? null
|
|
140
|
+
});
|
|
141
|
+
return createUnlockedPasskeyWallet({
|
|
142
|
+
address: credential.address,
|
|
143
|
+
credentialId: credential.credentialId,
|
|
144
|
+
vaultPayload
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
export const unlockPasskeyWallet = async ({ api }) => {
|
|
148
|
+
const { options, hasCredentials } = await api.unlockOptions();
|
|
149
|
+
if (!hasCredentials)
|
|
150
|
+
return null;
|
|
151
|
+
const authenticationResponse = await startAuthentication({
|
|
152
|
+
optionsJSON: hydratePasskeyPrfOptions(options)
|
|
153
|
+
});
|
|
154
|
+
const prfOutput = getPasskeyPrfOutput(authenticationResponse);
|
|
155
|
+
const result = await api.unlockVerify({
|
|
156
|
+
response: authenticationResponse
|
|
157
|
+
});
|
|
158
|
+
const key = await derivePasskeyVaultKey({
|
|
159
|
+
prfOutput,
|
|
160
|
+
salt: base64UrlToBytes(result.envelope.salt)
|
|
161
|
+
});
|
|
162
|
+
const vaultPlaintext = await decryptPasskeyVaultSecret({
|
|
163
|
+
envelope: result.envelope,
|
|
164
|
+
key
|
|
165
|
+
});
|
|
166
|
+
const vaultPayload = parsePasskeyWalletVaultPayload(vaultPlaintext);
|
|
167
|
+
const wallet = createUnlockedPasskeyWallet({
|
|
168
|
+
address: result.address,
|
|
169
|
+
credentialId: result.credentialId,
|
|
170
|
+
vaultPayload
|
|
171
|
+
});
|
|
172
|
+
if (wallet.account.address.toLowerCase() !== result.address.toLowerCase()) {
|
|
173
|
+
throw new Error('Passkey wallet envelope does not match its address.');
|
|
174
|
+
}
|
|
175
|
+
return wallet;
|
|
176
|
+
};
|
|
177
|
+
export const exportPasskeyWalletRecoveryPhrase = async ({ api, expectedAddress }) => {
|
|
178
|
+
const wallet = await unlockPasskeyWallet({ api });
|
|
179
|
+
if (wallet == null) {
|
|
180
|
+
throw new Error('No passkey wallet is available to export.');
|
|
181
|
+
}
|
|
182
|
+
if (wallet.address.toLowerCase() !== expectedAddress.toLowerCase()) {
|
|
183
|
+
throw new Error('Selected passkey does not unlock this wallet.');
|
|
184
|
+
}
|
|
185
|
+
return wallet.recoveryPhrase;
|
|
186
|
+
};
|
|
187
|
+
export const registerAdditionalPasskeyCredential = async ({ api, wallet, name }) => await registerPasskeyCredentialEnvelope({
|
|
188
|
+
api,
|
|
189
|
+
address: wallet.address,
|
|
190
|
+
vaultPayload: {
|
|
191
|
+
version: 1,
|
|
192
|
+
recoveryPhrase: wallet.recoveryPhrase,
|
|
193
|
+
userEncryptionPrivateKey: wallet.userEncryptionPrivateKey,
|
|
194
|
+
userEncryptionPublicKey: wallet.userEncryptionPublicKey,
|
|
195
|
+
userEncryptionKeyVersion: wallet.userEncryptionKeyVersion
|
|
196
|
+
},
|
|
197
|
+
name: name ?? 'Backup passkey'
|
|
198
|
+
});
|
|
199
|
+
export const isPasskeyCredentialUnavailableError = (error) => {
|
|
200
|
+
if (typeof error !== 'object' || error == null)
|
|
201
|
+
return false;
|
|
202
|
+
const { name, message } = error;
|
|
203
|
+
const normalizedName = typeof name === 'string' ? name.toLowerCase() : '';
|
|
204
|
+
const normalizedMessage = typeof message === 'string' ? message.toLowerCase() : '';
|
|
205
|
+
return (normalizedName === 'notallowederror' ||
|
|
206
|
+
normalizedName === 'invalidstateerror' ||
|
|
207
|
+
normalizedMessage.includes('no credentials') ||
|
|
208
|
+
normalizedMessage.includes('not allowed') ||
|
|
209
|
+
normalizedMessage.includes('could not be completed'));
|
|
210
|
+
};
|
|
211
|
+
export const unlockOrCreatePasskeyWallet = async ({ api, capabilities }) => {
|
|
212
|
+
if (typeof window === 'undefined' || window.PublicKeyCredential == null) {
|
|
213
|
+
throw new Error('Passkeys are not available in this browser.');
|
|
214
|
+
}
|
|
215
|
+
if (capabilities.method !== 'register') {
|
|
216
|
+
try {
|
|
217
|
+
const wallet = await unlockPasskeyWallet({ api });
|
|
218
|
+
if (wallet != null)
|
|
219
|
+
return wallet;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (!isPasskeyCredentialUnavailableError(error)) {
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return await registerPasskeyWallet({
|
|
228
|
+
api,
|
|
229
|
+
capabilities: {
|
|
230
|
+
...capabilities,
|
|
231
|
+
method: 'register'
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
2
|
+
import { type PasskeyVaultEnvelopeData } from './crypto';
|
|
3
|
+
export declare const passkeyChallengeTtlMs: number;
|
|
4
|
+
export type PasskeyVaultEnvelopeInput = {
|
|
5
|
+
address: string;
|
|
6
|
+
encryptedMnemonic: string;
|
|
7
|
+
salt: string;
|
|
8
|
+
nonce: string;
|
|
9
|
+
algorithm: string;
|
|
10
|
+
keyVersion: number;
|
|
11
|
+
};
|
|
12
|
+
export declare const validatePasskeyVaultEnvelopeInput: (input: PasskeyVaultEnvelopeInput) => PasskeyVaultEnvelopeInput & {
|
|
13
|
+
address: `0x${string}`;
|
|
14
|
+
};
|
|
15
|
+
export declare const createPasskeyChallengeExpiry: () => Date;
|
|
16
|
+
export declare const getWebAuthnClientDataChallenge: (response: unknown) => string;
|
|
17
|
+
export declare const createPasskeyRegistrationOptions: ({ rpId, userAddress, email }: {
|
|
18
|
+
rpId: string;
|
|
19
|
+
userAddress?: string | null;
|
|
20
|
+
email?: string | null;
|
|
21
|
+
}) => Promise<import("@simplewebauthn/server").PublicKeyCredentialCreationOptionsJSON>;
|
|
22
|
+
type VerifiedPasskeyRegistrationResponse = Awaited<ReturnType<typeof verifyRegistrationResponse>> & {
|
|
23
|
+
registrationInfo: NonNullable<Awaited<ReturnType<typeof verifyRegistrationResponse>>['registrationInfo']>;
|
|
24
|
+
};
|
|
25
|
+
export declare const verifyPasskeyRegistration: ({ response, expectedChallenge, expectedOrigin, expectedRpId, envelope }: {
|
|
26
|
+
response: unknown;
|
|
27
|
+
expectedChallenge: string;
|
|
28
|
+
expectedOrigin: string;
|
|
29
|
+
expectedRpId: string;
|
|
30
|
+
envelope: PasskeyVaultEnvelopeInput;
|
|
31
|
+
}) => Promise<{
|
|
32
|
+
verification: VerifiedPasskeyRegistrationResponse;
|
|
33
|
+
vaultEnvelope: PasskeyVaultEnvelopeInput & {
|
|
34
|
+
address: `0x${string}`;
|
|
35
|
+
};
|
|
36
|
+
}>;
|
|
37
|
+
export declare const createPasskeyAuthenticationOptions: ({ rpId, credentials }: {
|
|
38
|
+
rpId: string;
|
|
39
|
+
credentials?: Array<{
|
|
40
|
+
credentialId: string;
|
|
41
|
+
transports: string[];
|
|
42
|
+
}>;
|
|
43
|
+
}) => Promise<import("@simplewebauthn/server").PublicKeyCredentialRequestOptionsJSON>;
|
|
44
|
+
export declare const verifyPasskeyAuthentication: ({ response, expectedChallenge, expectedOrigin, expectedRpId, credential }: {
|
|
45
|
+
response: unknown;
|
|
46
|
+
expectedChallenge: string;
|
|
47
|
+
expectedOrigin: string;
|
|
48
|
+
expectedRpId: string;
|
|
49
|
+
credential: {
|
|
50
|
+
credentialId: string;
|
|
51
|
+
publicKey: string;
|
|
52
|
+
signCount: number;
|
|
53
|
+
};
|
|
54
|
+
}) => Promise<Awaited<ReturnType<typeof verifyAuthenticationResponse>>>;
|
|
55
|
+
export declare const toPasskeyVaultEnvelopeData: ({ encryptedMnemonic, salt, nonce, algorithm, keyVersion }: {
|
|
56
|
+
encryptedMnemonic: string;
|
|
57
|
+
salt: string;
|
|
58
|
+
nonce: string;
|
|
59
|
+
algorithm: string;
|
|
60
|
+
keyVersion: number;
|
|
61
|
+
}) => PasskeyVaultEnvelopeData;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
2
|
+
import { getAddress, isAddress } from 'viem';
|
|
3
|
+
import { base64UrlToBytes, passkeyVaultAlgorithm, passkeyVaultKeyVersion } from './crypto';
|
|
4
|
+
export const passkeyChallengeTtlMs = 5 * 60 * 1000;
|
|
5
|
+
export const validatePasskeyVaultEnvelopeInput = (input) => {
|
|
6
|
+
if (!isAddress(input.address)) {
|
|
7
|
+
throw new Error('Passkey vault envelope requires a valid wallet address.');
|
|
8
|
+
}
|
|
9
|
+
if (input.encryptedMnemonic.trim() === '') {
|
|
10
|
+
throw new Error('Passkey vault envelope requires encrypted recovery phrase data.');
|
|
11
|
+
}
|
|
12
|
+
if (input.salt.trim() === '') {
|
|
13
|
+
throw new Error('Passkey vault envelope requires a salt.');
|
|
14
|
+
}
|
|
15
|
+
if (input.nonce.trim() === '') {
|
|
16
|
+
throw new Error('Passkey vault envelope requires a nonce.');
|
|
17
|
+
}
|
|
18
|
+
if (input.algorithm !== passkeyVaultAlgorithm) {
|
|
19
|
+
throw new Error('Passkey vault envelope algorithm is not supported.');
|
|
20
|
+
}
|
|
21
|
+
if (input.keyVersion !== passkeyVaultKeyVersion) {
|
|
22
|
+
throw new Error('Passkey vault envelope key version is not supported.');
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
...input,
|
|
26
|
+
address: getAddress(input.address)
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export const createPasskeyChallengeExpiry = () => new Date(Date.now() + passkeyChallengeTtlMs);
|
|
30
|
+
export const getWebAuthnClientDataChallenge = (response) => {
|
|
31
|
+
const clientDataJSON = response?.response?.clientDataJSON;
|
|
32
|
+
if (typeof clientDataJSON !== 'string' || clientDataJSON === '') {
|
|
33
|
+
throw new Error('WebAuthn response is missing client data.');
|
|
34
|
+
}
|
|
35
|
+
const clientData = JSON.parse(new TextDecoder().decode(base64UrlToBytes(clientDataJSON)));
|
|
36
|
+
if (typeof clientData.challenge !== 'string' || clientData.challenge === '') {
|
|
37
|
+
throw new Error('WebAuthn response is missing its challenge.');
|
|
38
|
+
}
|
|
39
|
+
return clientData.challenge;
|
|
40
|
+
};
|
|
41
|
+
export const createPasskeyRegistrationOptions = async ({ rpId, userAddress, email }) => await generateRegistrationOptions({
|
|
42
|
+
rpName: 'Organigram',
|
|
43
|
+
rpID: rpId,
|
|
44
|
+
userName: email ?? userAddress ?? 'Organigram passkey wallet',
|
|
45
|
+
userDisplayName: email ?? userAddress ?? 'Organigram passkey wallet',
|
|
46
|
+
attestationType: 'none',
|
|
47
|
+
authenticatorSelection: {
|
|
48
|
+
residentKey: 'required',
|
|
49
|
+
userVerification: 'required'
|
|
50
|
+
},
|
|
51
|
+
extensions: {
|
|
52
|
+
prf: {
|
|
53
|
+
eval: {
|
|
54
|
+
first: new Uint8Array(32)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
export const verifyPasskeyRegistration = async ({ response, expectedChallenge, expectedOrigin, expectedRpId, envelope }) => {
|
|
60
|
+
const vaultEnvelope = validatePasskeyVaultEnvelopeInput(envelope);
|
|
61
|
+
const verification = await verifyRegistrationResponse({
|
|
62
|
+
response: response,
|
|
63
|
+
expectedChallenge,
|
|
64
|
+
expectedOrigin,
|
|
65
|
+
expectedRPID: expectedRpId,
|
|
66
|
+
requireUserVerification: true
|
|
67
|
+
});
|
|
68
|
+
if (!verification.verified || verification.registrationInfo == null) {
|
|
69
|
+
throw new Error('Passkey registration was not verified.');
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
verification: verification,
|
|
73
|
+
vaultEnvelope
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export const createPasskeyAuthenticationOptions = async ({ rpId, credentials = [] }) => {
|
|
77
|
+
const isDiscoverableAuthentication = credentials.length === 0;
|
|
78
|
+
return await generateAuthenticationOptions({
|
|
79
|
+
rpID: rpId,
|
|
80
|
+
userVerification: 'required',
|
|
81
|
+
...(!isDiscoverableAuthentication
|
|
82
|
+
? {
|
|
83
|
+
allowCredentials: credentials.map(credential => ({
|
|
84
|
+
id: credential.credentialId,
|
|
85
|
+
transports: credential.transports
|
|
86
|
+
}))
|
|
87
|
+
}
|
|
88
|
+
: {}),
|
|
89
|
+
extensions: {
|
|
90
|
+
prf: isDiscoverableAuthentication
|
|
91
|
+
? {
|
|
92
|
+
eval: {
|
|
93
|
+
first: new Uint8Array(32)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
: {
|
|
97
|
+
evalByCredential: Object.fromEntries(credentials.map(credential => [
|
|
98
|
+
credential.credentialId,
|
|
99
|
+
{
|
|
100
|
+
first: new Uint8Array(32)
|
|
101
|
+
}
|
|
102
|
+
]))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
export const verifyPasskeyAuthentication = async ({ response, expectedChallenge, expectedOrigin, expectedRpId, credential }) => await verifyAuthenticationResponse({
|
|
108
|
+
response: response,
|
|
109
|
+
expectedChallenge,
|
|
110
|
+
expectedOrigin,
|
|
111
|
+
expectedRPID: expectedRpId,
|
|
112
|
+
credential: {
|
|
113
|
+
id: credential.credentialId,
|
|
114
|
+
publicKey: Buffer.from(credential.publicKey, 'base64url'),
|
|
115
|
+
counter: credential.signCount
|
|
116
|
+
},
|
|
117
|
+
requireUserVerification: true
|
|
118
|
+
});
|
|
119
|
+
export const toPasskeyVaultEnvelopeData = ({ encryptedMnemonic, salt, nonce, algorithm, keyVersion }) => {
|
|
120
|
+
if (algorithm !== passkeyVaultAlgorithm) {
|
|
121
|
+
throw new Error('Passkey vault envelope algorithm is not supported.');
|
|
122
|
+
}
|
|
123
|
+
if (keyVersion !== passkeyVaultKeyVersion) {
|
|
124
|
+
throw new Error('Passkey vault envelope key version is not supported.');
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
algorithm,
|
|
128
|
+
keyVersion,
|
|
129
|
+
ciphertext: encryptedMnemonic,
|
|
130
|
+
salt,
|
|
131
|
+
nonce
|
|
132
|
+
};
|
|
133
|
+
};
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Config } from 'jest'
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
preset: 'ts-jest/presets/default-esm',
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
watchman: false,
|
|
7
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
8
|
+
moduleNameMapper: {
|
|
9
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
10
|
+
'^@organigram/js$': '<rootDir>/__mocks__/organigram-js.ts',
|
|
11
|
+
'^wagmi$': '<rootDir>/__mocks__/wagmi.ts'
|
|
12
|
+
},
|
|
13
|
+
transform: {
|
|
14
|
+
'^.+\\.tsx?$': [
|
|
15
|
+
'ts-jest',
|
|
16
|
+
{
|
|
17
|
+
tsconfig: {
|
|
18
|
+
module: 'ESNext',
|
|
19
|
+
moduleResolution: 'bundler',
|
|
20
|
+
target: 'ESNext',
|
|
21
|
+
resolveJsonModule: true,
|
|
22
|
+
esModuleInterop: true,
|
|
23
|
+
types: ['jest', 'node']
|
|
24
|
+
},
|
|
25
|
+
diagnostics: false,
|
|
26
|
+
useESM: true
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default config
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@organigram/passkey-wallet",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Organigram Passkey Wallet.",
|
|
5
|
+
"author": "Organigram (https://organigram.ai)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "ssh://git@github.com:organigram/organigram.git",
|
|
10
|
+
"directory": "packages/passkey-wallet"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.js",
|
|
19
|
+
"./crypto": "./dist/crypto.js",
|
|
20
|
+
"./vault": "./dist/vault.js",
|
|
21
|
+
"./webauthn-client": "./dist/webauthn-client.js",
|
|
22
|
+
"./webauthn-server": "./dist/webauthn-server.js",
|
|
23
|
+
"./wallet": "./dist/wallet.js",
|
|
24
|
+
"./eip1193": "./dist/eip1193.js",
|
|
25
|
+
"./rainbowkit": "./dist/rainbowkit.js",
|
|
26
|
+
"./errors": "./dist/errors.js"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@organigram/js": "workspace:^",
|
|
31
|
+
"@rainbow-me/rainbowkit": "^2.2.8",
|
|
32
|
+
"@simplewebauthn/browser": "^13.3.0",
|
|
33
|
+
"@simplewebauthn/server": "^13.3.2",
|
|
34
|
+
"viem": "^2.18.2",
|
|
35
|
+
"wagmi": "^2.16.3 || ^3.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@organigram/js": "^0.1.6",
|
|
39
|
+
"@simplewebauthn/browser": "^13.3.0",
|
|
40
|
+
"@simplewebauthn/server": "^13.3.2",
|
|
41
|
+
"viem": "2.55.2"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@rainbow-me/rainbowkit": "^2.2.8",
|
|
45
|
+
"@tsconfig/recommended": "^1.0.7",
|
|
46
|
+
"@types/jest": "^29.5.12",
|
|
47
|
+
"@types/node": "^20.19.37",
|
|
48
|
+
"jest": "^29.7.0",
|
|
49
|
+
"ts-jest": "^29.2.3",
|
|
50
|
+
"typescript": "^5.5.4",
|
|
51
|
+
"wagmi": "^2.16.3"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"docs:generate": "env-cmd --silent tsx ../docs/scripts/generatePackageTypeScriptReference.ts . passkey-wallet",
|
|
55
|
+
"build": "tsc -p tsconfig.build.json --pretty",
|
|
56
|
+
"test": "jest",
|
|
57
|
+
"typecheck": "tsc --noEmit --incremental false",
|
|
58
|
+
"lint": "eslint ."
|
|
59
|
+
},
|
|
60
|
+
"gitHead": "a2bf9b898c0c5f0dbcf80f2b9f705d5200519bb5"
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decryptPasskeyVaultSecret,
|
|
3
|
+
derivePasskeyVaultKey,
|
|
4
|
+
encryptPasskeyVaultSecret,
|
|
5
|
+
isPasskeyPrfSupported
|
|
6
|
+
} from './crypto'
|
|
7
|
+
|
|
8
|
+
describe('passkey vault crypto', () => {
|
|
9
|
+
it('encrypts and decrypts vault plaintext with a PRF-derived key', async () => {
|
|
10
|
+
const salt = new Uint8Array(32).fill(2)
|
|
11
|
+
const nonce = new Uint8Array(12).fill(3)
|
|
12
|
+
const key = await derivePasskeyVaultKey({
|
|
13
|
+
prfOutput: new Uint8Array(32).fill(1),
|
|
14
|
+
salt
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const envelope = await encryptPasskeyVaultSecret({
|
|
18
|
+
plaintext: 'secret vault payload',
|
|
19
|
+
key,
|
|
20
|
+
salt,
|
|
21
|
+
nonce
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
await expect(
|
|
25
|
+
decryptPasskeyVaultSecret({ envelope, key })
|
|
26
|
+
).resolves.toBe('secret vault payload')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('rejects vault envelopes decrypted with the wrong passkey output', async () => {
|
|
30
|
+
const salt = new Uint8Array(32).fill(13)
|
|
31
|
+
const key = await derivePasskeyVaultKey({
|
|
32
|
+
prfOutput: new Uint8Array(32).fill(1),
|
|
33
|
+
salt
|
|
34
|
+
})
|
|
35
|
+
const wrongKey = await derivePasskeyVaultKey({
|
|
36
|
+
prfOutput: new Uint8Array(32).fill(2),
|
|
37
|
+
salt
|
|
38
|
+
})
|
|
39
|
+
const envelope = await encryptPasskeyVaultSecret({
|
|
40
|
+
plaintext: 'secret vault payload',
|
|
41
|
+
key,
|
|
42
|
+
salt
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
await expect(
|
|
46
|
+
decryptPasskeyVaultSecret({ envelope, key: wrongKey })
|
|
47
|
+
).rejects.toThrow(/Unable to decrypt passkey wallet/)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('detects PRF support from client extension results', async () => {
|
|
51
|
+
await expect(isPasskeyPrfSupported({ credential: null })).resolves.toBe(
|
|
52
|
+
false
|
|
53
|
+
)
|
|
54
|
+
await expect(
|
|
55
|
+
isPasskeyPrfSupported({
|
|
56
|
+
credential: {
|
|
57
|
+
getClientExtensionResults: () => ({ prf: { enabled: true } })
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
).resolves.toBe(true)
|
|
61
|
+
await expect(
|
|
62
|
+
isPasskeyPrfSupported({
|
|
63
|
+
credential: {
|
|
64
|
+
getClientExtensionResults: () => ({ prf: { enabled: false } })
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
).resolves.toBe(false)
|
|
68
|
+
})
|
|
69
|
+
})
|