@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.
- package/README.md +65 -0
- package/lib/apdu.d.ts +56 -0
- package/lib/apdu.d.ts.map +1 -0
- package/lib/apdu.js +225 -0
- package/lib/buildRequests.d.ts +11 -0
- package/lib/buildRequests.d.ts.map +1 -0
- package/lib/buildRequests.js +24 -0
- package/lib/constants.d.ts +11 -0
- package/lib/constants.d.ts.map +1 -0
- package/lib/constants.js +15 -0
- package/lib/ctap2/cborUtils.d.ts +14 -0
- package/lib/ctap2/cborUtils.d.ts.map +1 -0
- package/lib/ctap2/cborUtils.js +141 -0
- package/lib/ctap2/getAssertion.d.ts +32 -0
- package/lib/ctap2/getAssertion.d.ts.map +1 -0
- package/lib/ctap2/getAssertion.js +104 -0
- package/lib/ctap2/makeCredential.d.ts +37 -0
- package/lib/ctap2/makeCredential.d.ts.map +1 -0
- package/lib/ctap2/makeCredential.js +118 -0
- package/lib/errors.d.ts +7 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +13 -0
- package/lib/helpers/authDataExtensions.d.ts +9 -0
- package/lib/helpers/authDataExtensions.d.ts.map +1 -0
- package/lib/helpers/authDataExtensions.js +192 -0
- package/lib/helpers/base64url.d.ts +3 -0
- package/lib/helpers/base64url.d.ts.map +1 -0
- package/lib/helpers/base64url.js +29 -0
- package/lib/helpers/clientData.d.ts +9 -0
- package/lib/helpers/clientData.d.ts.map +1 -0
- package/lib/helpers/clientData.js +18 -0
- package/lib/helpers/fromWebAuthnJson.d.ts +6 -0
- package/lib/helpers/fromWebAuthnJson.d.ts.map +1 -0
- package/lib/helpers/fromWebAuthnJson.js +125 -0
- package/lib/helpers/webauthnResponses.d.ts +21 -0
- package/lib/helpers/webauthnResponses.d.ts.map +1 -0
- package/lib/helpers/webauthnResponses.js +51 -0
- package/lib/helpers/webauthnTypes.d.ts +6 -0
- package/lib/helpers/webauthnTypes.d.ts.map +1 -0
- package/lib/helpers/webauthnTypes.js +2 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +8 -0
- package/lib/parseResponses.d.ts +19 -0
- package/lib/parseResponses.d.ts.map +1 -0
- package/lib/parseResponses.js +37 -0
- package/lib/publicApi.d.ts +15 -0
- package/lib/publicApi.d.ts.map +1 -0
- package/lib/publicApi.js +83 -0
- package/lib/types.d.ts +10 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/package.json +35 -0
- package/src/apdu.ts +285 -0
- package/src/constants.ts +16 -0
- package/src/ctap2/cborUtils.ts +155 -0
- package/src/ctap2/getAssertion.ts +154 -0
- package/src/ctap2/makeCredential.ts +173 -0
- package/src/errors.ts +11 -0
- package/src/helpers/authDataExtensions.ts +214 -0
- package/src/helpers/base64url.ts +24 -0
- package/src/helpers/clientData.ts +24 -0
- package/src/helpers/fromWebAuthnJson.ts +175 -0
- package/src/helpers/webauthnResponses.ts +113 -0
- package/src/index.ts +6 -0
- package/src/parseResponses.ts +67 -0
- package/src/publicApi.ts +152 -0
- package/src/types.ts +16 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { decode } from "cbor-x";
|
|
2
|
+
import {
|
|
3
|
+
parseCtaphidCborFromApduResponse,
|
|
4
|
+
unwrapCtaphidCborBody,
|
|
5
|
+
} from "../apdu";
|
|
6
|
+
import { CTAP_CBOR_MAKE_CRED } from "../constants";
|
|
7
|
+
import { ApduError } from "../errors";
|
|
8
|
+
import { bytesView, cborMapGet, encodeCtapCbor } from "./cborUtils";
|
|
9
|
+
|
|
10
|
+
export interface AuthenticatorMakeCredentialRequest {
|
|
11
|
+
clientDataHash: Uint8Array;
|
|
12
|
+
rp: { id: string; name: string };
|
|
13
|
+
user: {
|
|
14
|
+
id: Uint8Array;
|
|
15
|
+
name: string;
|
|
16
|
+
displayName: string;
|
|
17
|
+
icon?: string;
|
|
18
|
+
};
|
|
19
|
+
pubKeyCredParams: Array<{ alg: number; type: string }>;
|
|
20
|
+
excludeCredentials?: Array<{ id: Uint8Array; type?: string }>;
|
|
21
|
+
extensions?: Record<string, unknown>;
|
|
22
|
+
options?: { rk?: boolean; uv?: boolean };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AuthenticatorMakeCredentialResponse {
|
|
26
|
+
fmt: string;
|
|
27
|
+
authData: Uint8Array;
|
|
28
|
+
attStmt: Record<string, unknown>;
|
|
29
|
+
largeBlobKey?: Uint8Array;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildMakeCredentialMap(
|
|
33
|
+
req: AuthenticatorMakeCredentialRequest,
|
|
34
|
+
): Map<number, unknown> {
|
|
35
|
+
const m = new Map<number, unknown>();
|
|
36
|
+
m.set(1, req.clientDataHash);
|
|
37
|
+
m.set(2, { id: req.rp.id, name: req.rp.name });
|
|
38
|
+
const u: Record<string, unknown> = {
|
|
39
|
+
id: req.user.id,
|
|
40
|
+
name: req.user.name,
|
|
41
|
+
displayName: req.user.displayName,
|
|
42
|
+
};
|
|
43
|
+
if (req.user.icon !== undefined) {
|
|
44
|
+
u.icon = req.user.icon;
|
|
45
|
+
}
|
|
46
|
+
m.set(3, u);
|
|
47
|
+
m.set(
|
|
48
|
+
4,
|
|
49
|
+
req.pubKeyCredParams.map((p) => ({
|
|
50
|
+
alg: p.alg,
|
|
51
|
+
type: p.type ?? "public-key",
|
|
52
|
+
})),
|
|
53
|
+
);
|
|
54
|
+
if (req.excludeCredentials?.length) {
|
|
55
|
+
m.set(
|
|
56
|
+
5,
|
|
57
|
+
req.excludeCredentials.map((c) => ({
|
|
58
|
+
id: c.id,
|
|
59
|
+
type: c.type ?? "public-key",
|
|
60
|
+
})),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (req.extensions && Object.keys(req.extensions).length > 0) {
|
|
64
|
+
m.set(6, req.extensions);
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
req.options &&
|
|
68
|
+
(req.options.rk !== undefined || req.options.uv !== undefined)
|
|
69
|
+
) {
|
|
70
|
+
const opt: Record<string, boolean> = {};
|
|
71
|
+
if (req.options.rk !== undefined) {
|
|
72
|
+
opt.rk = req.options.rk;
|
|
73
|
+
}
|
|
74
|
+
if (req.options.uv !== undefined) {
|
|
75
|
+
opt.uv = req.options.uv;
|
|
76
|
+
}
|
|
77
|
+
m.set(7, opt);
|
|
78
|
+
}
|
|
79
|
+
return m;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function encodeAuthenticatorMakeCredentialRequest(
|
|
83
|
+
req: AuthenticatorMakeCredentialRequest,
|
|
84
|
+
): Uint8Array {
|
|
85
|
+
if (req.clientDataHash.length !== 32) {
|
|
86
|
+
throw new ApduError(
|
|
87
|
+
"clientDataHash must be 32 bytes (SHA-256)",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!req.pubKeyCredParams.length) {
|
|
91
|
+
throw new ApduError("pubKeyCredParams must not be empty");
|
|
92
|
+
}
|
|
93
|
+
const enc = encodeCtapCbor(buildMakeCredentialMap(req));
|
|
94
|
+
const frame = new Uint8Array(1 + enc.length);
|
|
95
|
+
frame[0] = CTAP_CBOR_MAKE_CRED;
|
|
96
|
+
frame.set(enc, 1);
|
|
97
|
+
return frame;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseAuthenticatorMakeCredentialResponse(
|
|
101
|
+
apduResponseIncludingSw: Uint8Array,
|
|
102
|
+
): AuthenticatorMakeCredentialResponse {
|
|
103
|
+
const cborMapBytes = unwrapCtaphidCborBody(
|
|
104
|
+
parseCtaphidCborFromApduResponse(apduResponseIncludingSw),
|
|
105
|
+
);
|
|
106
|
+
const decoded = decode(cborMapBytes) as Map<number, unknown> | object;
|
|
107
|
+
const fmtRaw = cborMapGet(decoded, 1);
|
|
108
|
+
const fmt = typeof fmtRaw === "string" ? fmtRaw : undefined;
|
|
109
|
+
const authData = bytesView(cborMapGet(decoded, 2));
|
|
110
|
+
const attStmtRaw = cborMapGet(decoded, 3);
|
|
111
|
+
if (
|
|
112
|
+
attStmtRaw === null ||
|
|
113
|
+
typeof attStmtRaw !== "object" ||
|
|
114
|
+
Array.isArray(attStmtRaw)
|
|
115
|
+
) {
|
|
116
|
+
throw new ApduError("attStmt (key 3) must be a CBOR map");
|
|
117
|
+
}
|
|
118
|
+
const attStmt = attStmtRaw as Record<string, unknown>;
|
|
119
|
+
if (!fmt || !authData) {
|
|
120
|
+
throw new ApduError(
|
|
121
|
+
"missing fmt (1) or authData (2) in makeCredential response",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const out: AuthenticatorMakeCredentialResponse = {
|
|
125
|
+
fmt,
|
|
126
|
+
authData,
|
|
127
|
+
attStmt,
|
|
128
|
+
};
|
|
129
|
+
const lbk = bytesView(cborMapGet(decoded, 5));
|
|
130
|
+
if (lbk) {
|
|
131
|
+
out.largeBlobKey = lbk;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildAttestationObjectBytes(
|
|
137
|
+
fmt: string,
|
|
138
|
+
authData: Uint8Array,
|
|
139
|
+
attStmt: Record<string, unknown>,
|
|
140
|
+
): Uint8Array {
|
|
141
|
+
// WebAuthn attestation object is CBOR; use CTAP2 canonical rules (CTAP §8) for interoperability.
|
|
142
|
+
return encodeCtapCbor({
|
|
143
|
+
fmt,
|
|
144
|
+
authData,
|
|
145
|
+
attStmt,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function extractCredentialIdFromAuthData(
|
|
150
|
+
authData: Uint8Array,
|
|
151
|
+
): Uint8Array {
|
|
152
|
+
if (authData.length < 37) {
|
|
153
|
+
throw new ApduError("authData too short");
|
|
154
|
+
}
|
|
155
|
+
const flags = authData[32];
|
|
156
|
+
if ((flags & 0x40) === 0) {
|
|
157
|
+
throw new ApduError(
|
|
158
|
+
"authData missing AT flag (attested credential data)",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
let off = 37 + 16;
|
|
162
|
+
if (off + 2 > authData.length) {
|
|
163
|
+
throw new ApduError(
|
|
164
|
+
"authData truncated before credential id length",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const credIdLen = (authData[off] << 8) | authData[off + 1];
|
|
168
|
+
off += 2;
|
|
169
|
+
if (off + credIdLen > authData.length) {
|
|
170
|
+
throw new ApduError("credential id length out of range");
|
|
171
|
+
}
|
|
172
|
+
return authData.slice(off, off + credIdLen);
|
|
173
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class ApduError extends Error {
|
|
2
|
+
declare readonly cause?: unknown;
|
|
3
|
+
|
|
4
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ApduError";
|
|
7
|
+
if (options?.cause !== undefined) {
|
|
8
|
+
(this as Error & { cause?: unknown }).cause = options.cause;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { decode } from "cbor-x";
|
|
2
|
+
|
|
3
|
+
/** WebAuthn authenticator data flags (byte index 32). */
|
|
4
|
+
const FLAG_AT = 0x40;
|
|
5
|
+
const FLAG_ED = 0x80;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Skip one definite CBOR data item (RFC 8949). Used to skip the COSE public key
|
|
9
|
+
* in attested credential data. Indefinite-length items throw (not used in WebAuthn authData).
|
|
10
|
+
*/
|
|
11
|
+
function skipOneCborItem(buf: Uint8Array, i: number): number {
|
|
12
|
+
const first = buf[i];
|
|
13
|
+
const mt = first >> 5;
|
|
14
|
+
const ai = first & 0x1f;
|
|
15
|
+
let p = i + 1;
|
|
16
|
+
|
|
17
|
+
if (mt === 0 || mt === 1) {
|
|
18
|
+
if (ai < 24) {
|
|
19
|
+
return p;
|
|
20
|
+
}
|
|
21
|
+
if (ai === 24) {
|
|
22
|
+
return p + 1;
|
|
23
|
+
}
|
|
24
|
+
if (ai === 25) {
|
|
25
|
+
return p + 2;
|
|
26
|
+
}
|
|
27
|
+
if (ai === 26) {
|
|
28
|
+
return p + 4;
|
|
29
|
+
}
|
|
30
|
+
if (ai === 27) {
|
|
31
|
+
return p + 8;
|
|
32
|
+
}
|
|
33
|
+
throw new Error("CBOR: invalid integer encoding");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (mt === 2 || mt === 3) {
|
|
37
|
+
const { len, next } = readLength(ai, buf, p);
|
|
38
|
+
return next + len;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (mt === 4) {
|
|
42
|
+
if (ai === 31) {
|
|
43
|
+
throw new Error("CBOR: indefinite array not supported in authData");
|
|
44
|
+
}
|
|
45
|
+
const { len, next } = readLength(ai, buf, p);
|
|
46
|
+
let q = next;
|
|
47
|
+
for (let k = 0; k < len; k++) {
|
|
48
|
+
q = skipOneCborItem(buf, q);
|
|
49
|
+
}
|
|
50
|
+
return q;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (mt === 5) {
|
|
54
|
+
if (ai === 31) {
|
|
55
|
+
throw new Error("CBOR: indefinite map not supported in authData");
|
|
56
|
+
}
|
|
57
|
+
const { len, next } = readLength(ai, buf, p);
|
|
58
|
+
let q = next;
|
|
59
|
+
for (let k = 0; k < len; k++) {
|
|
60
|
+
q = skipOneCborItem(buf, q);
|
|
61
|
+
q = skipOneCborItem(buf, q);
|
|
62
|
+
}
|
|
63
|
+
return q;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (mt === 6) {
|
|
67
|
+
const { next } = readTagNumber(ai, buf, p);
|
|
68
|
+
return skipOneCborItem(buf, next);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (mt === 7) {
|
|
72
|
+
if (ai < 24) {
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
if (ai === 24) {
|
|
76
|
+
return p + 1;
|
|
77
|
+
}
|
|
78
|
+
if (ai === 25) {
|
|
79
|
+
return p + 2;
|
|
80
|
+
}
|
|
81
|
+
if (ai === 26) {
|
|
82
|
+
return p + 5;
|
|
83
|
+
}
|
|
84
|
+
if (ai === 27) {
|
|
85
|
+
return p + 9;
|
|
86
|
+
}
|
|
87
|
+
throw new Error("CBOR: invalid simple/float encoding");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error("CBOR: unknown major type");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readLength(
|
|
94
|
+
ai: number,
|
|
95
|
+
buf: Uint8Array,
|
|
96
|
+
p: number,
|
|
97
|
+
): { len: number; next: number } {
|
|
98
|
+
if (ai < 24) {
|
|
99
|
+
return { len: ai, next: p };
|
|
100
|
+
}
|
|
101
|
+
if (ai === 24) {
|
|
102
|
+
return { len: buf[p], next: p + 1 };
|
|
103
|
+
}
|
|
104
|
+
if (ai === 25) {
|
|
105
|
+
return { len: (buf[p] << 8) | buf[p + 1], next: p + 2 };
|
|
106
|
+
}
|
|
107
|
+
if (ai === 26) {
|
|
108
|
+
const len =
|
|
109
|
+
((buf[p] << 24) |
|
|
110
|
+
(buf[p + 1] << 16) |
|
|
111
|
+
(buf[p + 2] << 8) |
|
|
112
|
+
buf[p + 3]) >>>
|
|
113
|
+
0;
|
|
114
|
+
return { len, next: p + 4 };
|
|
115
|
+
}
|
|
116
|
+
if (ai === 27) {
|
|
117
|
+
const hi = (buf[p] << 24) | (buf[p + 1] << 16) | (buf[p + 2] << 8) | buf[p + 3];
|
|
118
|
+
const lo =
|
|
119
|
+
(buf[p + 4] << 24) |
|
|
120
|
+
(buf[p + 5] << 16) |
|
|
121
|
+
(buf[p + 6] << 8) |
|
|
122
|
+
buf[p + 7];
|
|
123
|
+
const n = (BigInt(hi >>> 0) << 32n) | BigInt(lo >>> 0);
|
|
124
|
+
if (n > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
125
|
+
throw new Error("CBOR: length too large");
|
|
126
|
+
}
|
|
127
|
+
return { len: Number(n), next: p + 8 };
|
|
128
|
+
}
|
|
129
|
+
throw new Error("CBOR: invalid length encoding");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readTagNumber(
|
|
133
|
+
ai: number,
|
|
134
|
+
buf: Uint8Array,
|
|
135
|
+
p: number,
|
|
136
|
+
): { next: number } {
|
|
137
|
+
if (ai < 24) {
|
|
138
|
+
return { next: p };
|
|
139
|
+
}
|
|
140
|
+
if (ai === 24) {
|
|
141
|
+
return { next: p + 1 };
|
|
142
|
+
}
|
|
143
|
+
if (ai === 25) {
|
|
144
|
+
return { next: p + 2 };
|
|
145
|
+
}
|
|
146
|
+
if (ai === 26) {
|
|
147
|
+
return { next: p + 4 };
|
|
148
|
+
}
|
|
149
|
+
if (ai === 27) {
|
|
150
|
+
return { next: p + 8 };
|
|
151
|
+
}
|
|
152
|
+
throw new Error("CBOR: invalid tag encoding");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Advance past attested credential data (starts at `start`, first byte of AAGUID).
|
|
157
|
+
*/
|
|
158
|
+
function skipAttestedCredentialData(authData: Uint8Array, start: number): number {
|
|
159
|
+
if (authData.length < start + 18) {
|
|
160
|
+
throw new Error("authData: truncated attested credential data header");
|
|
161
|
+
}
|
|
162
|
+
let off = start + 16;
|
|
163
|
+
const credIdLen = (authData[off] << 8) | authData[off + 1];
|
|
164
|
+
off += 2 + credIdLen;
|
|
165
|
+
if (off > authData.length) {
|
|
166
|
+
throw new Error("authData: credential id out of range");
|
|
167
|
+
}
|
|
168
|
+
return skipOneCborItem(authData, off);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function cborExtensionsToRecord(decoded: unknown): Record<string, unknown> {
|
|
172
|
+
if (decoded instanceof Map) {
|
|
173
|
+
const o: Record<string, unknown> = {};
|
|
174
|
+
for (const [k, v] of decoded) {
|
|
175
|
+
if (typeof k === "string") {
|
|
176
|
+
o[k] = v;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return o;
|
|
180
|
+
}
|
|
181
|
+
if (decoded !== null && typeof decoded === "object" && !Array.isArray(decoded)) {
|
|
182
|
+
return { ...(decoded as Record<string, unknown>) };
|
|
183
|
+
}
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Parse the **extensions** CBOR map from WebAuthn authenticator data (ED flag).
|
|
189
|
+
* This is what browsers merge into `getClientExtensionResults()` for authenticator
|
|
190
|
+
* extension outputs; pure **client** extensions are not present here.
|
|
191
|
+
*
|
|
192
|
+
* @returns Plain object suitable for `clientExtensionResults`, or `{}` if ED is clear.
|
|
193
|
+
*/
|
|
194
|
+
export function parseAuthenticatorDataExtensions(
|
|
195
|
+
authData: Uint8Array,
|
|
196
|
+
): Record<string, unknown> {
|
|
197
|
+
if (authData.length < 37) {
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
const flags = authData[32];
|
|
201
|
+
let offset = 37;
|
|
202
|
+
if (flags & FLAG_AT) {
|
|
203
|
+
offset = skipAttestedCredentialData(authData, offset);
|
|
204
|
+
}
|
|
205
|
+
if ((flags & FLAG_ED) === 0) {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
if (offset >= authData.length) {
|
|
209
|
+
return {};
|
|
210
|
+
}
|
|
211
|
+
const extBytes = authData.subarray(offset);
|
|
212
|
+
const decoded = decode(extBytes);
|
|
213
|
+
return cborExtensionsToRecord(decoded);
|
|
214
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function base64URLStringToBuffer(base64URLString: string) {
|
|
2
|
+
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/");
|
|
3
|
+
const padLength = (4 - (base64.length % 4)) % 4;
|
|
4
|
+
const padded = base64.padEnd(base64.length + padLength, "=");
|
|
5
|
+
const binary = atob(padded);
|
|
6
|
+
const buffer = new ArrayBuffer(binary.length);
|
|
7
|
+
const bytes = new Uint8Array(buffer);
|
|
8
|
+
for (let i = 0; i < binary.length; i++) {
|
|
9
|
+
bytes[i] = binary.charCodeAt(i);
|
|
10
|
+
}
|
|
11
|
+
return buffer;
|
|
12
|
+
}
|
|
13
|
+
export function bufferToBase64URLString(buffer: Uint8Array) {
|
|
14
|
+
const bytes = new Uint8Array(buffer);
|
|
15
|
+
let str = "";
|
|
16
|
+
for (const charCode of bytes) {
|
|
17
|
+
str += String.fromCharCode(charCode);
|
|
18
|
+
}
|
|
19
|
+
const base64String = btoa(str);
|
|
20
|
+
return base64String
|
|
21
|
+
.replace(/\+/g, "-")
|
|
22
|
+
.replace(/\//g, "_")
|
|
23
|
+
.replace(/=/g, "");
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface CollectedClientDataInput {
|
|
2
|
+
challenge: string;
|
|
3
|
+
origin: string;
|
|
4
|
+
crossOrigin: boolean;
|
|
5
|
+
topOrigin?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function buildCollectedClientDataJSON(
|
|
9
|
+
type: "webauthn.get" | "webauthn.create",
|
|
10
|
+
input: CollectedClientDataInput,
|
|
11
|
+
): string {
|
|
12
|
+
const o: Record<string, unknown> = {
|
|
13
|
+
type,
|
|
14
|
+
challenge: input.challenge,
|
|
15
|
+
origin: input.origin,
|
|
16
|
+
};
|
|
17
|
+
if (input.crossOrigin !== undefined) {
|
|
18
|
+
o.crossOrigin = input.crossOrigin;
|
|
19
|
+
}
|
|
20
|
+
if (input.topOrigin !== undefined) {
|
|
21
|
+
o.topOrigin = input.topOrigin;
|
|
22
|
+
}
|
|
23
|
+
return JSON.stringify(o);
|
|
24
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
|
+
import { utf8ToBytes } from "@noble/hashes/utils.js";
|
|
3
|
+
import type { UserVerificationRequirement } from "@simplewebauthn/browser";
|
|
4
|
+
import { ApduError } from "../errors";
|
|
5
|
+
import { type AuthenticatorGetAssertionRequest } from "../ctap2/getAssertion";
|
|
6
|
+
import { type AuthenticatorMakeCredentialRequest } from "../ctap2/makeCredential";
|
|
7
|
+
import {
|
|
8
|
+
PublicKeyCredentialCreationOptionsJSONWithNfc,
|
|
9
|
+
PublicKeyCredentialRequestOptionsJSONWithNfc,
|
|
10
|
+
} from "../types";
|
|
11
|
+
import { base64URLStringToBuffer } from "./base64url";
|
|
12
|
+
import { buildCollectedClientDataJSON } from "./clientData";
|
|
13
|
+
|
|
14
|
+
function userVerificationToOptionalUvBool(
|
|
15
|
+
uv: UserVerificationRequirement | undefined,
|
|
16
|
+
): boolean | undefined {
|
|
17
|
+
if (uv === "required") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (uv === "discouraged") {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mapAuthenticatorSelection(
|
|
27
|
+
sel: PublicKeyCredentialCreationOptionsJSONWithNfc["authenticatorSelection"],
|
|
28
|
+
): { rk?: boolean; uv?: boolean } | undefined {
|
|
29
|
+
if (!sel) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
let rk: boolean | undefined;
|
|
33
|
+
if (sel.residentKey === "required" || sel.residentKey === "preferred") {
|
|
34
|
+
rk = true;
|
|
35
|
+
} else if (sel.residentKey === "discouraged") {
|
|
36
|
+
rk = false;
|
|
37
|
+
} else if (sel.requireResidentKey === true) {
|
|
38
|
+
rk = true;
|
|
39
|
+
}
|
|
40
|
+
const uv = userVerificationToOptionalUvBool(sel.userVerification);
|
|
41
|
+
if (rk === undefined && uv === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const o: { rk?: boolean; uv?: boolean } = {};
|
|
45
|
+
if (rk !== undefined) {
|
|
46
|
+
o.rk = rk;
|
|
47
|
+
}
|
|
48
|
+
if (uv !== undefined) {
|
|
49
|
+
o.uv = uv;
|
|
50
|
+
}
|
|
51
|
+
return o;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pickCtapExtensions(
|
|
55
|
+
ext: Record<string, unknown> | undefined,
|
|
56
|
+
mode: "assertion" | "registration",
|
|
57
|
+
): Record<string, unknown> | undefined {
|
|
58
|
+
if (!ext) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const out: Record<string, unknown> = {};
|
|
62
|
+
for (const [k, v] of Object.entries(ext)) {
|
|
63
|
+
if (v == null) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (mode === "assertion" && k === "credProps") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
out[k] = v;
|
|
70
|
+
}
|
|
71
|
+
return Object.keys(out).length ? out : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function authenticatorGetAssertionRequestFromPublicKeyCredentialRequestOptionsJSON(
|
|
75
|
+
args: PublicKeyCredentialRequestOptionsJSONWithNfc,
|
|
76
|
+
): AuthenticatorGetAssertionRequest {
|
|
77
|
+
const {
|
|
78
|
+
challenge,
|
|
79
|
+
rpId,
|
|
80
|
+
allowCredentials,
|
|
81
|
+
userVerification,
|
|
82
|
+
extensions,
|
|
83
|
+
crossOrigin = false,
|
|
84
|
+
topOrigin,
|
|
85
|
+
origin,
|
|
86
|
+
} = args;
|
|
87
|
+
|
|
88
|
+
if (!rpId) throw new ApduError("WebAuthn options: rpId is required");
|
|
89
|
+
|
|
90
|
+
const clientDataJSON = buildCollectedClientDataJSON("webauthn.get", {
|
|
91
|
+
challenge,
|
|
92
|
+
origin,
|
|
93
|
+
crossOrigin,
|
|
94
|
+
topOrigin,
|
|
95
|
+
});
|
|
96
|
+
const clientDataHash = sha256(utf8ToBytes(clientDataJSON));
|
|
97
|
+
|
|
98
|
+
const uv = userVerificationToOptionalUvBool(userVerification);
|
|
99
|
+
const uvOpts = uv === undefined ? undefined : { up: true as const, uv };
|
|
100
|
+
|
|
101
|
+
const parsedExtensions = extensions
|
|
102
|
+
? pickCtapExtensions(
|
|
103
|
+
extensions as Record<string, unknown> | undefined,
|
|
104
|
+
"assertion",
|
|
105
|
+
)
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
rpId,
|
|
110
|
+
clientDataHash,
|
|
111
|
+
allowCredentials: allowCredentials?.map((d) => ({
|
|
112
|
+
id: new Uint8Array(base64URLStringToBuffer(d.id)),
|
|
113
|
+
type: d.type ?? "public-key",
|
|
114
|
+
})),
|
|
115
|
+
extensions: parsedExtensions,
|
|
116
|
+
options: uvOpts,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function authenticatorMakeCredentialRequestFromPublicKeyCredentialCreationOptionsJSON(
|
|
121
|
+
args: PublicKeyCredentialCreationOptionsJSONWithNfc,
|
|
122
|
+
): AuthenticatorMakeCredentialRequest {
|
|
123
|
+
const {
|
|
124
|
+
challenge,
|
|
125
|
+
user,
|
|
126
|
+
excludeCredentials,
|
|
127
|
+
extensions,
|
|
128
|
+
authenticatorSelection,
|
|
129
|
+
crossOrigin = false,
|
|
130
|
+
topOrigin,
|
|
131
|
+
origin,
|
|
132
|
+
pubKeyCredParams,
|
|
133
|
+
rp,
|
|
134
|
+
} = args;
|
|
135
|
+
|
|
136
|
+
if (!rp.id) throw new ApduError("WebAuthn options: rp.id is required");
|
|
137
|
+
|
|
138
|
+
const clientDataJSON = buildCollectedClientDataJSON("webauthn.create", {
|
|
139
|
+
challenge,
|
|
140
|
+
origin,
|
|
141
|
+
crossOrigin,
|
|
142
|
+
topOrigin,
|
|
143
|
+
});
|
|
144
|
+
const clientDataHash = sha256(utf8ToBytes(clientDataJSON));
|
|
145
|
+
|
|
146
|
+
const userId = new Uint8Array(base64URLStringToBuffer(user.id));
|
|
147
|
+
|
|
148
|
+
const parsedExtensions = extensions
|
|
149
|
+
? pickCtapExtensions(
|
|
150
|
+
extensions as Record<string, unknown> | undefined,
|
|
151
|
+
"registration",
|
|
152
|
+
)
|
|
153
|
+
: undefined;
|
|
154
|
+
const opt = mapAuthenticatorSelection(authenticatorSelection);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
clientDataHash,
|
|
158
|
+
rp: { id: rp.id, name: rp.name },
|
|
159
|
+
user: {
|
|
160
|
+
id: userId,
|
|
161
|
+
name: user.name,
|
|
162
|
+
displayName: user.displayName,
|
|
163
|
+
},
|
|
164
|
+
pubKeyCredParams: pubKeyCredParams.map((p) => ({
|
|
165
|
+
alg: p.alg,
|
|
166
|
+
type: p.type ?? "public-key",
|
|
167
|
+
})),
|
|
168
|
+
excludeCredentials: excludeCredentials?.map((d) => ({
|
|
169
|
+
id: new Uint8Array(base64URLStringToBuffer(d.id)),
|
|
170
|
+
type: d.type ?? "public-key",
|
|
171
|
+
})),
|
|
172
|
+
extensions: parsedExtensions,
|
|
173
|
+
options: opt,
|
|
174
|
+
};
|
|
175
|
+
}
|