Files
fluxer/fluxer_desktop/native/webauthn/pure.cjs
T

172 lines
6.6 KiB
JavaScript

// SPDX-License-Identifier: AGPL-3.0-or-later
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY = 0;
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM = 1;
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 2;
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED = 1;
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED = 2;
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3;
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE = 1;
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT = 2;
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT = 3;
const WEBAUTHN_ENTERPRISE_ATTESTATION_NONE = 0;
const WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED = 1;
const WEBAUTHN_CTAP_TRANSPORT_USB = 0x00000001;
const WEBAUTHN_CTAP_TRANSPORT_NFC = 0x00000002;
const WEBAUTHN_CTAP_TRANSPORT_BLE = 0x00000004;
const WEBAUTHN_CTAP_TRANSPORT_INTERNAL = 0x00000010;
const WEBAUTHN_CTAP_TRANSPORT_HYBRID = 0x00000020;
const WEBAUTHN_CTAP_TRANSPORT_SMART_CARD = 0x00000040;
function nativeFileName(platform = process.platform, arch = process.arch) {
if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) return `webauthn.darwin-${arch}.node`;
if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) return `webauthn.win32-${arch}-msvc.node`;
if (platform === 'linux' && (arch === 'x64' || arch === 'arm64')) return `webauthn.linux-${arch}-gnu.node`;
return null;
}
function base64Url(buffer) {
return Buffer.from(buffer).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function normalizedOrigin(origin) {
if (typeof origin !== 'string' || origin.length === 0) {
throw new TypeError('@fluxer/webauthn requires an origin string');
}
return new URL(origin).origin;
}
function relyingPartyId(options, origin) {
if (typeof options.rpId === 'string' && options.rpId.length > 0) return options.rpId;
if (options.rp && typeof options.rp.id === 'string' && options.rp.id.length > 0) return options.rp.id;
return new URL(origin).hostname;
}
function clientDataJSON(type, challenge, origin) {
return Buffer.from(
JSON.stringify({
type,
challenge: base64Url(challenge),
origin,
crossOrigin: false,
}),
'utf8',
);
}
function authenticatorAttachment(value) {
if (value === 'platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM;
if (value === 'cross-platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY;
}
function userVerification(value) {
if (value === 'required') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED;
if (value === 'discouraged') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED;
}
function attestation(value) {
if (value === 'direct') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT;
if (value === 'indirect') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT;
return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE;
}
function enterpriseAttestation(value) {
return value === 'enterprise'
? WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED
: WEBAUTHN_ENTERPRISE_ATTESTATION_NONE;
}
function transportBits(transports) {
if (!Array.isArray(transports)) return 0;
let bits = 0;
for (const transport of transports) {
if (transport === 'usb') bits |= WEBAUTHN_CTAP_TRANSPORT_USB;
else if (transport === 'nfc') bits |= WEBAUTHN_CTAP_TRANSPORT_NFC;
else if (transport === 'ble') bits |= WEBAUTHN_CTAP_TRANSPORT_BLE;
else if (transport === 'internal') bits |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL;
else if (transport === 'hybrid') bits |= WEBAUTHN_CTAP_TRANSPORT_HYBRID;
else if (transport === 'smart-card') bits |= WEBAUTHN_CTAP_TRANSPORT_SMART_CARD;
}
return bits;
}
function credentialDescriptors(descriptors) {
if (!Array.isArray(descriptors)) return [];
return descriptors.map((descriptor) => ({
id: Buffer.from(descriptor.id),
transports: transportBits(descriptor.transports),
}));
}
function windowHandleBuffer(value) {
if (Buffer.isBuffer(value)) return value;
if (value instanceof Uint8Array) return Buffer.from(value);
return undefined;
}
function pinString(value) {
return typeof value === 'string' && value.length > 0 ? value : undefined;
}
function residentKeyFlags(selection) {
const residentKey = selection?.residentKey;
const requireResidentKey = Boolean(selection?.requireResidentKey) || residentKey === 'required';
return {
requireResidentKey,
preferResidentKey: requireResidentKey || residentKey === 'preferred',
};
}
function normalizeCreateOptions(options) {
if (!options || typeof options !== 'object') throw new TypeError('registration options must be an object');
const origin = normalizedOrigin(options.origin);
const selection = options.authenticatorSelection || {};
const residentKeys = residentKeyFlags(selection);
return {
rpId: relyingPartyId(options, origin),
rpName: options.rp && typeof options.rp.name === 'string' ? options.rp.name : relyingPartyId(options, origin),
challenge: Buffer.from(options.challenge),
userId: Buffer.from(options.user.id),
userName: options.user.name,
userDisplayName: options.user.displayName,
clientDataJSON: clientDataJSON('webauthn.create', options.challenge, origin),
pubKeyCredParams: options.pubKeyCredParams,
excludeCredentials: credentialDescriptors(options.excludeCredentials),
timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0,
authenticatorAttachment: authenticatorAttachment(selection.authenticatorAttachment),
userVerification: userVerification(selection.userVerification),
attestation: attestation(options.attestation),
enterpriseAttestation: enterpriseAttestation(options.attestation),
windowHandle: windowHandleBuffer(options.windowHandle),
pin: pinString(options.pin),
...residentKeys,
};
}
function normalizeGetOptions(options) {
if (!options || typeof options !== 'object') throw new TypeError('assertion options must be an object');
const origin = normalizedOrigin(options.origin);
return {
rpId: relyingPartyId(options, origin),
challenge: Buffer.from(options.challenge),
clientDataJSON: clientDataJSON('webauthn.get', options.challenge, origin),
allowCredentials: credentialDescriptors(options.allowCredentials),
timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0,
authenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY,
userVerification: userVerification(options.userVerification),
windowHandle: windowHandleBuffer(options.windowHandle),
pin: pinString(options.pin),
};
}
module.exports = {
base64Url,
clientDataJSON,
nativeFileName,
normalizeCreateOptions,
normalizeGetOptions,
transportBits,
};