@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.
@@ -0,0 +1,161 @@
1
+ import { toHex, type Hex } from "viem";
2
+
3
+ /**
4
+ * Converts a Base64URL encoded string to a Uint8Array.
5
+ * Base64URL uses - instead of + and _ instead of /, with no padding.
6
+ *
7
+ * @param base64url - The Base64URL encoded string
8
+ * @returns The decoded bytes as Uint8Array
9
+ */
10
+ export function base64UrlToBytes(base64url: string): Uint8Array {
11
+ // Convert Base64URL to standard Base64
12
+ let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
13
+
14
+ // Add padding if needed
15
+ const padding = base64.length % 4;
16
+ if (padding) {
17
+ base64 += "=".repeat(4 - padding);
18
+ }
19
+
20
+ // Decode Base64 to binary string
21
+ const binaryString = atob(base64);
22
+
23
+ // Convert binary string to Uint8Array
24
+ const bytes = new Uint8Array(binaryString.length);
25
+ for (let i = 0; i < binaryString.length; i++) {
26
+ bytes[i] = binaryString.charCodeAt(i);
27
+ }
28
+
29
+ return bytes;
30
+ }
31
+
32
+ /**
33
+ * Converts a Uint8Array to a Base64URL encoded string.
34
+ *
35
+ * @param bytes - The bytes to encode
36
+ * @returns The Base64URL encoded string (no padding)
37
+ */
38
+ export function bytesToBase64Url(bytes: Uint8Array): string {
39
+ // Convert bytes to binary string
40
+ let binaryString = "";
41
+ for (let i = 0; i < bytes.length; i++) {
42
+ binaryString += String.fromCharCode(bytes[i]);
43
+ }
44
+
45
+ // Encode to Base64
46
+ const base64 = btoa(binaryString);
47
+
48
+ // Convert to Base64URL (remove padding, replace + with -, / with _)
49
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
50
+ }
51
+
52
+ /**
53
+ * Converts a Base64URL encoded string to a Hex string.
54
+ *
55
+ * @param base64url - The Base64URL encoded string
56
+ * @returns The hex representation with 0x prefix
57
+ */
58
+ export function base64UrlToHex(base64url: string): Hex {
59
+ const bytes = base64UrlToBytes(base64url);
60
+ return toHex(bytes);
61
+ }
62
+
63
+ /**
64
+ * Converts a Hex string to a Base64URL encoded string.
65
+ *
66
+ * @param hex - The hex string (with or without 0x prefix)
67
+ * @returns The Base64URL encoded string
68
+ */
69
+ export function hexToBase64Url(hex: Hex): string {
70
+ // Remove 0x prefix if present
71
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
72
+
73
+ // Convert hex to bytes
74
+ const bytes = new Uint8Array(cleanHex.length / 2);
75
+ for (let i = 0; i < cleanHex.length; i += 2) {
76
+ bytes[i / 2] = parseInt(cleanHex.slice(i, i + 2), 16);
77
+ }
78
+
79
+ return bytesToBase64Url(bytes);
80
+ }
81
+
82
+ /**
83
+ * Converts a Uint8Array to a Hex string.
84
+ *
85
+ * @param bytes - The bytes to convert
86
+ * @returns The hex representation with 0x prefix
87
+ */
88
+ export function bytesToHex(bytes: Uint8Array): Hex {
89
+ return toHex(bytes);
90
+ }
91
+
92
+ /**
93
+ * Converts a Hex string to a Uint8Array.
94
+ *
95
+ * @param hex - The hex string (with or without 0x prefix)
96
+ * @returns The bytes as Uint8Array
97
+ */
98
+ export function hexToBytes(hex: Hex): Uint8Array {
99
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
100
+ const bytes = new Uint8Array(cleanHex.length / 2);
101
+ for (let i = 0; i < cleanHex.length; i += 2) {
102
+ bytes[i / 2] = parseInt(cleanHex.slice(i, i + 2), 16);
103
+ }
104
+ return bytes;
105
+ }
106
+
107
+ /**
108
+ * Converts a UTF-8 string to a Base64URL encoded string.
109
+ *
110
+ * @param str - The UTF-8 string
111
+ * @returns The Base64URL encoded string
112
+ */
113
+ export function stringToBase64Url(str: string): string {
114
+ const encoder = new TextEncoder();
115
+ const bytes = encoder.encode(str);
116
+ return bytesToBase64Url(bytes);
117
+ }
118
+
119
+ /**
120
+ * Converts a Base64URL encoded string to a UTF-8 string.
121
+ *
122
+ * @param base64url - The Base64URL encoded string
123
+ * @returns The decoded UTF-8 string
124
+ */
125
+ export function base64UrlToString(base64url: string): string {
126
+ const bytes = base64UrlToBytes(base64url);
127
+ const decoder = new TextDecoder();
128
+ return decoder.decode(bytes);
129
+ }
130
+
131
+ /**
132
+ * Pads a hex value to ensure it has the specified byte length.
133
+ * Useful for ensuring coordinates are 32 bytes.
134
+ *
135
+ * @param hex - The hex string to pad
136
+ * @param byteLength - The desired byte length (default: 32)
137
+ * @returns The padded hex string with 0x prefix
138
+ */
139
+ export function padHex(hex: Hex, byteLength = 32): Hex {
140
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
141
+ const targetLength = byteLength * 2;
142
+
143
+ if (cleanHex.length >= targetLength) {
144
+ return `0x${cleanHex.slice(-targetLength)}` as Hex;
145
+ }
146
+
147
+ return `0x${cleanHex.padStart(targetLength, "0")}` as Hex;
148
+ }
149
+
150
+ /**
151
+ * Concatenates multiple Hex values into a single Hex value.
152
+ *
153
+ * @param hexValues - The hex values to concatenate
154
+ * @returns The concatenated hex string with 0x prefix
155
+ */
156
+ export function concatHex(...hexValues: Hex[]): Hex {
157
+ const combined = hexValues
158
+ .map((h) => (h.startsWith("0x") ? h.slice(2) : h))
159
+ .join("");
160
+ return `0x${combined}` as Hex;
161
+ }
@@ -0,0 +1,356 @@
1
+ import { decode as cborDecode } from "cbor-x";
2
+ import type { Hex } from "viem";
3
+ import type { P256PublicKey } from "../types/index.js";
4
+ import { base64UrlToBytes, bytesToHex, padHex, concatHex } from "./base64url.js";
5
+
6
+ /**
7
+ * COSE Key Types (kty)
8
+ * @see https://www.iana.org/assignments/cose/cose.xhtml#key-type
9
+ */
10
+ const COSE_KEY_TYPE = {
11
+ OKP: 1, // Octet Key Pair
12
+ EC2: 2, // Elliptic Curve with x and y coordinates
13
+ RSA: 3, // RSA
14
+ SYMMETRIC: 4, // Symmetric key
15
+ } as const;
16
+
17
+ /**
18
+ * COSE Algorithms
19
+ * @see https://www.iana.org/assignments/cose/cose.xhtml#algorithms
20
+ */
21
+ const COSE_ALGORITHM = {
22
+ ES256: -7, // ECDSA w/ SHA-256 (P-256)
23
+ ES384: -35, // ECDSA w/ SHA-384 (P-384)
24
+ ES512: -36, // ECDSA w/ SHA-512 (P-521)
25
+ EDDSA: -8, // EdDSA
26
+ } as const;
27
+
28
+ /**
29
+ * COSE Elliptic Curves
30
+ * @see https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
31
+ */
32
+ const COSE_CURVE = {
33
+ P256: 1, // NIST P-256 (secp256r1)
34
+ P384: 2, // NIST P-384
35
+ P521: 3, // NIST P-521
36
+ ED25519: 6, // Ed25519
37
+ } as const;
38
+
39
+ /**
40
+ * COSE Key Parameter Labels
41
+ */
42
+ const COSE_KEY_PARAMS = {
43
+ KTY: 1, // Key type
44
+ ALG: 3, // Algorithm
45
+ CRV: -1, // Curve (for EC2 keys)
46
+ X: -2, // X coordinate (for EC2 keys)
47
+ Y: -3, // Y coordinate (for EC2 keys)
48
+ } as const;
49
+
50
+ /**
51
+ * Error thrown when COSE key parsing fails.
52
+ */
53
+ export class COSEParseError extends Error {
54
+ constructor(message: string) {
55
+ super(message);
56
+ this.name = "COSEParseError";
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Represents a parsed COSE key structure.
62
+ */
63
+ interface COSEKey {
64
+ kty: number;
65
+ alg?: number;
66
+ crv?: number;
67
+ x?: Uint8Array;
68
+ y?: Uint8Array;
69
+ }
70
+
71
+ /**
72
+ * Parses a COSE key from CBOR-encoded bytes.
73
+ *
74
+ * @param coseBytes - The CBOR-encoded COSE key bytes
75
+ * @returns The parsed COSE key structure
76
+ * @throws COSEParseError if parsing fails
77
+ */
78
+ function parseCOSEKey(coseBytes: Uint8Array): COSEKey {
79
+ let decoded: Map<number, unknown>;
80
+
81
+ try {
82
+ decoded = cborDecode(coseBytes);
83
+ } catch (error) {
84
+ throw new COSEParseError(
85
+ `Failed to decode CBOR: ${error instanceof Error ? error.message : "Unknown error"}`
86
+ );
87
+ }
88
+
89
+ // COSE keys are encoded as CBOR maps
90
+ if (!(decoded instanceof Map)) {
91
+ throw new COSEParseError(
92
+ "Invalid COSE key format: expected CBOR map"
93
+ );
94
+ }
95
+
96
+ const kty = decoded.get(COSE_KEY_PARAMS.KTY);
97
+ if (typeof kty !== "number") {
98
+ throw new COSEParseError("Invalid COSE key: missing or invalid kty");
99
+ }
100
+
101
+ const result: COSEKey = { kty };
102
+
103
+ // Optional algorithm
104
+ const alg = decoded.get(COSE_KEY_PARAMS.ALG);
105
+ if (typeof alg === "number") {
106
+ result.alg = alg;
107
+ }
108
+
109
+ // For EC2 keys, extract curve and coordinates
110
+ if (kty === COSE_KEY_TYPE.EC2) {
111
+ const crv = decoded.get(COSE_KEY_PARAMS.CRV);
112
+ if (typeof crv === "number") {
113
+ result.crv = crv;
114
+ }
115
+
116
+ const x = decoded.get(COSE_KEY_PARAMS.X);
117
+ if (x instanceof Uint8Array) {
118
+ result.x = x;
119
+ }
120
+
121
+ const y = decoded.get(COSE_KEY_PARAMS.Y);
122
+ if (y instanceof Uint8Array) {
123
+ result.y = y;
124
+ }
125
+ }
126
+
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Extracts the attestedCredentialData from authenticatorData.
132
+ * The attestedCredentialData contains the COSE public key.
133
+ *
134
+ * AuthenticatorData structure:
135
+ * - rpIdHash (32 bytes)
136
+ * - flags (1 byte)
137
+ * - signCount (4 bytes, big endian)
138
+ * - attestedCredentialData (variable, if AT flag is set)
139
+ * - aaguid (16 bytes)
140
+ * - credentialIdLength (2 bytes, big endian)
141
+ * - credentialId (credentialIdLength bytes)
142
+ * - credentialPublicKey (remaining bytes, CBOR encoded)
143
+ *
144
+ * @param authenticatorData - Raw authenticator data bytes
145
+ * @returns Object containing credentialId and publicKey bytes
146
+ * @throws COSEParseError if parsing fails
147
+ */
148
+ export function parseAuthenticatorData(authenticatorData: Uint8Array): {
149
+ credentialId: Uint8Array;
150
+ publicKeyBytes: Uint8Array;
151
+ } {
152
+ // Minimum length: 37 bytes (rpIdHash + flags + signCount)
153
+ if (authenticatorData.length < 37) {
154
+ throw new COSEParseError(
155
+ "Authenticator data too short: minimum 37 bytes required"
156
+ );
157
+ }
158
+
159
+ // Check AT (attestedCredentialData) flag (bit 6)
160
+ const flags = authenticatorData[32];
161
+ const hasAttestedCredData = (flags & 0x40) !== 0;
162
+
163
+ if (!hasAttestedCredData) {
164
+ throw new COSEParseError(
165
+ "Authenticator data does not contain attested credential data"
166
+ );
167
+ }
168
+
169
+ // Skip rpIdHash (32) + flags (1) + signCount (4) = 37 bytes
170
+ let offset = 37;
171
+
172
+ // Skip aaguid (16 bytes)
173
+ offset += 16;
174
+
175
+ if (authenticatorData.length < offset + 2) {
176
+ throw new COSEParseError(
177
+ "Authenticator data too short: missing credentialIdLength"
178
+ );
179
+ }
180
+
181
+ // Read credentialIdLength (2 bytes, big endian)
182
+ const credentialIdLength =
183
+ (authenticatorData[offset] << 8) | authenticatorData[offset + 1];
184
+ offset += 2;
185
+
186
+ if (authenticatorData.length < offset + credentialIdLength) {
187
+ throw new COSEParseError(
188
+ "Authenticator data too short: missing credentialId"
189
+ );
190
+ }
191
+
192
+ // Read credentialId
193
+ const credentialId = authenticatorData.slice(offset, offset + credentialIdLength);
194
+ offset += credentialIdLength;
195
+
196
+ // Remaining bytes are the CBOR-encoded public key
197
+ const publicKeyBytes = authenticatorData.slice(offset);
198
+
199
+ if (publicKeyBytes.length === 0) {
200
+ throw new COSEParseError(
201
+ "Authenticator data too short: missing public key"
202
+ );
203
+ }
204
+
205
+ return { credentialId, publicKeyBytes };
206
+ }
207
+
208
+ /**
209
+ * Extracts P256 (secp256r1) public key coordinates from a COSE key.
210
+ * This is the CRUCIAL function for Passkey integration.
211
+ *
212
+ * @param coseBytes - The CBOR-encoded COSE key bytes
213
+ * @returns The X and Y coordinates as 32-byte hex strings
214
+ * @throws COSEParseError if the key is not a valid P256 key
215
+ */
216
+ export function extractP256PublicKey(coseBytes: Uint8Array): P256PublicKey {
217
+ const coseKey = parseCOSEKey(coseBytes);
218
+
219
+ // Validate key type
220
+ if (coseKey.kty !== COSE_KEY_TYPE.EC2) {
221
+ throw new COSEParseError(
222
+ `Invalid key type: expected EC2 (${COSE_KEY_TYPE.EC2}), got ${coseKey.kty}`
223
+ );
224
+ }
225
+
226
+ // Validate curve (if present)
227
+ if (coseKey.crv !== undefined && coseKey.crv !== COSE_CURVE.P256) {
228
+ throw new COSEParseError(
229
+ `Invalid curve: expected P-256 (${COSE_CURVE.P256}), got ${coseKey.crv}`
230
+ );
231
+ }
232
+
233
+ // Validate algorithm (if present)
234
+ if (coseKey.alg !== undefined && coseKey.alg !== COSE_ALGORITHM.ES256) {
235
+ throw new COSEParseError(
236
+ `Invalid algorithm: expected ES256 (${COSE_ALGORITHM.ES256}), got ${coseKey.alg}`
237
+ );
238
+ }
239
+
240
+ // Validate coordinates
241
+ if (!coseKey.x || coseKey.x.length === 0) {
242
+ throw new COSEParseError("Missing X coordinate in COSE key");
243
+ }
244
+
245
+ if (!coseKey.y || coseKey.y.length === 0) {
246
+ throw new COSEParseError("Missing Y coordinate in COSE key");
247
+ }
248
+
249
+ // P-256 coordinates should be exactly 32 bytes
250
+ // However, some implementations may include leading zeros or strip them
251
+ // We handle both cases by padding/trimming to exactly 32 bytes
252
+ const xHex = padHex(bytesToHex(coseKey.x), 32);
253
+ const yHex = padHex(bytesToHex(coseKey.y), 32);
254
+
255
+ return { x: xHex, y: yHex };
256
+ }
257
+
258
+ /**
259
+ * Extracts the public key from a WebAuthn attestation response.
260
+ *
261
+ * @param attestationObject - Base64URL encoded attestation object from registration
262
+ * @returns The P256 public key coordinates
263
+ * @throws COSEParseError if extraction fails
264
+ */
265
+ export function extractPublicKeyFromAttestation(
266
+ attestationObject: string
267
+ ): {
268
+ credentialId: Uint8Array;
269
+ publicKey: P256PublicKey;
270
+ } {
271
+ // Decode the attestation object
272
+ const attestationBytes = base64UrlToBytes(attestationObject);
273
+
274
+ let attestation: { authData?: Uint8Array; fmt?: string };
275
+ try {
276
+ attestation = cborDecode(attestationBytes);
277
+ } catch (error) {
278
+ throw new COSEParseError(
279
+ `Failed to decode attestation object: ${error instanceof Error ? error.message : "Unknown error"}`
280
+ );
281
+ }
282
+
283
+ // Extract authData from attestation
284
+ if (!attestation.authData || !(attestation.authData instanceof Uint8Array)) {
285
+ throw new COSEParseError(
286
+ "Invalid attestation object: missing or invalid authData"
287
+ );
288
+ }
289
+
290
+ // Parse the authenticator data to get the public key
291
+ const { credentialId, publicKeyBytes } = parseAuthenticatorData(
292
+ attestation.authData
293
+ );
294
+
295
+ // Extract the P256 coordinates from the COSE key
296
+ const publicKey = extractP256PublicKey(publicKeyBytes);
297
+
298
+ return { credentialId, publicKey };
299
+ }
300
+
301
+ /**
302
+ * Encodes a P256 public key to the uncompressed format (0x04 || x || y).
303
+ * This is the standard SEC1 uncompressed point format.
304
+ *
305
+ * @param publicKey - The P256 public key coordinates
306
+ * @returns The uncompressed public key as hex (65 bytes total)
307
+ */
308
+ export function encodeUncompressedPublicKey(publicKey: P256PublicKey): Hex {
309
+ // Ensure coordinates are exactly 32 bytes each
310
+ const x = padHex(publicKey.x, 32);
311
+ const y = padHex(publicKey.y, 32);
312
+
313
+ // Uncompressed format: 0x04 || x || y
314
+ return concatHex("0x04" as Hex, x, y);
315
+ }
316
+
317
+ /**
318
+ * Validates that a public key is on the P256 curve.
319
+ * This is a basic validation - for full security, use a proper crypto library.
320
+ *
321
+ * @param publicKey - The public key coordinates to validate
322
+ * @returns true if the key appears valid
323
+ */
324
+ export function isValidP256PublicKey(publicKey: P256PublicKey): boolean {
325
+ // Basic validation: check that coordinates are 32 bytes each
326
+ const xClean = publicKey.x.startsWith("0x")
327
+ ? publicKey.x.slice(2)
328
+ : publicKey.x;
329
+ const yClean = publicKey.y.startsWith("0x")
330
+ ? publicKey.y.slice(2)
331
+ : publicKey.y;
332
+
333
+ // Each coordinate should be at most 64 hex chars (32 bytes)
334
+ if (xClean.length > 64 || yClean.length > 64) {
335
+ return false;
336
+ }
337
+
338
+ // Coordinates should not be zero
339
+ if (BigInt(`0x${xClean}`) === 0n || BigInt(`0x${yClean}`) === 0n) {
340
+ return false;
341
+ }
342
+
343
+ // P-256 prime
344
+ const p = BigInt(
345
+ "0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"
346
+ );
347
+
348
+ // Coordinates should be less than the prime
349
+ if (BigInt(`0x${xClean}`) >= p || BigInt(`0x${yClean}`) >= p) {
350
+ return false;
351
+ }
352
+
353
+ return true;
354
+ }
355
+
356
+ export { COSE_KEY_TYPE, COSE_ALGORITHM, COSE_CURVE };
@@ -0,0 +1,40 @@
1
+ // Base64URL encoding utilities
2
+ export {
3
+ base64UrlToBytes,
4
+ bytesToBase64Url,
5
+ base64UrlToHex,
6
+ hexToBase64Url,
7
+ bytesToHex,
8
+ hexToBytes,
9
+ stringToBase64Url,
10
+ base64UrlToString,
11
+ padHex,
12
+ concatHex,
13
+ } from "./base64url.js";
14
+
15
+ // COSE key parsing utilities
16
+ export {
17
+ extractP256PublicKey,
18
+ extractPublicKeyFromAttestation,
19
+ parseAuthenticatorData,
20
+ encodeUncompressedPublicKey,
21
+ isValidP256PublicKey,
22
+ COSEParseError,
23
+ COSE_KEY_TYPE,
24
+ COSE_ALGORITHM,
25
+ COSE_CURVE,
26
+ } from "./cose.js";
27
+
28
+ // Passkey/WebAuthn utilities
29
+ export {
30
+ parseAuthenticatorDataFromAssertion,
31
+ parseDERSignature,
32
+ normalizeSignatureS,
33
+ findJsonFieldPosition,
34
+ parseWebAuthnAssertion,
35
+ encodeWebAuthnSignature,
36
+ createChallenge,
37
+ hashMessage,
38
+ createAssertionOptions,
39
+ SignatureError,
40
+ } from "./passkey.js";