@mentaproject/signer-react-native 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +343 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/signer.d.ts +161 -0
- package/dist/signer.d.ts.map +1 -0
- package/dist/signer.js +378 -0
- package/dist/signer.js.map +1 -0
- package/dist/types/index.d.ts +117 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/base64url.d.ts +75 -0
- package/dist/utils/base64url.d.ts.map +1 -0
- package/dist/utils/base64url.js +142 -0
- package/dist/utils/base64url.js.map +1 -0
- package/dist/utils/cose.d.ts +98 -0
- package/dist/utils/cose.d.ts.map +1 -0
- package/dist/utils/cose.js +259 -0
- package/dist/utils/cose.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/passkey.d.ts +108 -0
- package/dist/utils/passkey.d.ts.map +1 -0
- package/dist/utils/passkey.js +296 -0
- package/dist/utils/passkey.js.map +1 -0
- package/package.json +71 -0
- package/src/index.ts +59 -0
- package/src/signer.ts +499 -0
- package/src/types/index.ts +140 -0
- package/src/utils/base64url.ts +161 -0
- package/src/utils/cose.ts +356 -0
- package/src/utils/index.ts +40 -0
- package/src/utils/passkey.ts +392 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { type Hex, encodeAbiParameters, keccak256 } from "viem";
|
|
2
|
+
import type {
|
|
3
|
+
AuthenticatorData,
|
|
4
|
+
WebAuthnSignature,
|
|
5
|
+
SignatureEncodingFormat,
|
|
6
|
+
} from "../types/index.js";
|
|
7
|
+
import {
|
|
8
|
+
base64UrlToBytes,
|
|
9
|
+
base64UrlToHex,
|
|
10
|
+
bytesToHex,
|
|
11
|
+
hexToBase64Url,
|
|
12
|
+
padHex,
|
|
13
|
+
} from "./base64url.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when signature parsing or encoding fails.
|
|
17
|
+
*/
|
|
18
|
+
export class SignatureError extends Error {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "SignatureError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parses the authenticator data from a WebAuthn assertion.
|
|
27
|
+
*
|
|
28
|
+
* @param authenticatorDataB64 - Base64URL encoded authenticator data
|
|
29
|
+
* @returns Parsed authenticator data structure
|
|
30
|
+
*/
|
|
31
|
+
export function parseAuthenticatorDataFromAssertion(
|
|
32
|
+
authenticatorDataB64: string
|
|
33
|
+
): AuthenticatorData {
|
|
34
|
+
const bytes = base64UrlToBytes(authenticatorDataB64);
|
|
35
|
+
|
|
36
|
+
if (bytes.length < 37) {
|
|
37
|
+
throw new SignatureError(
|
|
38
|
+
"Authenticator data too short: minimum 37 bytes required"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// rpIdHash (32 bytes)
|
|
43
|
+
const rpIdHash = bytesToHex(bytes.slice(0, 32));
|
|
44
|
+
|
|
45
|
+
// flags (1 byte)
|
|
46
|
+
const flags = bytes[32];
|
|
47
|
+
|
|
48
|
+
// signCount (4 bytes, big endian)
|
|
49
|
+
const signCount =
|
|
50
|
+
(bytes[33] << 24) | (bytes[34] << 16) | (bytes[35] << 8) | bytes[36];
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
rpIdHash,
|
|
54
|
+
flags,
|
|
55
|
+
signCount,
|
|
56
|
+
raw: bytesToHex(bytes),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parses a DER-encoded ECDSA signature into r and s components.
|
|
62
|
+
* WebAuthn signatures are DER-encoded ASN.1 sequences.
|
|
63
|
+
*
|
|
64
|
+
* DER format: 0x30 [total-length] 0x02 [r-length] [r] 0x02 [s-length] [s]
|
|
65
|
+
*
|
|
66
|
+
* @param signatureB64 - Base64URL encoded DER signature
|
|
67
|
+
* @returns Object with r and s as 32-byte hex strings
|
|
68
|
+
*/
|
|
69
|
+
export function parseDERSignature(signatureB64: string): { r: Hex; s: Hex } {
|
|
70
|
+
const bytes = base64UrlToBytes(signatureB64);
|
|
71
|
+
|
|
72
|
+
if (bytes.length < 8) {
|
|
73
|
+
throw new SignatureError("DER signature too short");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Verify it's a SEQUENCE
|
|
77
|
+
if (bytes[0] !== 0x30) {
|
|
78
|
+
throw new SignatureError(
|
|
79
|
+
`Invalid DER signature: expected SEQUENCE (0x30), got 0x${bytes[0].toString(16)}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let offset = 2; // Skip SEQUENCE tag and length
|
|
84
|
+
|
|
85
|
+
// Parse R
|
|
86
|
+
if (bytes[offset] !== 0x02) {
|
|
87
|
+
throw new SignatureError(
|
|
88
|
+
`Invalid DER signature: expected INTEGER (0x02) for R, got 0x${bytes[offset].toString(16)}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
offset++;
|
|
92
|
+
|
|
93
|
+
const rLength = bytes[offset];
|
|
94
|
+
offset++;
|
|
95
|
+
|
|
96
|
+
let rBytes = bytes.slice(offset, offset + rLength);
|
|
97
|
+
offset += rLength;
|
|
98
|
+
|
|
99
|
+
// Parse S
|
|
100
|
+
if (bytes[offset] !== 0x02) {
|
|
101
|
+
throw new SignatureError(
|
|
102
|
+
`Invalid DER signature: expected INTEGER (0x02) for S, got 0x${bytes[offset].toString(16)}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
offset++;
|
|
106
|
+
|
|
107
|
+
const sLength = bytes[offset];
|
|
108
|
+
offset++;
|
|
109
|
+
|
|
110
|
+
let sBytes = bytes.slice(offset, offset + sLength);
|
|
111
|
+
|
|
112
|
+
// Remove leading zero bytes (DER uses them for positive integers with high bit set)
|
|
113
|
+
if (rBytes[0] === 0x00 && rBytes.length > 32) {
|
|
114
|
+
rBytes = rBytes.slice(1);
|
|
115
|
+
}
|
|
116
|
+
if (sBytes[0] === 0x00 && sBytes.length > 32) {
|
|
117
|
+
sBytes = sBytes.slice(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Pad to 32 bytes if needed
|
|
121
|
+
const r = padHex(bytesToHex(rBytes), 32);
|
|
122
|
+
const s = padHex(bytesToHex(sBytes), 32);
|
|
123
|
+
|
|
124
|
+
return { r, s };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Normalizes the S value of an ECDSA signature to low-S form.
|
|
129
|
+
* This is required by some smart contract implementations to prevent
|
|
130
|
+
* signature malleability.
|
|
131
|
+
*
|
|
132
|
+
* For P-256, the curve order n is:
|
|
133
|
+
* 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
|
|
134
|
+
*
|
|
135
|
+
* If s > n/2, we replace s with n - s.
|
|
136
|
+
*
|
|
137
|
+
* @param s - The S component of the signature
|
|
138
|
+
* @returns The normalized S value in low-S form
|
|
139
|
+
*/
|
|
140
|
+
export function normalizeSignatureS(s: Hex): Hex {
|
|
141
|
+
const sClean = s.startsWith("0x") ? s.slice(2) : s;
|
|
142
|
+
const sValue = BigInt(`0x${sClean}`);
|
|
143
|
+
|
|
144
|
+
// P-256 curve order
|
|
145
|
+
const n = BigInt(
|
|
146
|
+
"0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"
|
|
147
|
+
);
|
|
148
|
+
const halfN = n / 2n;
|
|
149
|
+
|
|
150
|
+
if (sValue > halfN) {
|
|
151
|
+
const normalizedS = n - sValue;
|
|
152
|
+
return padHex(`0x${normalizedS.toString(16)}` as Hex, 32);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return padHex(s, 32);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Finds the position of a substring in a JSON string.
|
|
160
|
+
* Used to locate the challenge and type fields in clientDataJSON.
|
|
161
|
+
*
|
|
162
|
+
* @param json - The JSON string to search
|
|
163
|
+
* @param key - The key to find (e.g., "challenge", "type")
|
|
164
|
+
* @returns The byte offset of the key's value
|
|
165
|
+
*/
|
|
166
|
+
export function findJsonFieldPosition(json: string, key: string): number {
|
|
167
|
+
// Find the key with quotes: "key":"
|
|
168
|
+
const searchPattern = `"${key}":"`;
|
|
169
|
+
const position = json.indexOf(searchPattern);
|
|
170
|
+
|
|
171
|
+
if (position === -1) {
|
|
172
|
+
throw new SignatureError(`Field "${key}" not found in clientDataJSON`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Return position after the opening quote of the value
|
|
176
|
+
return position + searchPattern.length;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parses a WebAuthn assertion response into its components.
|
|
181
|
+
*
|
|
182
|
+
* @param assertion - The WebAuthn assertion response
|
|
183
|
+
* @returns Parsed signature components
|
|
184
|
+
*/
|
|
185
|
+
export function parseWebAuthnAssertion(assertion: {
|
|
186
|
+
authenticatorData: string;
|
|
187
|
+
clientDataJSON: string;
|
|
188
|
+
signature: string;
|
|
189
|
+
}): WebAuthnSignature {
|
|
190
|
+
// Parse authenticator data
|
|
191
|
+
const authenticatorData = base64UrlToHex(assertion.authenticatorData);
|
|
192
|
+
|
|
193
|
+
// Decode clientDataJSON
|
|
194
|
+
const clientDataJSONBytes = base64UrlToBytes(assertion.clientDataJSON);
|
|
195
|
+
const decoder = new TextDecoder();
|
|
196
|
+
const clientDataJSON = decoder.decode(clientDataJSONBytes);
|
|
197
|
+
|
|
198
|
+
// Parse the DER signature
|
|
199
|
+
const { r, s: rawS } = parseDERSignature(assertion.signature);
|
|
200
|
+
|
|
201
|
+
// Normalize S to low-S form
|
|
202
|
+
const s = normalizeSignatureS(rawS);
|
|
203
|
+
|
|
204
|
+
// Find positions for challenge and type in clientDataJSON
|
|
205
|
+
const challengeLocation = findJsonFieldPosition(clientDataJSON, "challenge");
|
|
206
|
+
const responseTypeLocation = findJsonFieldPosition(clientDataJSON, "type");
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
authenticatorData,
|
|
210
|
+
clientDataJSON,
|
|
211
|
+
r,
|
|
212
|
+
s,
|
|
213
|
+
challengeLocation,
|
|
214
|
+
responseTypeLocation,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Encodes a WebAuthn signature for smart contract verification.
|
|
220
|
+
* Different smart contract implementations expect different formats.
|
|
221
|
+
*
|
|
222
|
+
* @param signature - The parsed WebAuthn signature
|
|
223
|
+
* @param format - The encoding format to use
|
|
224
|
+
* @returns The encoded signature as hex
|
|
225
|
+
*/
|
|
226
|
+
export function encodeWebAuthnSignature(
|
|
227
|
+
signature: WebAuthnSignature,
|
|
228
|
+
format: SignatureEncodingFormat = "kernel"
|
|
229
|
+
): Hex {
|
|
230
|
+
switch (format) {
|
|
231
|
+
case "kernel":
|
|
232
|
+
return encodeKernelSignature(signature);
|
|
233
|
+
case "rhinestone":
|
|
234
|
+
return encodeRhinestoneSignature(signature);
|
|
235
|
+
case "raw":
|
|
236
|
+
return encodeRawSignature(signature);
|
|
237
|
+
default:
|
|
238
|
+
throw new SignatureError(`Unknown signature format: ${format}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Encodes signature for Kernel/ZeroDev smart accounts.
|
|
244
|
+
*
|
|
245
|
+
* Format: abi.encode(
|
|
246
|
+
* bytes authenticatorData,
|
|
247
|
+
* string clientDataJSON,
|
|
248
|
+
* uint256 responseTypeLocation,
|
|
249
|
+
* uint256 r,
|
|
250
|
+
* uint256 s,
|
|
251
|
+
* bool usePrecompile
|
|
252
|
+
* )
|
|
253
|
+
*/
|
|
254
|
+
function encodeKernelSignature(signature: WebAuthnSignature): Hex {
|
|
255
|
+
// Find the challengeIndex (position of challenge value in clientDataJSON)
|
|
256
|
+
const challengeIndex = signature.challengeLocation;
|
|
257
|
+
|
|
258
|
+
// Find responseTypeLocation (position after "type":" in the JSON)
|
|
259
|
+
const responseTypeIndex = signature.responseTypeLocation;
|
|
260
|
+
|
|
261
|
+
return encodeAbiParameters(
|
|
262
|
+
[
|
|
263
|
+
{ type: "bytes" }, // authenticatorData
|
|
264
|
+
{ type: "string" }, // clientDataJSON
|
|
265
|
+
{ type: "uint256" }, // challengeIndex
|
|
266
|
+
{ type: "uint256" }, // responseTypeIndex
|
|
267
|
+
{ type: "uint256" }, // r
|
|
268
|
+
{ type: "uint256" }, // s
|
|
269
|
+
],
|
|
270
|
+
[
|
|
271
|
+
signature.authenticatorData,
|
|
272
|
+
signature.clientDataJSON,
|
|
273
|
+
BigInt(challengeIndex),
|
|
274
|
+
BigInt(responseTypeIndex),
|
|
275
|
+
BigInt(signature.r),
|
|
276
|
+
BigInt(signature.s),
|
|
277
|
+
]
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Encodes signature for Rhinestone smart accounts.
|
|
283
|
+
*
|
|
284
|
+
* Format: abi.encode(
|
|
285
|
+
* bytes authenticatorData,
|
|
286
|
+
* bytes clientDataJSON,
|
|
287
|
+
* uint256[2] signature (r, s)
|
|
288
|
+
* )
|
|
289
|
+
*/
|
|
290
|
+
function encodeRhinestoneSignature(signature: WebAuthnSignature): Hex {
|
|
291
|
+
const encoder = new TextEncoder();
|
|
292
|
+
const clientDataBytes = encoder.encode(signature.clientDataJSON);
|
|
293
|
+
|
|
294
|
+
return encodeAbiParameters(
|
|
295
|
+
[
|
|
296
|
+
{ type: "bytes" }, // authenticatorData
|
|
297
|
+
{ type: "bytes" }, // clientDataJSON as bytes
|
|
298
|
+
{ type: "uint256[2]" }, // [r, s]
|
|
299
|
+
],
|
|
300
|
+
[
|
|
301
|
+
signature.authenticatorData,
|
|
302
|
+
bytesToHex(clientDataBytes),
|
|
303
|
+
[BigInt(signature.r), BigInt(signature.s)],
|
|
304
|
+
]
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Encodes signature in raw format (just r || s).
|
|
310
|
+
* Useful for custom implementations or debugging.
|
|
311
|
+
*/
|
|
312
|
+
function encodeRawSignature(signature: WebAuthnSignature): Hex {
|
|
313
|
+
// Simple concatenation of r and s (64 bytes total)
|
|
314
|
+
const rClean = signature.r.startsWith("0x")
|
|
315
|
+
? signature.r.slice(2)
|
|
316
|
+
: signature.r;
|
|
317
|
+
const sClean = signature.s.startsWith("0x")
|
|
318
|
+
? signature.s.slice(2)
|
|
319
|
+
: signature.s;
|
|
320
|
+
|
|
321
|
+
return `0x${rClean}${sClean}` as Hex;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Creates a WebAuthn challenge from a message hash.
|
|
326
|
+
* The challenge is typically the Base64URL encoding of the hash.
|
|
327
|
+
*
|
|
328
|
+
* @param messageHash - The message hash (usually a UserOpHash)
|
|
329
|
+
* @returns The challenge as Base64URL string
|
|
330
|
+
*/
|
|
331
|
+
export function createChallenge(messageHash: Hex): string {
|
|
332
|
+
return hexToBase64Url(messageHash);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Hashes a message according to EIP-191 (personal_sign).
|
|
337
|
+
* This is used when signing arbitrary messages.
|
|
338
|
+
*
|
|
339
|
+
* @param message - The message to hash
|
|
340
|
+
* @returns The EIP-191 hash
|
|
341
|
+
*/
|
|
342
|
+
export function hashMessage(message: string | Uint8Array): Hex {
|
|
343
|
+
const messageBytes =
|
|
344
|
+
typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
345
|
+
|
|
346
|
+
// EIP-191: "\x19Ethereum Signed Message:\n" + len(message) + message
|
|
347
|
+
const prefix = `\x19Ethereum Signed Message:\n${messageBytes.length}`;
|
|
348
|
+
const prefixBytes = new TextEncoder().encode(prefix);
|
|
349
|
+
|
|
350
|
+
const combined = new Uint8Array(prefixBytes.length + messageBytes.length);
|
|
351
|
+
combined.set(prefixBytes, 0);
|
|
352
|
+
combined.set(messageBytes, prefixBytes.length);
|
|
353
|
+
|
|
354
|
+
return keccak256(combined);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Creates an assertion options object for react-native-passkey.
|
|
359
|
+
*
|
|
360
|
+
* @param challenge - The challenge as Base64URL string
|
|
361
|
+
* @param rpId - The Relying Party ID
|
|
362
|
+
* @param credentialId - The credential ID to use (Base64URL)
|
|
363
|
+
* @param userVerification - User verification requirement
|
|
364
|
+
* @param timeout - Timeout in milliseconds
|
|
365
|
+
* @returns Options object for Passkey.get()
|
|
366
|
+
*/
|
|
367
|
+
export function createAssertionOptions(
|
|
368
|
+
challenge: string,
|
|
369
|
+
rpId: string,
|
|
370
|
+
credentialId: string,
|
|
371
|
+
userVerification: "required" | "preferred" | "discouraged" = "required",
|
|
372
|
+
timeout = 60000
|
|
373
|
+
): {
|
|
374
|
+
rpId: string;
|
|
375
|
+
challenge: string;
|
|
376
|
+
allowCredentials: Array<{ id: string; type: "public-key" }>;
|
|
377
|
+
userVerification: "required" | "preferred" | "discouraged";
|
|
378
|
+
timeout: number;
|
|
379
|
+
} {
|
|
380
|
+
return {
|
|
381
|
+
rpId,
|
|
382
|
+
challenge,
|
|
383
|
+
allowCredentials: [
|
|
384
|
+
{
|
|
385
|
+
id: credentialId,
|
|
386
|
+
type: "public-key",
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
userVerification,
|
|
390
|
+
timeout,
|
|
391
|
+
};
|
|
392
|
+
}
|