Implement WebAuthn auth for minting PKP and storing encryption condition (#2)

* Implement WebAuthn auth for minting PKP and storing encryption condition

* Update copy

* Update copy
This commit is contained in:
Howard
2023-02-23 14:37:39 -08:00
committed by GitHub
parent 7843588411
commit 339986b666
10 changed files with 1379 additions and 318 deletions

File diff suppressed because it is too large Load Diff

33
src/utils/cbor.ts Normal file
View File

@ -0,0 +1,33 @@
// copy-🍝 from https://github.com/MasterKale/SimpleWebAuthn/blob/33528afe001d4aca62052dce204c0398c3127ffd/packages/server/src/helpers/decodeCbor.ts
export function decodeCborFirst(
cbor: any,
input:
| string
| Buffer
| ArrayBuffer
| Uint8Array
| Uint8ClampedArray
| DataView
): any {
try {
// throws if there are extra bytes
return cbor.decodeFirstSync(input);
} catch (err) {
const _err = err as CborDecoderError;
// if the error was due to extra bytes, return the unpacked value
if (_err.value) {
return _err.value;
}
throw err;
}
}
/**
* Intuited from a quick scan of `cbor.decodeFirstSync()` here:
*
* https://github.com/hildjj/node-cbor/blob/v5.1.0/lib/decoder.js#L189
*/
class CborDecoderError extends Error {
value: any;
}

View File

@ -0,0 +1,39 @@
// copy-🍝 from https://github.com/MasterKale/SimpleWebAuthn/blob/33528afe001d4aca62052dce204c0398c3127ffd/packages/server/src/helpers/decodeAttestationObject.ts#L8
/**
* Convert an AttestationObject buffer to a proper object
*
* @param base64AttestationObject Attestation Object buffer
*/
export function decodeAttestationObject(
cbor: any,
attestationObject: Buffer
): AttestationObject {
const toCBOR: AttestationObject = cbor.decodeAllSync(attestationObject)[0];
return toCBOR;
}
export type AttestationFormat =
| "fido-u2f"
| "packed"
| "android-safetynet"
| "android-key"
| "tpm"
| "apple"
| "none";
export type AttestationObject = {
fmt: AttestationFormat;
attStmt: AttestationStatement;
authData: Buffer;
};
export type AttestationStatement = {
sig?: Buffer;
x5c?: Buffer[];
response?: Buffer;
alg?: number;
ver?: string;
certInfo?: Buffer;
pubArea?: Buffer;
};

View File

@ -0,0 +1,19 @@
// copy-🍝 from https://github.com/MasterKale/SimpleWebAuthn/blob/33528afe001d4aca62052dce204c0398c3127ffd/packages/server/src/helpers/decodeAuthenticatorExtensions.ts
/**
* Convert authenticator extension data buffer to a proper object
*
* @param extensionData Authenticator Extension Data buffer
*/
export function decodeAuthenticatorExtensions(cbor, extensionData) {
let toCBOR;
try {
toCBOR = cbor.decodeAllSync(extensionData)[0];
} catch (err) {
const _err = err;
throw new Error(
`Error decoding authenticator extensions: ${_err.message}`
);
}
return toCBOR;
}

View File

@ -0,0 +1,112 @@
import { decodeCborFirst } from "./cbor";
import { decodeAuthenticatorExtensions } from "./decodeAuthenticatorExtensions";
/**
* Make sense of the authData buffer contained in an Attestation
*/
export function parseAuthenticatorData(
cbor: any,
authData: Buffer
): ParsedAuthenticatorData {
if (authData.byteLength < 37) {
throw new Error(
`Authenticator data was ${authData.byteLength} bytes, expected at least 37 bytes`
);
}
let pointer = 0;
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 = counterBuf.readUInt32BE(0);
let aaguid: Buffer | undefined = undefined;
let credentialID: Buffer | undefined = undefined;
let credentialPublicKey: Buffer | undefined = undefined;
if (flags.at) {
aaguid = authData.slice(pointer, (pointer += 16));
const credIDLenBuf = authData.slice(pointer, (pointer += 2));
const credIDLen = credIDLenBuf.readUInt16BE(0);
credentialID = authData.slice(pointer, (pointer += credIDLen));
// Decode the next CBOR item in the buffer, then re-encode it back to a Buffer
const firstDecoded = decodeCborFirst(cbor, authData.slice(pointer));
const firstEncoded = Buffer.from(cbor.encode(firstDecoded));
credentialPublicKey = firstEncoded;
pointer += firstEncoded.byteLength;
}
let extensionsData: any | undefined = undefined;
let extensionsDataBuffer: Buffer | undefined = undefined;
if (flags.ed) {
const firstDecoded = decodeCborFirst(cbor, authData.slice(pointer));
const firstEncoded = Buffer.from(cbor.encode(firstDecoded));
extensionsDataBuffer = firstEncoded;
extensionsData = decodeAuthenticatorExtensions(
cbor,
extensionsDataBuffer
);
pointer += firstEncoded.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 {
rpIdHash,
flagsBuf,
flags,
counter,
counterBuf,
aaguid,
credentialID,
credentialPublicKey,
extensionsData,
extensionsDataBuffer,
};
}
export type ParsedAuthenticatorData = {
rpIdHash: Buffer;
flagsBuf: Buffer;
flags: {
up: boolean;
uv: boolean;
be: boolean;
bs: boolean;
at: boolean;
ed: boolean;
flagsInt: number;
};
counter: number;
counterBuf: Buffer;
aaguid?: Buffer;
credentialID?: Buffer;
credentialPublicKey?: Buffer;
extensionsData?: any;
extensionsDataBuffer?: Buffer;
};