1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
import {
AuthenticationExtensionsAuthenticatorOutputs,
decodeAuthenticatorExtensions,
} from './decodeAuthenticatorExtensions.ts';
import { isoCBOR, isoUint8Array } from './iso/index.ts';
import { COSEPublicKey } from './cose.ts';
/**
* Make sense of the authData buffer contained in an Attestation
*/
export function parseAuthenticatorData(
authData: Uint8Array,
): ParsedAuthenticatorData {
if (authData.byteLength < 37) {
throw new Error(
`Authenticator data was ${authData.byteLength} bytes, expected at least 37 bytes`,
);
}
let pointer = 0;
const dataView = isoUint8Array.toDataView(authData);
const rpIdHash = authData.slice(pointer, pointer += 32);
const flagsBuf = authData.slice(pointer, pointer += 1);
const flagsInt = flagsBuf[0];
// Bit positions can be referenced here:
// https://www.w3.org/TR/webauthn-2/#flags
const flags = {
up: !!(flagsInt & (1 << 0)), // User Presence
uv: !!(flagsInt & (1 << 2)), // User Verified
be: !!(flagsInt & (1 << 3)), // Backup Eligibility
bs: !!(flagsInt & (1 << 4)), // Backup State
at: !!(flagsInt & (1 << 6)), // Attested Credential Data Present
ed: !!(flagsInt & (1 << 7)), // Extension Data Present
flagsInt,
};
const counterBuf = authData.slice(pointer, pointer + 4);
const counter = dataView.getUint32(pointer, false);
pointer += 4;
let aaguid: Uint8Array | undefined = undefined;
let credentialID: Uint8Array | undefined = undefined;
let credentialPublicKey: Uint8Array | undefined = undefined;
if (flags.at) {
aaguid = authData.slice(pointer, pointer += 16);
const credIDLen = dataView.getUint16(pointer);
pointer += 2;
credentialID = authData.slice(pointer, pointer += credIDLen);
// Decode the next CBOR item in the buffer, then re-encode it back to a Buffer
const firstDecoded = isoCBOR.decodeFirst<COSEPublicKey>(
authData.slice(pointer),
);
const firstEncoded = Uint8Array.from(isoCBOR.encode(firstDecoded));
credentialPublicKey = firstEncoded;
pointer += firstEncoded.byteLength;
}
let extensionsData: AuthenticationExtensionsAuthenticatorOutputs | undefined = undefined;
let extensionsDataBuffer: Uint8Array | undefined = undefined;
if (flags.ed) {
const firstDecoded = isoCBOR.decodeFirst(authData.slice(pointer));
extensionsDataBuffer = Uint8Array.from(isoCBOR.encode(firstDecoded));
extensionsData = decodeAuthenticatorExtensions(extensionsDataBuffer);
pointer += extensionsDataBuffer.byteLength;
}
// Pointer should be at the end of the authenticator data, otherwise too much data was sent
if (authData.byteLength > pointer) {
throw new Error('Leftover bytes detected while parsing authenticator data');
}
return _parseAuthenticatorDataInternals.stubThis({
rpIdHash,
flagsBuf,
flags,
counter,
counterBuf,
aaguid,
credentialID,
credentialPublicKey,
extensionsData,
extensionsDataBuffer,
});
}
export type ParsedAuthenticatorData = {
rpIdHash: Uint8Array;
flagsBuf: Uint8Array;
flags: {
up: boolean;
uv: boolean;
be: boolean;
bs: boolean;
at: boolean;
ed: boolean;
flagsInt: number;
};
counter: number;
counterBuf: Uint8Array;
aaguid?: Uint8Array;
credentialID?: Uint8Array;
credentialPublicKey?: Uint8Array;
extensionsData?: AuthenticationExtensionsAuthenticatorOutputs;
extensionsDataBuffer?: Uint8Array;
};
// Make it possible to stub the return value during testing
export const _parseAuthenticatorDataInternals = {
stubThis: (value: ParsedAuthenticatorData) => value,
};
|