@revibase/ctap2-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +65 -0
  2. package/lib/apdu.d.ts +56 -0
  3. package/lib/apdu.d.ts.map +1 -0
  4. package/lib/apdu.js +225 -0
  5. package/lib/buildRequests.d.ts +11 -0
  6. package/lib/buildRequests.d.ts.map +1 -0
  7. package/lib/buildRequests.js +24 -0
  8. package/lib/constants.d.ts +11 -0
  9. package/lib/constants.d.ts.map +1 -0
  10. package/lib/constants.js +15 -0
  11. package/lib/ctap2/cborUtils.d.ts +14 -0
  12. package/lib/ctap2/cborUtils.d.ts.map +1 -0
  13. package/lib/ctap2/cborUtils.js +141 -0
  14. package/lib/ctap2/getAssertion.d.ts +32 -0
  15. package/lib/ctap2/getAssertion.d.ts.map +1 -0
  16. package/lib/ctap2/getAssertion.js +104 -0
  17. package/lib/ctap2/makeCredential.d.ts +37 -0
  18. package/lib/ctap2/makeCredential.d.ts.map +1 -0
  19. package/lib/ctap2/makeCredential.js +118 -0
  20. package/lib/errors.d.ts +7 -0
  21. package/lib/errors.d.ts.map +1 -0
  22. package/lib/errors.js +13 -0
  23. package/lib/helpers/authDataExtensions.d.ts +9 -0
  24. package/lib/helpers/authDataExtensions.d.ts.map +1 -0
  25. package/lib/helpers/authDataExtensions.js +192 -0
  26. package/lib/helpers/base64url.d.ts +3 -0
  27. package/lib/helpers/base64url.d.ts.map +1 -0
  28. package/lib/helpers/base64url.js +29 -0
  29. package/lib/helpers/clientData.d.ts +9 -0
  30. package/lib/helpers/clientData.d.ts.map +1 -0
  31. package/lib/helpers/clientData.js +18 -0
  32. package/lib/helpers/fromWebAuthnJson.d.ts +6 -0
  33. package/lib/helpers/fromWebAuthnJson.d.ts.map +1 -0
  34. package/lib/helpers/fromWebAuthnJson.js +125 -0
  35. package/lib/helpers/webauthnResponses.d.ts +21 -0
  36. package/lib/helpers/webauthnResponses.d.ts.map +1 -0
  37. package/lib/helpers/webauthnResponses.js +51 -0
  38. package/lib/helpers/webauthnTypes.d.ts +6 -0
  39. package/lib/helpers/webauthnTypes.d.ts.map +1 -0
  40. package/lib/helpers/webauthnTypes.js +2 -0
  41. package/lib/index.d.ts +4 -0
  42. package/lib/index.d.ts.map +1 -0
  43. package/lib/index.js +8 -0
  44. package/lib/parseResponses.d.ts +19 -0
  45. package/lib/parseResponses.d.ts.map +1 -0
  46. package/lib/parseResponses.js +37 -0
  47. package/lib/publicApi.d.ts +15 -0
  48. package/lib/publicApi.d.ts.map +1 -0
  49. package/lib/publicApi.js +83 -0
  50. package/lib/types.d.ts +10 -0
  51. package/lib/types.d.ts.map +1 -0
  52. package/lib/types.js +2 -0
  53. package/package.json +35 -0
  54. package/src/apdu.ts +285 -0
  55. package/src/constants.ts +16 -0
  56. package/src/ctap2/cborUtils.ts +155 -0
  57. package/src/ctap2/getAssertion.ts +154 -0
  58. package/src/ctap2/makeCredential.ts +173 -0
  59. package/src/errors.ts +11 -0
  60. package/src/helpers/authDataExtensions.ts +214 -0
  61. package/src/helpers/base64url.ts +24 -0
  62. package/src/helpers/clientData.ts +24 -0
  63. package/src/helpers/fromWebAuthnJson.ts +175 -0
  64. package/src/helpers/webauthnResponses.ts +113 -0
  65. package/src/index.ts +6 -0
  66. package/src/parseResponses.ts +67 -0
  67. package/src/publicApi.ts +152 -0
  68. package/src/types.ts +16 -0
  69. package/tsconfig.json +16 -0
package/src/apdu.ts ADDED
@@ -0,0 +1,285 @@
1
+ import {
2
+ FIDO_AID,
3
+ NFC_TX_CHUNK_SIZE,
4
+ SW1_MORE_DATA,
5
+ SW_NO_ERROR,
6
+ } from "./constants";
7
+ import { ApduError } from "./errors";
8
+
9
+ const CLA_CHAIN = 0x10;
10
+
11
+ /** CTAP2 error codes */
12
+ function formatCtapErrorMessage(code: number): string {
13
+ const hex = `0x${code.toString(16).padStart(2, "0")}`;
14
+ const names: Record<number, string> = {
15
+ 0x01: "INVALID_COMMAND",
16
+ 0x02: "INVALID_PARAMETER",
17
+ 0x03: "INVALID_LENGTH",
18
+ 0x11: "CBOR_UNEXPECTED_TYPE",
19
+ 0x12: "INVALID_CBOR",
20
+ 0x14: "MISSING_PARAMETER",
21
+ 0x2e: "NO_CREDENTIALS",
22
+ 0x30: "NOT_ALLOWED",
23
+ };
24
+ const name = names[code];
25
+ return name ? `CTAP error ${hex} (${name})` : `CTAP error code ${hex}`;
26
+ }
27
+
28
+ interface ParsedIso7816Response {
29
+ data: Uint8Array;
30
+ sw1: number;
31
+ sw2: number;
32
+ statusWord: number;
33
+ }
34
+
35
+ interface NfcShortApduSegment {
36
+ bytes: Uint8Array;
37
+ chained: boolean;
38
+ }
39
+
40
+ export function encodeExtendedCase3e(
41
+ cla: number,
42
+ ins: number,
43
+ p1: number,
44
+ p2: number,
45
+ data: Uint8Array,
46
+ ): Uint8Array {
47
+ const n = data.length;
48
+ if (n > 65535) {
49
+ throw new ApduError("APDU data field exceeds 65535 bytes");
50
+ }
51
+ const out = new Uint8Array(7 + n + 2);
52
+ out[0] = cla & 0xff;
53
+ out[1] = ins & 0xff;
54
+ out[2] = p1 & 0xff;
55
+ out[3] = p2 & 0xff;
56
+ out[4] = 0x00;
57
+ out[5] = (n >> 8) & 0xff;
58
+ out[6] = n & 0xff;
59
+ out.set(data, 7);
60
+ out[7 + n] = 0x00;
61
+ out[7 + n + 1] = 0x00;
62
+ return out;
63
+ }
64
+
65
+ export function encodeShortApdu(
66
+ cla: number,
67
+ ins: number,
68
+ p1: number,
69
+ p2: number,
70
+ payload: Uint8Array,
71
+ options: { chained: boolean },
72
+ ): Uint8Array {
73
+ const lc = payload.length;
74
+ if (lc > 255) {
75
+ throw new ApduError("short APDU payload must be ≤ 255 bytes");
76
+ }
77
+ const withLe = !options.chained;
78
+ const out = new Uint8Array(5 + lc + (withLe ? 1 : 0));
79
+ out[0] = (cla & 0xff) | (options.chained ? CLA_CHAIN : 0);
80
+ out[1] = ins & 0xff;
81
+ out[2] = p1 & 0xff;
82
+ out[3] = p2 & 0xff;
83
+ out[4] = lc & 0xff;
84
+ out.set(payload, 5);
85
+ if (withLe) {
86
+ out[5 + lc] = 0x00;
87
+ }
88
+ return out;
89
+ }
90
+
91
+ /**
92
+ * Send CTAP CBOR command APDUs
93
+ * Extended APDU in memory is split into short chained frames;
94
+ * Intermediate responses must be SW 0x9000, the last response carries CTAP payload + SW.
95
+ */
96
+ export async function transmitCborApduSegments(
97
+ segments: NfcShortApduSegment[],
98
+ transceive: (apdu: Uint8Array) => Promise<Uint8Array>,
99
+ ): Promise<Uint8Array> {
100
+ let last: Uint8Array | undefined;
101
+ for (const seg of segments) {
102
+ const resp = await transceive(seg.bytes);
103
+ if (resp.length < 2) {
104
+ throw new ApduError(
105
+ `APDU response too short for status word (${resp.length} byte(s))`,
106
+ );
107
+ }
108
+ const sw = (resp[resp.length - 2] << 8) | resp[resp.length - 1];
109
+ if (seg.chained) {
110
+ if (sw !== SW_NO_ERROR) {
111
+ throw new ApduError(
112
+ `ISO7816 status 0x${sw.toString(16).padStart(4, "0")}`,
113
+ );
114
+ }
115
+ } else {
116
+ last = resp;
117
+ }
118
+ }
119
+ if (!last) {
120
+ throw new ApduError("no final APDU response");
121
+ }
122
+ return last;
123
+ }
124
+
125
+ /** Max GET RESPONSE iterations (matches practical NFC response sizes). */
126
+ const GET_RESPONSE_MAX_CHAIN = 256;
127
+
128
+ /**
129
+ * Follow ISO 7816 "more data" responses for CBOR:
130
+ * while SW1 == 0x61, send GET RESPONSE (CLA 0x80, INS 0xc0, Le = SW2) and
131
+ * concatenate response bodies; final buffer is payload + SW1 + SW2.
132
+ */
133
+ export async function collectCborApduResponse(
134
+ firstResponse: Uint8Array,
135
+ transceive: (apdu: Uint8Array) => Promise<Uint8Array>,
136
+ ): Promise<Uint8Array> {
137
+ const chunks: Uint8Array[] = [];
138
+ let cur = firstResponse;
139
+ for (let i = 0; i < GET_RESPONSE_MAX_CHAIN; i++) {
140
+ if (cur.length < 2) {
141
+ throw new ApduError("APDU response too short");
142
+ }
143
+ const sw1 = cur[cur.length - 2];
144
+ const sw2 = cur[cur.length - 1];
145
+ chunks.push(cur.slice(0, -2));
146
+ if (sw1 !== SW1_MORE_DATA) {
147
+ const totalLen = chunks.reduce((a, c) => a + c.length, 0) + 2;
148
+ const out = new Uint8Array(totalLen);
149
+ let o = 0;
150
+ for (const c of chunks) {
151
+ out.set(c, o);
152
+ o += c.length;
153
+ }
154
+ out[o] = sw1;
155
+ out[o + 1] = sw2;
156
+ return out;
157
+ }
158
+ cur = await transceive(buildGetResponseApdu(sw2, true));
159
+ }
160
+ throw new ApduError("GET RESPONSE chain exceeded limit");
161
+ }
162
+
163
+ /**
164
+ * Full NFC CTAP2 command exchange for Java Card–style FIDO applets: send short
165
+ * chained command frames ({@link transmitCborApduSegments}), then resolve
166
+ * `61 xx` / GET RESPONSE until `90 00` (same as libfido2 `nfc_do_tx` +
167
+ * `rx_msg`). Use after SELECT.
168
+ */
169
+ export async function transceiveNfcCtap2Command(
170
+ segments: NfcShortApduSegment[],
171
+ transceive: (apdu: Uint8Array) => Promise<Uint8Array>,
172
+ ): Promise<Uint8Array> {
173
+ return collectCborApduResponse(
174
+ await transmitCborApduSegments(segments, transceive),
175
+ transceive,
176
+ );
177
+ }
178
+
179
+ export function splitExtendedApduForNfcTransmit(
180
+ extended: Uint8Array,
181
+ chunkSize: number = NFC_TX_CHUNK_SIZE,
182
+ ): NfcShortApduSegment[] {
183
+ if (extended.length < 9) {
184
+ throw new ApduError("extended APDU too short");
185
+ }
186
+ const cla = extended[0];
187
+ const ins = extended[1];
188
+ const p1 = extended[2];
189
+ const p2 = extended[3];
190
+ const data = extended.slice(7, extended.length - 2);
191
+ const segments: NfcShortApduSegment[] = [];
192
+ let off = 0;
193
+ while (data.length - off > chunkSize) {
194
+ const chunk = data.subarray(off, off + chunkSize);
195
+ segments.push({
196
+ bytes: encodeShortApdu(cla, ins, p1, p2, chunk, { chained: true }),
197
+ chained: true,
198
+ });
199
+ off += chunkSize;
200
+ }
201
+ const rest = data.subarray(off);
202
+ segments.push({
203
+ bytes: encodeShortApdu(cla, ins, p1, p2, rest, { chained: false }),
204
+ chained: false,
205
+ });
206
+ return segments;
207
+ }
208
+
209
+ export function buildSelectFidoApdu(): Uint8Array {
210
+ return encodeExtendedCase3e(0x00, 0xa4, 0x04, 0x00, FIDO_AID);
211
+ }
212
+
213
+ /** Short APDU(s) for SELECT — preferred over {@link buildSelectFidoApdu} on NFC. */
214
+ export function buildSelectFidoApduSegments(): NfcShortApduSegment[] {
215
+ return splitExtendedApduForNfcTransmit(buildSelectFidoApdu());
216
+ }
217
+
218
+ export function buildCborCommandApdu(cborPayload: Uint8Array): Uint8Array {
219
+ return encodeExtendedCase3e(0x80, 0x10, 0x00, 0x00, cborPayload);
220
+ }
221
+
222
+ export function buildGetResponseApdu(le: number, cbor: boolean): Uint8Array {
223
+ const apdu = new Uint8Array(5);
224
+ apdu[0] = cbor ? 0x80 : 0x00;
225
+ apdu[1] = 0xc0;
226
+ apdu[2] = 0x00;
227
+ apdu[3] = 0x00;
228
+ apdu[4] = le & 0xff;
229
+ return apdu;
230
+ }
231
+
232
+ export function parseIso7816Response(buf: Uint8Array): ParsedIso7816Response {
233
+ if (buf.length < 2) {
234
+ throw new ApduError("response must include SW1 and SW2");
235
+ }
236
+ const sw1 = buf[buf.length - 2];
237
+ const sw2 = buf[buf.length - 1];
238
+ const statusWord = (sw1 << 8) | sw2;
239
+ return {
240
+ data: buf.slice(0, -2),
241
+ sw1,
242
+ sw2,
243
+ statusWord,
244
+ };
245
+ }
246
+
247
+ function isSuccessStatus(statusWord: number): boolean {
248
+ return statusWord === SW_NO_ERROR;
249
+ }
250
+
251
+ type ParseCtaphidCborBodyResult =
252
+ | { ok: true; cborMapBytes: Uint8Array }
253
+ | { ok: false; layer: "iso7816"; statusWord: number }
254
+ | { ok: false; layer: "ctap"; errorCode: number };
255
+
256
+ export function parseCtaphidCborFromApduResponse(
257
+ apduResponseIncludingSw: Uint8Array,
258
+ ): ParseCtaphidCborBodyResult {
259
+ const { data, statusWord } = parseIso7816Response(apduResponseIncludingSw);
260
+ if (!isSuccessStatus(statusWord)) {
261
+ return { ok: false, layer: "iso7816", statusWord };
262
+ }
263
+ if (data.length < 1) {
264
+ throw new ApduError("empty CTAP payload after ISO7816 success");
265
+ }
266
+ const status = data[0];
267
+ if (status !== 0) {
268
+ return { ok: false, layer: "ctap", errorCode: status };
269
+ }
270
+ return { ok: true, cborMapBytes: data.subarray(1) };
271
+ }
272
+
273
+ export function unwrapCtaphidCborBody(
274
+ parsed: ParseCtaphidCborBodyResult,
275
+ ): Uint8Array {
276
+ if (!parsed.ok) {
277
+ if (parsed.layer === "iso7816") {
278
+ throw new ApduError(
279
+ `ISO7816 status 0x${parsed.statusWord.toString(16).padStart(4, "0")}`,
280
+ );
281
+ }
282
+ throw new ApduError(formatCtapErrorMessage(parsed.errorCode));
283
+ }
284
+ return parsed.cborMapBytes;
285
+ }
@@ -0,0 +1,16 @@
1
+ export const NFC_TX_CHUNK_SIZE = 240;
2
+
3
+ export const FIDO_AID = Uint8Array.from([
4
+ 0xa0, 0x00, 0x00, 0x06, 0x47, 0x2f, 0x00, 0x01,
5
+ ]);
6
+
7
+ export const CTAP_CMD_MSG = 0x03;
8
+ export const CTAP_CMD_INIT = 0x06;
9
+ export const CTAP_CMD_CBOR = 0x10;
10
+
11
+ export const CTAP_CBOR_MAKE_CRED = 0x01;
12
+ export const CTAP_CBOR_ASSERT = 0x02;
13
+ export const CTAP_CBOR_GETINFO = 0x04;
14
+
15
+ export const SW_NO_ERROR = 0x9000;
16
+ export const SW1_MORE_DATA = 0x61;
@@ -0,0 +1,155 @@
1
+ import { Encoder } from "cbor-x";
2
+
3
+ /**
4
+ * cbor-x encodes JS `Map` with CBOR tag 259 by default. CTAP2/FIDO2 expects a
5
+ * plain CBOR map (no tag), or authenticators return FIDO_ERR_CBOR_UNEXPECTED_TYPE (0x11).
6
+ */
7
+ const ctapCborEncoder = new Encoder({
8
+ useTag259ForMaps: false,
9
+ useRecords: false,
10
+ mapsAsObjects: true,
11
+ structuredClone: false,
12
+ tagUint8Array: false,
13
+ bundleStrings: false,
14
+ } as ConstructorParameters<typeof Encoder>[0]);
15
+
16
+ const textEncoder = new TextEncoder();
17
+
18
+ /**
19
+ * CTAP2 canonical CBOR key order for text-string map keys (FIDO CTAP §8 Message Encoding):
20
+ * shorter UTF-8 length sorts first; same length → lower byte-wise lexical order.
21
+ */
22
+ export function compareCtapTextMapKeys(a: string, b: string): number {
23
+ const ab = textEncoder.encode(a);
24
+ const bb = textEncoder.encode(b);
25
+ if (ab.length !== bb.length) {
26
+ return ab.length - bb.length;
27
+ }
28
+ for (let i = 0; i < ab.length; i++) {
29
+ if (ab[i] !== bb[i]) {
30
+ return ab[i] - bb[i];
31
+ }
32
+ }
33
+ return 0;
34
+ }
35
+
36
+ /**
37
+ * Recursively reorders every CBOR-serialized map to CTAP2 canonical form (sorted keys).
38
+ * Integer-key maps (command parameters, COSE, etc.) are sorted numerically.
39
+ */
40
+ export function canonicalizeCtapValue(value: unknown): unknown {
41
+ if (value === null || value === undefined) {
42
+ return value;
43
+ }
44
+ const t = typeof value;
45
+ if (
46
+ t === "boolean" ||
47
+ t === "number" ||
48
+ t === "string" ||
49
+ t === "bigint"
50
+ ) {
51
+ return value;
52
+ }
53
+ if (value instanceof Uint8Array) {
54
+ return value;
55
+ }
56
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
57
+ const view = value as ArrayBufferView;
58
+ return new Uint8Array(
59
+ view.buffer,
60
+ view.byteOffset,
61
+ view.byteLength,
62
+ );
63
+ }
64
+ if (Array.isArray(value)) {
65
+ return value.map((v) => canonicalizeCtapValue(v));
66
+ }
67
+ if (value instanceof Map) {
68
+ const keys = [...value.keys()];
69
+ if (keys.length === 0) {
70
+ return new Map();
71
+ }
72
+ const allNum = keys.every((k) => typeof k === "number");
73
+ if (allNum) {
74
+ const sorted = (keys as number[])
75
+ .slice()
76
+ .sort((a, b) => a - b);
77
+ const m = new Map<number, unknown>();
78
+ for (const k of sorted) {
79
+ m.set(k, canonicalizeCtapValue(value.get(k)));
80
+ }
81
+ return m;
82
+ }
83
+ const allStr = keys.every((k) => typeof k === "string");
84
+ if (allStr) {
85
+ const sorted = (keys as string[])
86
+ .slice()
87
+ .sort(compareCtapTextMapKeys);
88
+ const o: Record<string, unknown> = {};
89
+ for (const k of sorted) {
90
+ o[k] = canonicalizeCtapValue(value.get(k));
91
+ }
92
+ return o;
93
+ }
94
+ throw new Error(
95
+ "CTAP CBOR: map keys must be all numbers or all strings for canonical encoding",
96
+ );
97
+ }
98
+ if (t === "object") {
99
+ const o = value as Record<string, unknown>;
100
+ const keys = Object.keys(o).sort(compareCtapTextMapKeys);
101
+ const out: Record<string, unknown> = {};
102
+ for (const k of keys) {
103
+ out[k] = canonicalizeCtapValue(o[k]);
104
+ }
105
+ return out;
106
+ }
107
+ return value;
108
+ }
109
+
110
+ export function encodeCtapCbor(value: unknown): Uint8Array {
111
+ const encoded = ctapCborEncoder.encode(canonicalizeCtapValue(value));
112
+ if (encoded instanceof Uint8Array) {
113
+ return new Uint8Array(encoded);
114
+ }
115
+ if (ArrayBuffer.isView(encoded)) {
116
+ const v = encoded as ArrayBufferView;
117
+ return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
118
+ }
119
+ return Uint8Array.from(encoded as ArrayLike<number>);
120
+ }
121
+
122
+ export function bytesView(v: unknown): Uint8Array | undefined {
123
+ if (v instanceof Uint8Array) {
124
+ return new Uint8Array(v);
125
+ }
126
+ if (ArrayBuffer.isView(v) && !(v instanceof DataView)) {
127
+ const view = v as ArrayBufferView;
128
+ return new Uint8Array(
129
+ view.buffer,
130
+ view.byteOffset,
131
+ view.byteLength,
132
+ );
133
+ }
134
+ return undefined;
135
+ }
136
+
137
+ export function cborMapGet(
138
+ map: Map<number, unknown> | object,
139
+ key: number,
140
+ ): unknown {
141
+ if (map instanceof Map) {
142
+ return map.get(key);
143
+ }
144
+ if (map !== null && typeof map === "object") {
145
+ const o = map as Record<number | string, unknown>;
146
+ if (key in o) {
147
+ return o[key];
148
+ }
149
+ const s = String(key);
150
+ if (s in o) {
151
+ return o[s];
152
+ }
153
+ }
154
+ return undefined;
155
+ }
@@ -0,0 +1,154 @@
1
+ import { decode } from "cbor-x";
2
+ import {
3
+ parseCtaphidCborFromApduResponse,
4
+ unwrapCtaphidCborBody,
5
+ } from "../apdu";
6
+ import { CTAP_CBOR_ASSERT } from "../constants";
7
+ import { ApduError } from "../errors";
8
+ import { bytesView, cborMapGet, encodeCtapCbor } from "./cborUtils";
9
+
10
+ export interface AuthenticatorGetAssertionRequest {
11
+ rpId: string;
12
+ clientDataHash: Uint8Array;
13
+ allowCredentials?: Array<{ id: Uint8Array; type?: string }>;
14
+ extensions?: Record<string, unknown>;
15
+ options?: { up?: boolean; uv?: boolean };
16
+ }
17
+
18
+ export interface AuthenticatorGetAssertionResponse {
19
+ credential?: {
20
+ id: Uint8Array;
21
+ type?: string;
22
+ };
23
+ authData: Uint8Array;
24
+ signature: Uint8Array;
25
+ user?: {
26
+ id: Uint8Array;
27
+ name?: string;
28
+ displayName?: string;
29
+ icon?: string;
30
+ };
31
+ numberOfCredentials?: number;
32
+ largeBlobKey?: Uint8Array;
33
+ }
34
+
35
+ function buildRequestMap(
36
+ req: AuthenticatorGetAssertionRequest,
37
+ ): Map<number, unknown> {
38
+ const m = new Map<number, unknown>();
39
+ m.set(1, req.rpId);
40
+ m.set(2, req.clientDataHash);
41
+ if (req.allowCredentials?.length) {
42
+ m.set(
43
+ 3,
44
+ req.allowCredentials.map((c) => ({
45
+ id: c.id,
46
+ type: c.type ?? "public-key",
47
+ })),
48
+ );
49
+ }
50
+ if (req.extensions && Object.keys(req.extensions).length > 0) {
51
+ m.set(4, req.extensions);
52
+ }
53
+ if (
54
+ req.options &&
55
+ (req.options.up !== undefined || req.options.uv !== undefined)
56
+ ) {
57
+ const opt: Record<string, boolean> = {};
58
+ if (req.options.up !== undefined) {
59
+ opt.up = req.options.up;
60
+ }
61
+ if (req.options.uv !== undefined) {
62
+ opt.uv = req.options.uv;
63
+ }
64
+ m.set(5, opt);
65
+ }
66
+ return m;
67
+ }
68
+
69
+ function decodeAssertionMap(
70
+ decoded: Map<number, unknown> | object,
71
+ ): AuthenticatorGetAssertionResponse {
72
+ const authData = bytesView(cborMapGet(decoded, 2));
73
+ const signature = bytesView(cborMapGet(decoded, 3));
74
+ if (!authData || !signature) {
75
+ throw new ApduError(
76
+ "missing authData (2) or signature (3) in response",
77
+ );
78
+ }
79
+ const out: AuthenticatorGetAssertionResponse = {
80
+ authData,
81
+ signature,
82
+ };
83
+
84
+ const credRaw = cborMapGet(decoded, 1);
85
+ if (credRaw !== null && typeof credRaw === "object") {
86
+ const o = credRaw as Record<string, unknown>;
87
+ const id = bytesView(o.id);
88
+ if (id) {
89
+ out.credential = {
90
+ id,
91
+ type: typeof o.type === "string" ? o.type : undefined,
92
+ };
93
+ }
94
+ }
95
+
96
+ const userRaw = cborMapGet(decoded, 4);
97
+ if (userRaw !== null && typeof userRaw === "object") {
98
+ const o = userRaw as Record<string, unknown>;
99
+ const id = bytesView(o.id);
100
+ if (id) {
101
+ out.user = {
102
+ id,
103
+ name: typeof o.name === "string" ? o.name : undefined,
104
+ displayName:
105
+ typeof o.displayName === "string"
106
+ ? o.displayName
107
+ : undefined,
108
+ icon: typeof o.icon === "string" ? o.icon : undefined,
109
+ };
110
+ }
111
+ }
112
+
113
+ const nRaw = cborMapGet(decoded, 5);
114
+ let n: number | undefined;
115
+ if (typeof nRaw === "number" && Number.isFinite(nRaw)) {
116
+ n = Math.trunc(nRaw);
117
+ } else if (typeof nRaw === "bigint") {
118
+ n = Number(nRaw);
119
+ }
120
+ if (n !== undefined) {
121
+ out.numberOfCredentials = n;
122
+ }
123
+
124
+ const lbk = bytesView(cborMapGet(decoded, 7));
125
+ if (lbk) {
126
+ out.largeBlobKey = lbk;
127
+ }
128
+ return out;
129
+ }
130
+
131
+ export function encodeAuthenticatorGetAssertionRequest(
132
+ req: AuthenticatorGetAssertionRequest,
133
+ ): Uint8Array {
134
+ if (req.clientDataHash.length !== 32) {
135
+ throw new ApduError(
136
+ "clientDataHash must be 32 bytes (SHA-256)",
137
+ );
138
+ }
139
+ const enc = encodeCtapCbor(buildRequestMap(req));
140
+ const frame = new Uint8Array(1 + enc.length);
141
+ frame[0] = CTAP_CBOR_ASSERT;
142
+ frame.set(enc, 1);
143
+ return frame;
144
+ }
145
+
146
+ export function parseAuthenticatorGetAssertionResponse(
147
+ apduResponseIncludingSw: Uint8Array,
148
+ ): AuthenticatorGetAssertionResponse {
149
+ const cborMapBytes = unwrapCtaphidCborBody(
150
+ parseCtaphidCborFromApduResponse(apduResponseIncludingSw),
151
+ );
152
+ const decoded = decode(cborMapBytes) as Map<number, unknown> | object;
153
+ return decodeAssertionMap(decoded);
154
+ }