mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
171 lines
4.6 KiB
JavaScript
171 lines
4.6 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
const {existsSync} = require('node:fs');
|
|
const {createHash} = require('node:crypto');
|
|
const {join, sep} = require('node:path');
|
|
const {createNativeLoadError, loadNativeBinding} = require('./loader-diagnostics.cjs');
|
|
const {
|
|
base64Url,
|
|
clientDataJSON,
|
|
nativeFileName,
|
|
normalizeCreateOptions,
|
|
normalizeGetOptions,
|
|
transportBits,
|
|
} = require('./pure.cjs');
|
|
const MODULE_NAME = '@fluxer/webauthn';
|
|
const SKIP_NATIVE_PROBE_ENV = 'FLUXER_WEBAUTHN_SKIP_NATIVE_PROBE';
|
|
|
|
function resolveNativeRoot() {
|
|
const asarSegment = `${sep}app.asar${sep}`;
|
|
if (!__dirname.includes(asarSegment)) return __dirname;
|
|
const unpackedDir = __dirname.replace(asarSegment, `${sep}app.asar.unpacked${sep}`);
|
|
return existsSync(unpackedDir) ? unpackedDir : __dirname;
|
|
}
|
|
|
|
let binding = null;
|
|
let loadError = null;
|
|
|
|
const fileName = nativeFileName();
|
|
|
|
if (fileName) {
|
|
try {
|
|
const nativeRoot = resolveNativeRoot();
|
|
const nativePath = join(nativeRoot, fileName);
|
|
const loaded = loadNativeBinding({
|
|
moduleName: MODULE_NAME,
|
|
nativePath,
|
|
nativeRoot,
|
|
packageDir: __dirname,
|
|
skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
|
|
});
|
|
binding = loaded.binding;
|
|
loadError = loaded.loadError;
|
|
if (loadError) throw loadError;
|
|
} catch (error) {
|
|
loadError = createNativeLoadError({
|
|
moduleName: MODULE_NAME,
|
|
nativeRoot: resolveNativeRoot(),
|
|
packageDir: __dirname,
|
|
reason: 'native loader threw before binding load completed',
|
|
cause: error,
|
|
skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
|
|
});
|
|
throw loadError;
|
|
}
|
|
} else {
|
|
loadError = createNativeLoadError({
|
|
moduleName: MODULE_NAME,
|
|
nativeRoot: resolveNativeRoot(),
|
|
packageDir: __dirname,
|
|
reason: `no native binary mapping for ${process.platform}/${process.arch}`,
|
|
skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
|
|
});
|
|
throw loadError;
|
|
}
|
|
|
|
function clientDataHash(data) {
|
|
return createHash('sha256').update(data).digest();
|
|
}
|
|
|
|
function unavailableError() {
|
|
return new Error(
|
|
`@fluxer/webauthn native backend unavailable on ${process.platform}/${process.arch}: ${
|
|
loadError instanceof Error ? loadError.message : 'unknown load error'
|
|
}`,
|
|
);
|
|
}
|
|
|
|
function requireBinding() {
|
|
if (!binding) throw unavailableError();
|
|
return binding;
|
|
}
|
|
|
|
async function isSupported() {
|
|
if (!binding || typeof binding.isSupported !== 'function') return false;
|
|
return Boolean(await binding.isSupported());
|
|
}
|
|
|
|
function rawIdCredential(rawId, response, authenticatorAttachment) {
|
|
const id = base64Url(rawId);
|
|
return {
|
|
id,
|
|
rawId,
|
|
response: Buffer.from(JSON.stringify(response), 'utf8'),
|
|
authenticatorAttachment,
|
|
type: 'public-key',
|
|
};
|
|
}
|
|
|
|
async function create(options) {
|
|
const native = requireBinding();
|
|
if (typeof native.create !== 'function') {
|
|
throw new Error(
|
|
`@fluxer/webauthn native backend did not export registration on ${process.platform}/${process.arch}`,
|
|
);
|
|
}
|
|
const normalized = normalizeCreateOptions(options);
|
|
normalized.clientDataHash = clientDataHash(normalized.clientDataJSON);
|
|
const result = await native.create(normalized);
|
|
return rawIdCredential(
|
|
result.rawId,
|
|
{
|
|
clientDataJSON: base64Url(result.clientDataJSON),
|
|
attestationObject: base64Url(result.attestationObject),
|
|
},
|
|
result.authenticatorAttachment,
|
|
);
|
|
}
|
|
|
|
async function get(options) {
|
|
const native = requireBinding();
|
|
if (typeof native.get !== 'function') {
|
|
throw new Error(
|
|
`@fluxer/webauthn native backend did not export authentication on ${process.platform}/${process.arch}`,
|
|
);
|
|
}
|
|
const normalized = normalizeGetOptions(options);
|
|
normalized.clientDataHash = clientDataHash(normalized.clientDataJSON);
|
|
const result = await native.get(normalized);
|
|
const response = {
|
|
clientDataJSON: base64Url(result.clientDataJSON),
|
|
authenticatorData: base64Url(result.authenticatorData),
|
|
signature: base64Url(result.signature),
|
|
};
|
|
if (result.userHandle) response.userHandle = base64Url(result.userHandle);
|
|
return rawIdCredential(result.rawId, response, result.authenticatorAttachment);
|
|
}
|
|
|
|
function getBackendInfo() {
|
|
if (!binding || typeof binding.getBackendInfo !== 'function') {
|
|
return {
|
|
target: `${process.platform}/${process.arch}`,
|
|
backend: 'unavailable',
|
|
nativeLoaded: false,
|
|
ceremoniesImplemented: false,
|
|
platformBrokerAvailable: false,
|
|
platformAuthenticatorAvailable: false,
|
|
supported: false,
|
|
apiVersion: 0,
|
|
reason: loadError instanceof Error ? loadError.message : 'native backend did not load',
|
|
};
|
|
}
|
|
return binding.getBackendInfo();
|
|
}
|
|
|
|
module.exports = {
|
|
create,
|
|
get,
|
|
getBackendInfo,
|
|
isSupported,
|
|
loadError,
|
|
_private: {
|
|
base64Url,
|
|
clientDataJSON,
|
|
nativeFileName,
|
|
normalizeCreateOptions,
|
|
normalizeGetOptions,
|
|
resolveNativeRoot,
|
|
transportBits,
|
|
},
|
|
};
|