ox 0.12.3 → 0.13.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/CHANGELOG.md +17 -0
- package/_cjs/core/P256.js +1 -1
- package/_cjs/core/P256.js.map +1 -1
- package/_cjs/core/WebAuthnP256.js +15 -256
- package/_cjs/core/WebAuthnP256.js.map +1 -1
- package/_cjs/core/WebCryptoP256.js +3 -1
- package/_cjs/core/WebCryptoP256.js.map +1 -1
- package/_cjs/core/internal/webauthn.js +5 -13
- package/_cjs/core/internal/webauthn.js.map +1 -1
- package/_cjs/index.docs.js +1 -0
- package/_cjs/index.docs.js.map +1 -1
- package/_cjs/tempo/TxEnvelopeTempo.js +19 -1
- package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_cjs/webauthn/Authentication.js +246 -0
- package/_cjs/webauthn/Authentication.js.map +1 -0
- package/_cjs/webauthn/Authenticator.js +55 -0
- package/_cjs/webauthn/Authenticator.js.map +1 -0
- package/_cjs/webauthn/Credential.js +53 -0
- package/_cjs/webauthn/Credential.js.map +1 -0
- package/_cjs/webauthn/Registration.js +349 -0
- package/_cjs/webauthn/Registration.js.map +1 -0
- package/_cjs/webauthn/Types.js +3 -0
- package/_cjs/webauthn/Types.js.map +1 -0
- package/_cjs/webauthn/index.js +9 -0
- package/_cjs/webauthn/index.js.map +1 -0
- package/_cjs/webauthn/internal/utils.js +53 -0
- package/_cjs/webauthn/internal/utils.js.map +1 -0
- package/_esm/core/P256.js +1 -1
- package/_esm/core/P256.js.map +1 -1
- package/_esm/core/WebAuthnP256.js +13 -261
- package/_esm/core/WebAuthnP256.js.map +1 -1
- package/_esm/core/WebCryptoP256.js +4 -1
- package/_esm/core/WebCryptoP256.js.map +1 -1
- package/_esm/core/internal/webauthn.js +5 -13
- package/_esm/core/internal/webauthn.js.map +1 -1
- package/_esm/erc8021/index.js +2 -2
- package/_esm/index.docs.js +1 -0
- package/_esm/index.docs.js.map +1 -1
- package/_esm/tempo/TransactionReceipt.js +1 -1
- package/_esm/tempo/TransactionRequest.js +1 -1
- package/_esm/tempo/TxEnvelopeTempo.js +20 -1
- package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_esm/webauthn/Authentication.js +453 -0
- package/_esm/webauthn/Authentication.js.map +1 -0
- package/_esm/webauthn/Authenticator.js +176 -0
- package/_esm/webauthn/Authenticator.js.map +1 -0
- package/_esm/webauthn/Credential.js +95 -0
- package/_esm/webauthn/Credential.js.map +1 -0
- package/_esm/webauthn/Registration.js +512 -0
- package/_esm/webauthn/Registration.js.map +1 -0
- package/_esm/webauthn/Types.js +2 -0
- package/_esm/webauthn/Types.js.map +1 -0
- package/_esm/webauthn/index.js +31 -0
- package/_esm/webauthn/index.js.map +1 -0
- package/_esm/webauthn/internal/utils.js +52 -0
- package/_esm/webauthn/internal/utils.js.map +1 -0
- package/_types/core/WebAuthnP256.d.ts +33 -208
- package/_types/core/WebAuthnP256.d.ts.map +1 -1
- package/_types/core/WebCryptoP256.d.ts +2 -0
- package/_types/core/WebCryptoP256.d.ts.map +1 -1
- package/_types/core/internal/webauthn.d.ts +2 -110
- package/_types/core/internal/webauthn.d.ts.map +1 -1
- package/_types/erc8021/index.d.ts +2 -2
- package/_types/index.docs.d.ts +1 -0
- package/_types/index.docs.d.ts.map +1 -1
- package/_types/tempo/Transaction.d.ts +2 -2
- package/_types/tempo/TransactionReceipt.d.ts +2 -2
- package/_types/tempo/TransactionRequest.d.ts +2 -2
- package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/_types/webauthn/Authentication.d.ts +324 -0
- package/_types/webauthn/Authentication.d.ts.map +1 -0
- package/_types/webauthn/Authenticator.d.ts +182 -0
- package/_types/webauthn/Authenticator.d.ts.map +1 -0
- package/_types/webauthn/Credential.d.ts +77 -0
- package/_types/webauthn/Credential.d.ts.map +1 -0
- package/_types/webauthn/Registration.d.ts +308 -0
- package/_types/webauthn/Registration.d.ts.map +1 -0
- package/_types/webauthn/Types.d.ts +106 -0
- package/_types/webauthn/Types.d.ts.map +1 -0
- package/_types/webauthn/index.d.ts +33 -0
- package/_types/webauthn/index.d.ts.map +1 -0
- package/_types/webauthn/internal/utils.d.ts +17 -0
- package/_types/webauthn/internal/utils.d.ts.map +1 -0
- package/core/P256.ts +1 -1
- package/core/WebAuthnP256.ts +37 -582
- package/core/WebCryptoP256.ts +6 -1
- package/core/internal/webauthn.ts +6 -165
- package/erc8021/index.ts +2 -2
- package/index.docs.ts +1 -0
- package/package.json +31 -1
- package/tempo/Transaction.ts +2 -2
- package/tempo/TransactionReceipt.ts +2 -2
- package/tempo/TransactionRequest.ts +2 -2
- package/tempo/TxEnvelopeTempo.test.ts +6 -0
- package/tempo/TxEnvelopeTempo.ts +22 -2
- package/version.ts +1 -1
- package/webauthn/Authentication/package.json +6 -0
- package/webauthn/Authentication.ts +673 -0
- package/webauthn/Authenticator/package.json +6 -0
- package/webauthn/Authenticator.ts +259 -0
- package/webauthn/Credential/package.json +6 -0
- package/webauthn/Credential.ts +146 -0
- package/webauthn/Registration/package.json +6 -0
- package/webauthn/Registration.ts +805 -0
- package/webauthn/Types/package.json +6 -0
- package/webauthn/Types.ts +158 -0
- package/webauthn/index.ts +38 -0
- package/webauthn/internal/utils.ts +63 -0
- package/webauthn/package.json +6 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import * as Base64 from '../core/Base64.js'
|
|
2
|
+
import * as Bytes from '../core/Bytes.js'
|
|
3
|
+
import * as Errors from '../core/Errors.js'
|
|
4
|
+
import * as Hash from '../core/Hash.js'
|
|
5
|
+
import * as Hex from '../core/Hex.js'
|
|
6
|
+
import type { OneOf } from '../core/internal/types.js'
|
|
7
|
+
import * as internal from '../core/internal/webauthn.js'
|
|
8
|
+
import * as P256 from '../core/P256.js'
|
|
9
|
+
import type * as PublicKey from '../core/PublicKey.js'
|
|
10
|
+
import * as Signature from '../core/Signature.js'
|
|
11
|
+
import { getAuthenticatorData, getClientDataJSON } from './Authenticator.js'
|
|
12
|
+
import type * as Credential_ from './Credential.js'
|
|
13
|
+
import {
|
|
14
|
+
base64UrlOptions,
|
|
15
|
+
bufferSourceToBytes,
|
|
16
|
+
bytesToArrayBuffer,
|
|
17
|
+
deserializeExtensions,
|
|
18
|
+
responseKeys,
|
|
19
|
+
serializeExtensions,
|
|
20
|
+
} from './internal/utils.js'
|
|
21
|
+
import type * as Types from './Types.js'
|
|
22
|
+
|
|
23
|
+
/** Response from a WebAuthn authentication ceremony. */
|
|
24
|
+
export type Response<serialized extends boolean = false> = {
|
|
25
|
+
id: string
|
|
26
|
+
metadata: Credential_.SignMetadata
|
|
27
|
+
raw: Types.PublicKeyCredential<serialized>
|
|
28
|
+
signature: serialized extends true ? Hex.Hex : Signature.Signature<false>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Deserializes credential request options that can be passed to
|
|
33
|
+
* `navigator.credentials.get()`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts twoslash
|
|
37
|
+
* import { Authentication } from 'ox/webauthn'
|
|
38
|
+
*
|
|
39
|
+
* const options = Authentication.getOptions({
|
|
40
|
+
* challenge: '0xdeadbeef',
|
|
41
|
+
* })
|
|
42
|
+
* const serialized = Authentication.serializeOptions(options)
|
|
43
|
+
*
|
|
44
|
+
* // ... send to server and back ...
|
|
45
|
+
*
|
|
46
|
+
* const deserialized = Authentication.deserializeOptions(serialized) // [!code focus]
|
|
47
|
+
* const credential = await window.navigator.credentials.get(deserialized)
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @param options - The serialized credential request options.
|
|
51
|
+
* @returns The deserialized credential request options.
|
|
52
|
+
*/
|
|
53
|
+
export function deserializeOptions(
|
|
54
|
+
options: Types.CredentialRequestOptions<true>,
|
|
55
|
+
): Types.CredentialRequestOptions {
|
|
56
|
+
const { publicKey, ...rest } = options
|
|
57
|
+
if (!publicKey) return { ...rest }
|
|
58
|
+
|
|
59
|
+
const { allowCredentials, challenge, extensions, ...publicKeyRest } =
|
|
60
|
+
publicKey
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...rest,
|
|
64
|
+
publicKey: {
|
|
65
|
+
...publicKeyRest,
|
|
66
|
+
challenge: Bytes.fromHex(challenge),
|
|
67
|
+
...(allowCredentials && {
|
|
68
|
+
allowCredentials: allowCredentials.map(({ id, ...rest }) => ({
|
|
69
|
+
...rest,
|
|
70
|
+
id: Base64.toBytes(id),
|
|
71
|
+
})),
|
|
72
|
+
}),
|
|
73
|
+
...(extensions && {
|
|
74
|
+
extensions: deserializeExtensions(extensions),
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export declare namespace deserializeOptions {
|
|
81
|
+
type ErrorType = Base64.toBytes.ErrorType | Errors.GlobalErrorType
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Deserializes a serialized authentication response.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts twoslash
|
|
89
|
+
* import { Authentication } from 'ox/webauthn'
|
|
90
|
+
*
|
|
91
|
+
* const response = Authentication.deserializeResponse({ // [!code focus]
|
|
92
|
+
* id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
|
|
93
|
+
* metadata: { // [!code focus]
|
|
94
|
+
* authenticatorData: '0x49960de5...', // [!code focus]
|
|
95
|
+
* clientDataJSON: '{"type":"webauthn.get",...}', // [!code focus]
|
|
96
|
+
* challengeIndex: 23, // [!code focus]
|
|
97
|
+
* typeIndex: 1, // [!code focus]
|
|
98
|
+
* userVerificationRequired: true, // [!code focus]
|
|
99
|
+
* }, // [!code focus]
|
|
100
|
+
* raw: { // [!code focus]
|
|
101
|
+
* id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
|
|
102
|
+
* type: 'public-key', // [!code focus]
|
|
103
|
+
* authenticatorAttachment: 'platform', // [!code focus]
|
|
104
|
+
* rawId: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
|
|
105
|
+
* response: { clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0' }, // [!code focus]
|
|
106
|
+
* }, // [!code focus]
|
|
107
|
+
* signature: '0x...', // [!code focus]
|
|
108
|
+
* }) // [!code focus]
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @param response - The serialized authentication response.
|
|
112
|
+
* @returns The deserialized authentication response.
|
|
113
|
+
*/
|
|
114
|
+
export function deserializeResponse(response: Response<true>): Response {
|
|
115
|
+
const { id, metadata, raw, signature } = response
|
|
116
|
+
|
|
117
|
+
const rawResponse: Record<string, ArrayBuffer> = {}
|
|
118
|
+
for (const [key, value] of Object.entries(raw.response))
|
|
119
|
+
rawResponse[key] = bytesToArrayBuffer(Base64.toBytes(value))
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
id,
|
|
123
|
+
metadata,
|
|
124
|
+
raw: {
|
|
125
|
+
id: raw.id,
|
|
126
|
+
type: raw.type,
|
|
127
|
+
authenticatorAttachment: raw.authenticatorAttachment,
|
|
128
|
+
rawId: bytesToArrayBuffer(Base64.toBytes(raw.rawId)),
|
|
129
|
+
response: rawResponse as unknown as Types.AuthenticatorResponse,
|
|
130
|
+
getClientExtensionResults: () => ({}),
|
|
131
|
+
},
|
|
132
|
+
signature: Signature.from(signature),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export declare namespace deserializeResponse {
|
|
137
|
+
type ErrorType =
|
|
138
|
+
| Base64.toBytes.ErrorType
|
|
139
|
+
| Signature.from.ErrorType
|
|
140
|
+
| Errors.GlobalErrorType
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Returns the request options to sign a challenge with the Web Authentication API.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts twoslash
|
|
148
|
+
* import { Authentication } from 'ox/webauthn'
|
|
149
|
+
*
|
|
150
|
+
* const options = Authentication.getOptions({
|
|
151
|
+
* challenge: '0xdeadbeef',
|
|
152
|
+
* })
|
|
153
|
+
*
|
|
154
|
+
* const credential = await window.navigator.credentials.get(options)
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* @param options - Options.
|
|
158
|
+
* @returns The credential request options.
|
|
159
|
+
*/
|
|
160
|
+
export function getOptions(
|
|
161
|
+
options: getOptions.Options,
|
|
162
|
+
): Types.CredentialRequestOptions {
|
|
163
|
+
const {
|
|
164
|
+
credentialId,
|
|
165
|
+
challenge,
|
|
166
|
+
extensions,
|
|
167
|
+
rpId = window.location.hostname,
|
|
168
|
+
userVerification = 'required',
|
|
169
|
+
} = options
|
|
170
|
+
return {
|
|
171
|
+
publicKey: {
|
|
172
|
+
...(credentialId
|
|
173
|
+
? {
|
|
174
|
+
allowCredentials: Array.isArray(credentialId)
|
|
175
|
+
? credentialId.map((id) => ({
|
|
176
|
+
id: Base64.toBytes(id),
|
|
177
|
+
type: 'public-key',
|
|
178
|
+
}))
|
|
179
|
+
: [
|
|
180
|
+
{
|
|
181
|
+
id: Base64.toBytes(credentialId),
|
|
182
|
+
type: 'public-key',
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
}
|
|
186
|
+
: {}),
|
|
187
|
+
challenge: Bytes.fromHex(challenge),
|
|
188
|
+
...(extensions && { extensions }),
|
|
189
|
+
rpId,
|
|
190
|
+
userVerification,
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export declare namespace getOptions {
|
|
196
|
+
type Options = {
|
|
197
|
+
/** The credential ID to use. */
|
|
198
|
+
credentialId?: string | string[] | undefined
|
|
199
|
+
/** The challenge to sign. */
|
|
200
|
+
challenge: Hex.Hex
|
|
201
|
+
/** List of Web Authentication API credentials to use during creation or authentication. */
|
|
202
|
+
extensions?:
|
|
203
|
+
| Types.PublicKeyCredentialRequestOptions['extensions']
|
|
204
|
+
| undefined
|
|
205
|
+
/** The relying party identifier to use. */
|
|
206
|
+
rpId?: Types.PublicKeyCredentialRequestOptions['rpId'] | undefined
|
|
207
|
+
/** The user verification requirement. */
|
|
208
|
+
userVerification?:
|
|
209
|
+
| Types.PublicKeyCredentialRequestOptions['userVerification']
|
|
210
|
+
| undefined
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type ErrorType =
|
|
214
|
+
| Bytes.fromHex.ErrorType
|
|
215
|
+
| Base64.toBytes.ErrorType
|
|
216
|
+
| Errors.GlobalErrorType
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Constructs the final digest that was signed and computed by the authenticator. This payload includes
|
|
221
|
+
* the cryptographic `challenge`, as well as authenticator metadata (`authenticatorData` + `clientDataJSON`).
|
|
222
|
+
* This value can be also used with raw P256 verification (such as `P256.verify` or
|
|
223
|
+
* `WebCryptoP256.verify`).
|
|
224
|
+
*
|
|
225
|
+
* :::warning
|
|
226
|
+
*
|
|
227
|
+
* This function is mainly for testing purposes or for manually constructing
|
|
228
|
+
* signing payloads. In most cases you will not need this function and
|
|
229
|
+
* instead use `Authentication.sign`.
|
|
230
|
+
*
|
|
231
|
+
* :::
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts twoslash
|
|
235
|
+
* import { Authentication } from 'ox/webauthn'
|
|
236
|
+
* import { WebCryptoP256 } from 'ox'
|
|
237
|
+
*
|
|
238
|
+
* const { metadata, payload } = Authentication.getSignPayload({ // [!code focus]
|
|
239
|
+
* challenge: '0xdeadbeef', // [!code focus]
|
|
240
|
+
* }) // [!code focus]
|
|
241
|
+
*
|
|
242
|
+
* const { publicKey, privateKey } = await WebCryptoP256.createKeyPair()
|
|
243
|
+
*
|
|
244
|
+
* const signature = await WebCryptoP256.sign({
|
|
245
|
+
* payload,
|
|
246
|
+
* privateKey,
|
|
247
|
+
* })
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* @param options - Options to construct the signing payload.
|
|
251
|
+
* @returns The signing payload.
|
|
252
|
+
*/
|
|
253
|
+
export function getSignPayload(
|
|
254
|
+
options: getSignPayload.Options,
|
|
255
|
+
): getSignPayload.ReturnType {
|
|
256
|
+
const {
|
|
257
|
+
challenge,
|
|
258
|
+
crossOrigin,
|
|
259
|
+
extraClientData,
|
|
260
|
+
flag,
|
|
261
|
+
origin,
|
|
262
|
+
rpId,
|
|
263
|
+
signCount,
|
|
264
|
+
userVerification = 'required',
|
|
265
|
+
} = options
|
|
266
|
+
|
|
267
|
+
const authenticatorData = getAuthenticatorData({
|
|
268
|
+
flag,
|
|
269
|
+
rpId,
|
|
270
|
+
signCount,
|
|
271
|
+
})
|
|
272
|
+
const clientDataJSON = getClientDataJSON({
|
|
273
|
+
challenge,
|
|
274
|
+
crossOrigin,
|
|
275
|
+
extraClientData,
|
|
276
|
+
origin,
|
|
277
|
+
})
|
|
278
|
+
const clientDataJSONHash = Hash.sha256(Hex.fromString(clientDataJSON))
|
|
279
|
+
|
|
280
|
+
const challengeIndex = clientDataJSON.indexOf('"challenge"')
|
|
281
|
+
const typeIndex = clientDataJSON.indexOf('"type"')
|
|
282
|
+
|
|
283
|
+
const metadata = {
|
|
284
|
+
authenticatorData,
|
|
285
|
+
clientDataJSON,
|
|
286
|
+
challengeIndex,
|
|
287
|
+
typeIndex,
|
|
288
|
+
userVerificationRequired: userVerification === 'required',
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const payload = Hex.concat(authenticatorData, clientDataJSONHash)
|
|
292
|
+
|
|
293
|
+
return { metadata, payload }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export declare namespace getSignPayload {
|
|
297
|
+
type Options = {
|
|
298
|
+
/** The challenge to sign. */
|
|
299
|
+
challenge: Hex.Hex
|
|
300
|
+
/** If set to `true`, it means that the calling context is an `<iframe>` that is not same origin with its ancestor frames. */
|
|
301
|
+
crossOrigin?: boolean | undefined
|
|
302
|
+
/** Additional client data to include in the client data JSON. */
|
|
303
|
+
extraClientData?: Record<string, unknown> | undefined
|
|
304
|
+
/** If set to `true`, the payload will be hashed before being returned. */
|
|
305
|
+
hash?: boolean | undefined
|
|
306
|
+
/** A bitfield that indicates various attributes that were asserted by the authenticator. [Read more](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data#flags) */
|
|
307
|
+
flag?: number | undefined
|
|
308
|
+
/** The fully qualified origin of the relying party which has been given by the client/browser to the authenticator. */
|
|
309
|
+
origin?: string | undefined
|
|
310
|
+
/** The [Relying Party ID](https://w3c.github.io/webauthn/#relying-party-identifier) that the credential is scoped to. */
|
|
311
|
+
rpId?: Types.PublicKeyCredentialRequestOptions['rpId'] | undefined
|
|
312
|
+
/** A signature counter, if supported by the authenticator (set to 0 otherwise). */
|
|
313
|
+
signCount?: number | undefined
|
|
314
|
+
/** The user verification requirement that the authenticator will enforce. */
|
|
315
|
+
userVerification?:
|
|
316
|
+
| Types.PublicKeyCredentialRequestOptions['userVerification']
|
|
317
|
+
| undefined
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
type ReturnType = {
|
|
321
|
+
metadata: Credential_.SignMetadata
|
|
322
|
+
payload: Hex.Hex
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
type ErrorType =
|
|
326
|
+
| Hash.sha256.ErrorType
|
|
327
|
+
| Hex.concat.ErrorType
|
|
328
|
+
| Hex.fromString.ErrorType
|
|
329
|
+
| Errors.GlobalErrorType
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Serializes credential request options into a JSON-serializable
|
|
334
|
+
* format, converting `BufferSource` fields to base64url strings.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```ts twoslash
|
|
338
|
+
* import { Authentication } from 'ox/webauthn'
|
|
339
|
+
*
|
|
340
|
+
* const options = Authentication.getOptions({
|
|
341
|
+
* challenge: '0xdeadbeef',
|
|
342
|
+
* })
|
|
343
|
+
*
|
|
344
|
+
* const serialized = Authentication.serializeOptions(options) // [!code focus]
|
|
345
|
+
*
|
|
346
|
+
* // `serialized` is JSON-serializable — send it to a server, store it, etc.
|
|
347
|
+
* const json = JSON.stringify(serialized)
|
|
348
|
+
* ```
|
|
349
|
+
*
|
|
350
|
+
* @param options - The credential request options to serialize.
|
|
351
|
+
* @returns The serialized credential request options.
|
|
352
|
+
*/
|
|
353
|
+
export function serializeOptions(
|
|
354
|
+
options: Types.CredentialRequestOptions,
|
|
355
|
+
): Types.CredentialRequestOptions<true> {
|
|
356
|
+
const { publicKey, signal: _, ...rest } = options
|
|
357
|
+
if (!publicKey) return { ...rest }
|
|
358
|
+
|
|
359
|
+
const { allowCredentials, challenge, extensions, ...publicKeyRest } =
|
|
360
|
+
publicKey
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
...rest,
|
|
364
|
+
publicKey: {
|
|
365
|
+
...publicKeyRest,
|
|
366
|
+
challenge: Hex.fromBytes(bufferSourceToBytes(challenge)),
|
|
367
|
+
...(allowCredentials && {
|
|
368
|
+
allowCredentials: allowCredentials.map(({ id, ...rest }) => ({
|
|
369
|
+
...rest,
|
|
370
|
+
id: Base64.fromBytes(bufferSourceToBytes(id), base64UrlOptions),
|
|
371
|
+
})),
|
|
372
|
+
}),
|
|
373
|
+
...(extensions && {
|
|
374
|
+
extensions: serializeExtensions(extensions),
|
|
375
|
+
}),
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export declare namespace serializeOptions {
|
|
381
|
+
type ErrorType = Base64.fromBytes.ErrorType | Errors.GlobalErrorType
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Serializes an authentication response into a JSON-serializable
|
|
386
|
+
* format, converting `BufferSource` fields to base64url strings
|
|
387
|
+
* and the signature to a hex string.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```ts twoslash
|
|
391
|
+
* import { Authentication } from 'ox/webauthn'
|
|
392
|
+
*
|
|
393
|
+
* const response = await Authentication.sign({
|
|
394
|
+
* challenge: '0xdeadbeef',
|
|
395
|
+
* })
|
|
396
|
+
*
|
|
397
|
+
* const serialized = Authentication.serializeResponse(response) // [!code focus]
|
|
398
|
+
*
|
|
399
|
+
* // `serialized` is JSON-serializable — send it to a server, store it, etc.
|
|
400
|
+
* const json = JSON.stringify(serialized)
|
|
401
|
+
* ```
|
|
402
|
+
*
|
|
403
|
+
* @param response - The authentication response to serialize.
|
|
404
|
+
* @returns The serialized authentication response.
|
|
405
|
+
*/
|
|
406
|
+
export function serializeResponse(response: Response): Response<true> {
|
|
407
|
+
const { id, metadata, raw, signature } = response
|
|
408
|
+
|
|
409
|
+
const rawResponse = {} as Record<string, string>
|
|
410
|
+
for (const key of responseKeys) {
|
|
411
|
+
const value = (raw.response as unknown as Record<string, unknown>)[key]
|
|
412
|
+
if (value instanceof ArrayBuffer)
|
|
413
|
+
rawResponse[key] = Base64.fromBytes(
|
|
414
|
+
new Uint8Array(value),
|
|
415
|
+
base64UrlOptions,
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
id,
|
|
421
|
+
metadata,
|
|
422
|
+
raw: {
|
|
423
|
+
id: raw.id,
|
|
424
|
+
type: raw.type,
|
|
425
|
+
authenticatorAttachment: raw.authenticatorAttachment,
|
|
426
|
+
rawId: Base64.fromBytes(bufferSourceToBytes(raw.rawId), base64UrlOptions),
|
|
427
|
+
response: rawResponse as unknown as Types.AuthenticatorResponse<true>,
|
|
428
|
+
},
|
|
429
|
+
signature: Signature.toHex(signature),
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export declare namespace serializeResponse {
|
|
434
|
+
type ErrorType =
|
|
435
|
+
| Base64.fromBytes.ErrorType
|
|
436
|
+
| Signature.toHex.ErrorType
|
|
437
|
+
| Errors.GlobalErrorType
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Signs a challenge using a stored WebAuthn P256 Credential. If no Credential is provided,
|
|
442
|
+
* a prompt will be displayed for the user to select an existing Credential
|
|
443
|
+
* that was previously registered.
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```ts twoslash
|
|
447
|
+
* import { Registration, Authentication } from 'ox/webauthn'
|
|
448
|
+
*
|
|
449
|
+
* const credential = await Registration.create({
|
|
450
|
+
* name: 'Example',
|
|
451
|
+
* })
|
|
452
|
+
*
|
|
453
|
+
* const { metadata, signature } = await Authentication.sign({ // [!code focus]
|
|
454
|
+
* credentialId: credential.id, // [!code focus]
|
|
455
|
+
* challenge: '0xdeadbeef', // [!code focus]
|
|
456
|
+
* }) // [!code focus]
|
|
457
|
+
* // @log: {
|
|
458
|
+
* // @log: metadata: {
|
|
459
|
+
* // @log: authenticatorData: '0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
|
|
460
|
+
* // @log: clientDataJSON: '{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
|
|
461
|
+
* // @log: challengeIndex: 23,
|
|
462
|
+
* // @log: typeIndex: 1,
|
|
463
|
+
* // @log: userVerificationRequired: true,
|
|
464
|
+
* // @log: },
|
|
465
|
+
* // @log: signature: { r: 51231...4215n, s: 12345...6789n },
|
|
466
|
+
* // @log: }
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* @param options - Options.
|
|
470
|
+
* @returns The signature.
|
|
471
|
+
*/
|
|
472
|
+
export async function sign(options: sign.Options): Promise<sign.ReturnType> {
|
|
473
|
+
const {
|
|
474
|
+
getFn = window.navigator.credentials.get.bind(window.navigator.credentials),
|
|
475
|
+
...rest
|
|
476
|
+
} = options
|
|
477
|
+
const requestOptions =
|
|
478
|
+
'publicKey' in rest
|
|
479
|
+
? (rest as Types.CredentialRequestOptions)
|
|
480
|
+
: getOptions(rest as never)
|
|
481
|
+
try {
|
|
482
|
+
const credential = (await getFn(
|
|
483
|
+
requestOptions as never,
|
|
484
|
+
)) as Types.PublicKeyCredential
|
|
485
|
+
if (!credential) throw new SignFailedError()
|
|
486
|
+
const response = credential.response as AuthenticatorAssertionResponse
|
|
487
|
+
|
|
488
|
+
const clientDataJSON = String.fromCharCode(
|
|
489
|
+
...new Uint8Array(response.clientDataJSON),
|
|
490
|
+
)
|
|
491
|
+
const challengeIndex = clientDataJSON.indexOf('"challenge"')
|
|
492
|
+
const typeIndex = clientDataJSON.indexOf('"type"')
|
|
493
|
+
|
|
494
|
+
const signature = internal.parseAsn1Signature(
|
|
495
|
+
new Uint8Array(response.signature),
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
id: credential.id,
|
|
500
|
+
metadata: {
|
|
501
|
+
authenticatorData: Hex.fromBytes(
|
|
502
|
+
new Uint8Array(response.authenticatorData),
|
|
503
|
+
),
|
|
504
|
+
clientDataJSON,
|
|
505
|
+
challengeIndex,
|
|
506
|
+
typeIndex,
|
|
507
|
+
userVerificationRequired:
|
|
508
|
+
requestOptions.publicKey!.userVerification === 'required',
|
|
509
|
+
},
|
|
510
|
+
signature,
|
|
511
|
+
raw: credential,
|
|
512
|
+
}
|
|
513
|
+
} catch (error) {
|
|
514
|
+
throw new SignFailedError({
|
|
515
|
+
cause: error as Error,
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export declare namespace sign {
|
|
521
|
+
type Options = OneOf<
|
|
522
|
+
| (getOptions.Options & {
|
|
523
|
+
/**
|
|
524
|
+
* Credential request function. Useful for environments that do not support
|
|
525
|
+
* the WebAuthn API natively (i.e. React Native or testing environments).
|
|
526
|
+
*
|
|
527
|
+
* @default window.navigator.credentials.get
|
|
528
|
+
*/
|
|
529
|
+
getFn?:
|
|
530
|
+
| ((
|
|
531
|
+
options?: Types.CredentialRequestOptions | undefined,
|
|
532
|
+
) => Promise<Types.Credential | null>)
|
|
533
|
+
| undefined
|
|
534
|
+
})
|
|
535
|
+
| Types.CredentialRequestOptions
|
|
536
|
+
>
|
|
537
|
+
|
|
538
|
+
type ReturnType = Response
|
|
539
|
+
|
|
540
|
+
type ErrorType =
|
|
541
|
+
| Hex.fromBytes.ErrorType
|
|
542
|
+
| getOptions.ErrorType
|
|
543
|
+
| Errors.GlobalErrorType
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Thrown when a WebAuthn P256 credential request fails. */
|
|
547
|
+
export class SignFailedError extends Errors.BaseError<Error> {
|
|
548
|
+
override readonly name = 'Authentication.SignFailedError'
|
|
549
|
+
|
|
550
|
+
constructor({ cause }: { cause?: Error | undefined } = {}) {
|
|
551
|
+
super('Failed to request credential.', {
|
|
552
|
+
cause,
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Verifies a signature using the Credential's public key and the challenge which was signed.
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```ts twoslash
|
|
562
|
+
* import { Registration, Authentication } from 'ox/webauthn'
|
|
563
|
+
*
|
|
564
|
+
* const credential = await Registration.create({
|
|
565
|
+
* name: 'Example',
|
|
566
|
+
* })
|
|
567
|
+
*
|
|
568
|
+
* const { metadata, signature } = await Authentication.sign({
|
|
569
|
+
* credentialId: credential.id,
|
|
570
|
+
* challenge: '0xdeadbeef',
|
|
571
|
+
* })
|
|
572
|
+
*
|
|
573
|
+
* const result = Authentication.verify({ // [!code focus]
|
|
574
|
+
* metadata, // [!code focus]
|
|
575
|
+
* challenge: '0xdeadbeef', // [!code focus]
|
|
576
|
+
* publicKey: credential.publicKey, // [!code focus]
|
|
577
|
+
* signature, // [!code focus]
|
|
578
|
+
* }) // [!code focus]
|
|
579
|
+
* // @log: true
|
|
580
|
+
* ```
|
|
581
|
+
*
|
|
582
|
+
* @param options - Options.
|
|
583
|
+
* @returns Whether the signature is valid.
|
|
584
|
+
*/
|
|
585
|
+
export function verify(options: verify.Options): boolean {
|
|
586
|
+
const { challenge, metadata, origin, publicKey, rpId, signature } = options
|
|
587
|
+
const { authenticatorData, clientDataJSON, userVerificationRequired } =
|
|
588
|
+
metadata
|
|
589
|
+
|
|
590
|
+
const authenticatorDataBytes = Bytes.fromHex(authenticatorData)
|
|
591
|
+
|
|
592
|
+
// Check length of `authenticatorData`.
|
|
593
|
+
if (authenticatorDataBytes.length < 37) return false
|
|
594
|
+
|
|
595
|
+
// If rpId is provided, validate the rpIdHash in authenticatorData.
|
|
596
|
+
if (rpId !== undefined) {
|
|
597
|
+
const rpIdHash = authenticatorDataBytes.slice(0, 32)
|
|
598
|
+
const expectedRpIdHash = Hash.sha256(Hex.fromString(rpId), { as: 'Bytes' })
|
|
599
|
+
if (!Bytes.isEqual(rpIdHash, expectedRpIdHash)) return false
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const flag = authenticatorDataBytes[32]!
|
|
603
|
+
|
|
604
|
+
// Verify that the UP bit of the flags in authData is set.
|
|
605
|
+
if ((flag & 0x01) !== 0x01) return false
|
|
606
|
+
|
|
607
|
+
// If user verification was determined to be required, verify that
|
|
608
|
+
// the UV bit of the flags in authData is set. Otherwise, ignore the
|
|
609
|
+
// value of the UV flag.
|
|
610
|
+
if (userVerificationRequired && (flag & 0x04) !== 0x04) return false
|
|
611
|
+
|
|
612
|
+
// If the BE bit of the flags in authData is not set, verify that
|
|
613
|
+
// the BS bit is not set.
|
|
614
|
+
if ((flag & 0x08) !== 0x08 && (flag & 0x10) === 0x10) return false
|
|
615
|
+
|
|
616
|
+
// Parse clientDataJSON for validation.
|
|
617
|
+
const clientData = JSON.parse(clientDataJSON)
|
|
618
|
+
|
|
619
|
+
// Verify that response is for an authentication assertion.
|
|
620
|
+
if (clientData.type !== 'webauthn.get') return false
|
|
621
|
+
|
|
622
|
+
// Validate the challenge in the clientDataJSON.
|
|
623
|
+
if (
|
|
624
|
+
!clientData.challenge ||
|
|
625
|
+
Hex.fromBytes(Base64.toBytes(clientData.challenge)) !== challenge
|
|
626
|
+
)
|
|
627
|
+
return false
|
|
628
|
+
|
|
629
|
+
// If origin is provided, validate origin.
|
|
630
|
+
if (origin !== undefined) {
|
|
631
|
+
const origins = Array.isArray(origin) ? origin : [origin]
|
|
632
|
+
if (!origins.includes(clientData.origin)) return false
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const clientDataJSONHash = Hash.sha256(Bytes.fromString(clientDataJSON), {
|
|
636
|
+
as: 'Bytes',
|
|
637
|
+
})
|
|
638
|
+
const payload = Bytes.concat(authenticatorDataBytes, clientDataJSONHash)
|
|
639
|
+
|
|
640
|
+
return P256.verify({
|
|
641
|
+
hash: true,
|
|
642
|
+
payload,
|
|
643
|
+
publicKey,
|
|
644
|
+
signature,
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export declare namespace verify {
|
|
649
|
+
type Options = {
|
|
650
|
+
/** The challenge to verify. */
|
|
651
|
+
challenge: Hex.Hex
|
|
652
|
+
/** The public key to verify the signature with. */
|
|
653
|
+
publicKey: PublicKey.PublicKey
|
|
654
|
+
/** The signature to verify. */
|
|
655
|
+
signature: Signature.Signature<false>
|
|
656
|
+
/** The metadata to verify the signature with. */
|
|
657
|
+
metadata: Credential_.SignMetadata
|
|
658
|
+
/** Expected origin(s). If provided, the `clientDataJSON` origin will be validated. */
|
|
659
|
+
origin?: string | string[] | undefined
|
|
660
|
+
/** Expected relying party ID. If provided, the `rpIdHash` in `authenticatorData` will be validated. */
|
|
661
|
+
rpId?: string | undefined
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
type ErrorType =
|
|
665
|
+
| Base64.toBytes.ErrorType
|
|
666
|
+
| Bytes.concat.ErrorType
|
|
667
|
+
| Bytes.fromHex.ErrorType
|
|
668
|
+
| Bytes.isEqual.ErrorType
|
|
669
|
+
| Hash.sha256.ErrorType
|
|
670
|
+
| Hex.fromString.ErrorType
|
|
671
|
+
| P256.verify.ErrorType
|
|
672
|
+
| Errors.GlobalErrorType
|
|
673
|
+
}
|