mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 06:50:10 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e46cd371f | ||
|
|
db31792cef | ||
|
|
8c65cb2e80 | ||
|
|
14dff8ee6a |
@@ -31,7 +31,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
security_stamp TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
verify_devices INTEGER NOT NULL DEFAULT 1,
|
||||
verify_devices INTEGER NOT NULL DEFAULT 0,
|
||||
totp_secret TEXT,
|
||||
totp_recovery_code TEXT,
|
||||
api_key TEXT,
|
||||
|
||||
+31
-50
@@ -352,7 +352,7 @@ export async function handleRegister(request: Request, env: Env): Promise<Respon
|
||||
securityStamp: generateUUID(),
|
||||
role: 'user',
|
||||
status: 'active',
|
||||
verifyDevices: true,
|
||||
verifyDevices: false, // new-device verification requires email delivery (not available)
|
||||
totpSecret: null,
|
||||
totpRecoveryCode: null,
|
||||
yubikeyKey1: null,
|
||||
@@ -553,51 +553,31 @@ export async function handleUpdateProfile(request: Request, env: Env, userId: st
|
||||
}
|
||||
|
||||
// PUT/POST /api/accounts/verify-devices
|
||||
// New-device verification requires an email delivery channel which NodeWarden
|
||||
// does not provide. This endpoint always rejects the request so clients receive
|
||||
// clear feedback that the feature is unavailable rather than silently ignoring
|
||||
// the user's preference.
|
||||
export async function handleSetVerifyDevices(request: Request, env: Env, userId: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const auth = new AuthService(env);
|
||||
const user = await storage.getUserById(userId);
|
||||
if (!user) return errorResponse('User not found', 404);
|
||||
|
||||
let body: {
|
||||
secret?: string;
|
||||
masterPasswordHash?: string;
|
||||
verifyDevices?: boolean;
|
||||
VerifyDevices?: boolean;
|
||||
};
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return errorResponse('Invalid JSON', 400);
|
||||
}
|
||||
|
||||
const verifyDevices = typeof body.verifyDevices === 'boolean' ? body.verifyDevices : body.VerifyDevices;
|
||||
if (typeof verifyDevices !== 'boolean') {
|
||||
return errorResponse('verifyDevices must be true or false', 400);
|
||||
}
|
||||
|
||||
const verified = await verifyUserSecret(auth, user, body.secret || body.masterPasswordHash);
|
||||
if (!verified) {
|
||||
return errorResponse('User verification failed.', 400);
|
||||
}
|
||||
|
||||
user.verifyDevices = verifyDevices;
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
// Log the attempt for audit purposes, but do not change state.
|
||||
await writeAuditEvent(storage, {
|
||||
actorUserId: user.id,
|
||||
action: 'account.verify_devices.update',
|
||||
action: 'account.verify_devices.update.rejected',
|
||||
category: 'security',
|
||||
level: 'security',
|
||||
level: 'info',
|
||||
targetType: 'user',
|
||||
targetId: user.id,
|
||||
metadata: {
|
||||
verifyDevices: user.verifyDevices,
|
||||
reason: 'new-device verification is not supported (no email delivery channel)',
|
||||
...auditRequestMetadata(request),
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(null, { status: 200 });
|
||||
return errorResponse('New device verification is not available on this server. Enable TOTP or WebAuthn two-factor authentication instead.', 400);
|
||||
}
|
||||
|
||||
// GET /api/accounts/keys
|
||||
@@ -819,13 +799,16 @@ function yubiKeyResponse(user: User): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function deviceVerificationSettingsResponse(user: User): Record<string, unknown> {
|
||||
const enabled = user.verifyDevices !== false;
|
||||
// New-device verification requires an email delivery channel to send OTP
|
||||
// challenges to unknown devices. NodeWarden does not integrate with an email
|
||||
// provider, so this feature is intentionally unavailable. The settings
|
||||
// response always reports disabled regardless of any legacy DB value.
|
||||
function deviceVerificationSettingsResponse(_user: User): Record<string, unknown> {
|
||||
return {
|
||||
Enabled: enabled,
|
||||
enabled,
|
||||
VerifyDevices: enabled,
|
||||
verifyDevices: enabled,
|
||||
Enabled: false,
|
||||
enabled: false,
|
||||
VerifyDevices: false,
|
||||
verifyDevices: false,
|
||||
Object: 'deviceVerificationSettings',
|
||||
object: 'deviceVerificationSettings',
|
||||
};
|
||||
@@ -915,9 +898,10 @@ export async function handleGetDeviceVerificationSettings(request: Request, env:
|
||||
}
|
||||
|
||||
// PUT/POST /api/two-factor/device-verification-settings
|
||||
// New-device verification is not supported (no email delivery channel).
|
||||
// Reject any attempt to enable it; always return disabled state.
|
||||
export async function handlePutDeviceVerificationSettings(request: Request, env: Env, userId: string): Promise<Response> {
|
||||
const storage = new StorageService(env.DB);
|
||||
const auth = new AuthService(env);
|
||||
const user = await storage.getUserById(userId);
|
||||
if (!user) return errorResponse('User not found', 404);
|
||||
|
||||
@@ -929,31 +913,28 @@ export async function handlePutDeviceVerificationSettings(request: Request, env:
|
||||
}
|
||||
|
||||
const rawEnabled = body.enabled ?? body.Enabled ?? body.verifyDevices ?? body.VerifyDevices;
|
||||
if (typeof rawEnabled !== 'boolean') {
|
||||
return errorResponse('enabled must be true or false', 400);
|
||||
}
|
||||
|
||||
const secret = readBodyString(body, ['masterPasswordHash', 'MasterPasswordHash', 'secret', 'Secret']);
|
||||
const verified = await verifyUserSecret(auth, user, secret);
|
||||
if (!verified) return errorResponse('User verification failed.', 400);
|
||||
|
||||
user.verifyDevices = rawEnabled;
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await storage.saveUser(user);
|
||||
// Log the attempt for audit purposes — never change state.
|
||||
await writeAuditEvent(storage, {
|
||||
actorUserId: user.id,
|
||||
action: 'account.verify_devices.update',
|
||||
action: 'account.verify_devices.update.rejected',
|
||||
category: 'security',
|
||||
level: 'security',
|
||||
level: 'info',
|
||||
targetType: 'user',
|
||||
targetId: user.id,
|
||||
metadata: {
|
||||
verifyDevices: user.verifyDevices,
|
||||
requested: rawEnabled,
|
||||
reason: 'new-device verification is not supported (no email delivery channel)',
|
||||
source: 'two-factor.device-verification-settings',
|
||||
...auditRequestMetadata(request),
|
||||
},
|
||||
});
|
||||
|
||||
if (rawEnabled === true) {
|
||||
return errorResponse('New device verification is not available on this server. Enable TOTP or WebAuthn two-factor authentication instead.', 400);
|
||||
}
|
||||
|
||||
// Setting to false is the only supported state — return it.
|
||||
return jsonResponse(deviceVerificationSettingsResponse(user));
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,11 @@ function isWorkerHandledPath(path: string): boolean {
|
||||
path.startsWith('/api/') ||
|
||||
path.startsWith('/identity/') ||
|
||||
path.startsWith('/icons/') ||
|
||||
path.startsWith('/fill-assist/') ||
|
||||
path.startsWith('/notifications/') ||
|
||||
path.startsWith('/.well-known/') ||
|
||||
path === '/v1/assetlinks:check' ||
|
||||
path === '/web-bootstrap' ||
|
||||
path === '/config' ||
|
||||
path === '/api/config' ||
|
||||
path === '/api/version'
|
||||
|
||||
@@ -301,7 +301,7 @@ async function importPreparedBackupRows(db: D1Database, payload: BackupPayload['
|
||||
config: await prepareImportedConfigRows(env, payload.config || [], payload.users || []),
|
||||
users: cloneRows(payload.users || []).map((row) => ({
|
||||
...row,
|
||||
verify_devices: row.verify_devices ?? 1,
|
||||
verify_devices: row.verify_devices ?? 0,
|
||||
yubikey_nfc: row.yubikey_nfc ?? 0,
|
||||
})),
|
||||
domain_settings: cloneRows(payload.domain_settings || []),
|
||||
|
||||
@@ -14,11 +14,11 @@ const SCHEMA_STATEMENTS: readonly string[] = [
|
||||
'id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT, master_password_hint TEXT, master_password_hash TEXT NOT NULL, ' +
|
||||
'key TEXT NOT NULL, private_key TEXT, public_key TEXT, kdf_type INTEGER NOT NULL, ' +
|
||||
'kdf_iterations INTEGER NOT NULL, kdf_memory INTEGER, kdf_parallelism INTEGER, ' +
|
||||
'security_stamp TEXT NOT NULL, role TEXT NOT NULL DEFAULT \'user\', status TEXT NOT NULL DEFAULT \'active\', verify_devices INTEGER NOT NULL DEFAULT 1, totp_secret TEXT, totp_recovery_code TEXT, yubikey_key1 TEXT, yubikey_key2 TEXT, yubikey_key3 TEXT, yubikey_key4 TEXT, yubikey_key5 TEXT, yubikey_nfc INTEGER NOT NULL DEFAULT 0, api_key TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)',
|
||||
'security_stamp TEXT NOT NULL, role TEXT NOT NULL DEFAULT \'user\', status TEXT NOT NULL DEFAULT \'active\', verify_devices INTEGER NOT NULL DEFAULT 0, totp_secret TEXT, totp_recovery_code TEXT, yubikey_key1 TEXT, yubikey_key2 TEXT, yubikey_key3 TEXT, yubikey_key4 TEXT, yubikey_key5 TEXT, yubikey_nfc INTEGER NOT NULL DEFAULT 0, api_key TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)',
|
||||
'ALTER TABLE users ADD COLUMN master_password_hint TEXT',
|
||||
'ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT \'user\'',
|
||||
'ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT \'active\'',
|
||||
'ALTER TABLE users ADD COLUMN verify_devices INTEGER NOT NULL DEFAULT 1',
|
||||
'ALTER TABLE users ADD COLUMN verify_devices INTEGER NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE users ADD COLUMN totp_secret TEXT',
|
||||
'ALTER TABLE users ADD COLUMN totp_recovery_code TEXT',
|
||||
'ALTER TABLE users ADD COLUMN yubikey_key1 TEXT',
|
||||
|
||||
@@ -23,7 +23,7 @@ function mapUserRow(row: any): User {
|
||||
securityStamp: row.security_stamp,
|
||||
role: row.role === 'admin' ? 'admin' : 'user',
|
||||
status: row.status === 'banned' ? 'banned' : 'active',
|
||||
verifyDevices: row.verify_devices == null ? true : !!row.verify_devices,
|
||||
verifyDevices: row.verify_devices == null ? false : !!row.verify_devices,
|
||||
totpSecret: row.totp_secret ?? null,
|
||||
totpRecoveryCode: row.totp_recovery_code ?? null,
|
||||
yubikeyKey1: row.yubikey_key1 ?? null,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import type { Env } from '../types';
|
||||
|
||||
// Keep this list aligned with Bitwarden server's default FIDO2 origins.
|
||||
// These are the stable store IDs for the official Chromium-based extensions.
|
||||
export const OFFICIAL_BITWARDEN_BROWSER_EXTENSION_ORIGINS = [
|
||||
'chrome-extension://nngceckbapebfimnlniiiahkandclblb',
|
||||
'chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh',
|
||||
'chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn',
|
||||
] as const;
|
||||
|
||||
export function normalizeOrigin(value: unknown): string | null {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return null;
|
||||
@@ -25,7 +33,7 @@ export function isBrowserExtensionOrigin(origin: unknown): boolean {
|
||||
export function getConfiguredWebAuthnAllowedOrigins(
|
||||
env: Pick<Env, 'WEBAUTHN_ALLOWED_ORIGINS'>
|
||||
): string[] {
|
||||
const seen = new Set<string>();
|
||||
const seen = new Set<string>(OFFICIAL_BITWARDEN_BROWSER_EXTENSION_ORIGINS);
|
||||
for (const item of String(env.WEBAUTHN_ALLOWED_ORIGINS || '').split(',')) {
|
||||
const origin = normalizeOrigin(item);
|
||||
if (origin) seen.add(origin);
|
||||
|
||||
@@ -30,7 +30,9 @@ export function buildProfileResponse(user: User, env?: Env): ProfileResponse {
|
||||
forcePasswordReset: false,
|
||||
avatarColor: null,
|
||||
creationDate: user.createdAt,
|
||||
verifyDevices: user.verifyDevices !== false,
|
||||
// New-device verification is not supported without an email delivery channel.
|
||||
// Always report disabled so clients do not present a false security posture.
|
||||
verifyDevices: false,
|
||||
role: user.role,
|
||||
status: user.status,
|
||||
object: 'profile',
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function SettingsPage(props: SettingsPageProps) {
|
||||
const [accountPasskeys, setAccountPasskeys] = useState<AccountPasskeyCredential[]>([]);
|
||||
const [accountPasskeysLoading, setAccountPasskeysLoading] = useState(false);
|
||||
const [accountPasskeyName, setAccountPasskeyName] = useState(t('txt_account_passkey'));
|
||||
const [accountPasskeyDirectUnlock, setAccountPasskeyDirectUnlock] = useState(false);
|
||||
const [accountPasskeyDirectUnlock, setAccountPasskeyDirectUnlock] = useState(true);
|
||||
const [accountPasskeyPromptId, setAccountPasskeyPromptId] = useState<string | null>(null);
|
||||
const [createPasskeyDialogOpen, setCreatePasskeyDialogOpen] = useState(false);
|
||||
const [createPasskeyMasterPassword, setCreatePasskeyMasterPassword] = useState('');
|
||||
@@ -509,7 +509,7 @@ export default function SettingsPage(props: SettingsPageProps) {
|
||||
setCreatePasskeyDialogOpen(false);
|
||||
setCreatePasskeyMasterPassword('');
|
||||
setAccountPasskeyName(t('txt_account_passkey'));
|
||||
setAccountPasskeyDirectUnlock(false);
|
||||
setAccountPasskeyDirectUnlock(true);
|
||||
}
|
||||
|
||||
async function submitCreatePasskeyDialog(): Promise<void> {
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface PendingAccountPasskeyCredential {
|
||||
deviceResponse: PublicKeyCredential;
|
||||
request: Record<string, unknown>;
|
||||
supportsPrf: boolean;
|
||||
prfKey?: Uint8Array;
|
||||
}
|
||||
|
||||
export interface AccountPasskeyPrfKeySet {
|
||||
@@ -82,20 +83,9 @@ async function getLoginWithPrfSalt(): Promise<Uint8Array> {
|
||||
return new Uint8Array(hash);
|
||||
}
|
||||
|
||||
function credentialIdToBase64Url(id: BufferSource): string | null {
|
||||
try {
|
||||
const bytes = id instanceof ArrayBuffer
|
||||
? new Uint8Array(id)
|
||||
: new Uint8Array(id.buffer, id.byteOffset, id.byteLength);
|
||||
return bytesToBase64Url(bytes);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type PrfEvalInput = { first: Uint8Array };
|
||||
|
||||
function buildLegacyPrfExtension(salt: Uint8Array): Record<string, unknown> {
|
||||
function buildPrfExtension(salt: Uint8Array): Record<string, unknown> {
|
||||
const evalInput: PrfEvalInput = { first: salt };
|
||||
return {
|
||||
prf: {
|
||||
@@ -104,34 +94,23 @@ function buildLegacyPrfExtension(salt: Uint8Array): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function buildCredentialPrfExtension(
|
||||
salt: Uint8Array,
|
||||
credentialIds: Array<string | null | undefined>
|
||||
): Record<string, unknown> {
|
||||
const evalInput = { first: salt };
|
||||
const evalByCredential = credentialIds
|
||||
.filter((id): id is string => !!id)
|
||||
.reduce<Record<string, PrfEvalInput>>((out, id) => {
|
||||
out[id] = evalInput;
|
||||
return out;
|
||||
}, {});
|
||||
if (!Object.keys(evalByCredential).length) return buildLegacyPrfExtension(salt);
|
||||
return {
|
||||
prf: {
|
||||
evalByCredential,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function withPrfExtension(
|
||||
options: PublicKeyCredentialCreationOptions,
|
||||
salt: Uint8Array
|
||||
): PublicKeyCredentialCreationOptions;
|
||||
function withPrfExtension(
|
||||
options: PublicKeyCredentialRequestOptions,
|
||||
extension: Record<string, unknown>
|
||||
): PublicKeyCredentialRequestOptions {
|
||||
salt: Uint8Array
|
||||
): PublicKeyCredentialRequestOptions;
|
||||
function withPrfExtension(
|
||||
options: PublicKeyCredentialCreationOptions | PublicKeyCredentialRequestOptions,
|
||||
salt: Uint8Array
|
||||
): PublicKeyCredentialCreationOptions | PublicKeyCredentialRequestOptions {
|
||||
return {
|
||||
...options,
|
||||
extensions: {
|
||||
...((options as any).extensions || {}),
|
||||
...extension,
|
||||
...buildPrfExtension(salt),
|
||||
} as any,
|
||||
};
|
||||
}
|
||||
@@ -154,70 +133,17 @@ function readPrfFirstResult(credential: PublicKeyCredential): ArrayBuffer | unde
|
||||
return result instanceof ArrayBuffer ? result : undefined;
|
||||
}
|
||||
|
||||
function hasPrfExtensionResult(credential: PublicKeyCredential): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(credential.getClientExtensionResults() as any, 'prf');
|
||||
}
|
||||
|
||||
function shouldRetryWithLegacyPrf(error: unknown): boolean {
|
||||
const name = error instanceof DOMException || error instanceof Error ? error.name : '';
|
||||
return name === 'NotSupportedError' || name === 'SyntaxError' || name === 'TypeError';
|
||||
}
|
||||
|
||||
function shouldRetryCreateWithoutPrf(error: unknown): boolean {
|
||||
const name = error instanceof DOMException || error instanceof Error ? error.name : '';
|
||||
const message = error instanceof DOMException || error instanceof Error ? error.message : '';
|
||||
return (
|
||||
name === 'NotSupportedError' ||
|
||||
name === 'SyntaxError' ||
|
||||
name === 'TypeError' ||
|
||||
(name === 'UnknownError' && /transient/i.test(message))
|
||||
);
|
||||
}
|
||||
|
||||
async function canRequestPrfExtension(): Promise<boolean> {
|
||||
if (/\bFirefox\//i.test(navigator.userAgent)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getPublicKeyCredentialWithPrf(
|
||||
options: PublicKeyCredentialRequestOptions,
|
||||
salt: Uint8Array,
|
||||
credentialIds: string[] = []
|
||||
salt: Uint8Array
|
||||
): Promise<PublicKeyCredential> {
|
||||
const attempts = credentialIds.length
|
||||
? [
|
||||
buildCredentialPrfExtension(salt, credentialIds),
|
||||
buildLegacyPrfExtension(salt),
|
||||
]
|
||||
: [buildLegacyPrfExtension(salt)];
|
||||
let lastCredential: PublicKeyCredential | null = null;
|
||||
for (let index = 0; index < attempts.length; index += 1) {
|
||||
try {
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: withPrfExtension(options, attempts[index]),
|
||||
publicKey: withPrfExtension(options, salt),
|
||||
});
|
||||
if (!(credential instanceof PublicKeyCredential)) {
|
||||
throw new Error(t('txt_no_passkey_selected'));
|
||||
}
|
||||
lastCredential = credential;
|
||||
if (readPrfFirstResult(credential) || hasPrfExtensionResult(credential) || index === attempts.length - 1) {
|
||||
return credential;
|
||||
}
|
||||
} catch (error) {
|
||||
if (index === attempts.length - 1 || !shouldRetryWithLegacyPrf(error)) {
|
||||
if (lastCredential) return lastCredential;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastCredential) return lastCredential;
|
||||
throw new Error(t('txt_no_passkey_selected'));
|
||||
}
|
||||
|
||||
function prfCredentialIdsFromAllowCredentials(options: PublicKeyCredentialRequestOptions): string[] {
|
||||
return (options.allowCredentials || [])
|
||||
.map((credential) => credentialIdToBase64Url(credential.id))
|
||||
.filter((id): id is string => !!id);
|
||||
}
|
||||
|
||||
async function prfOutputToKey(prfOutput: ArrayBuffer): Promise<Uint8Array> {
|
||||
@@ -282,8 +208,7 @@ export async function assertAccountPasskey(
|
||||
const nativeOptions = cloneRequestOptions(response.options);
|
||||
const credential = await getPublicKeyCredentialWithPrf(
|
||||
nativeOptions,
|
||||
await getLoginWithPrfSalt(),
|
||||
prfCredentialIdsFromAllowCredentials(nativeOptions)
|
||||
await getLoginWithPrfSalt()
|
||||
);
|
||||
const prfResult = readPrfFirstResult(credential);
|
||||
return {
|
||||
@@ -309,34 +234,22 @@ export async function createAccountPasskeyCredential(
|
||||
}
|
||||
return credential;
|
||||
};
|
||||
let credential: PublicKeyCredential;
|
||||
if (requestPrf && await canRequestPrfExtension()) {
|
||||
const prfOptions: PublicKeyCredentialCreationOptions = {
|
||||
...noPrfOptions,
|
||||
extensions: {
|
||||
...((noPrfOptions as any).extensions || {}),
|
||||
prf: {},
|
||||
} as any,
|
||||
};
|
||||
try {
|
||||
credential = await createWithOptions(prfOptions);
|
||||
} catch (error) {
|
||||
if (!shouldRetryCreateWithoutPrf(error)) throw error;
|
||||
credential = await createWithOptions(noPrfOptions);
|
||||
}
|
||||
} else {
|
||||
credential = await createWithOptions(noPrfOptions);
|
||||
}
|
||||
const prfSalt = requestPrf ? await getLoginWithPrfSalt() : null;
|
||||
const credential = await createWithOptions(
|
||||
prfSalt ? withPrfExtension(noPrfOptions, prfSalt) : noPrfOptions
|
||||
);
|
||||
if (!(credential instanceof PublicKeyCredential)) {
|
||||
throw new Error(t('txt_no_passkey_created'));
|
||||
}
|
||||
const supportsPrf = !!(credential.getClientExtensionResults() as any).prf?.enabled;
|
||||
const prfResult = readPrfFirstResult(credential);
|
||||
const supportsPrf = !!prfResult || (credential.getClientExtensionResults() as any).prf?.enabled === true;
|
||||
return {
|
||||
token: response.token,
|
||||
createOptions: nativeOptions,
|
||||
deviceResponse: credential,
|
||||
request: attestationRequest(credential),
|
||||
supportsPrf,
|
||||
prfKey: prfResult ? await prfOutputToKey(prfResult) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -373,8 +286,10 @@ export async function buildAccountPasskeyPrfKeySet(
|
||||
pending: PendingAccountPasskeyCredential,
|
||||
userKey: { symEncKey: string; symMacKey: string }
|
||||
): Promise<AccountPasskeyPrfKeySet> {
|
||||
if (pending.prfKey) {
|
||||
return buildAccountPasskeyPrfKeySetFromPrfKey(pending.prfKey, userKey);
|
||||
}
|
||||
const rawId = new Uint8Array(pending.deviceResponse.rawId);
|
||||
const credentialId = bytesToBase64Url(rawId);
|
||||
const assertionOptions: PublicKeyCredentialRequestOptions = {
|
||||
challenge: pending.createOptions?.challenge!,
|
||||
rpId: pending.createOptions?.rp?.id,
|
||||
@@ -384,8 +299,7 @@ export async function buildAccountPasskeyPrfKeySet(
|
||||
};
|
||||
const assertion = await getPublicKeyCredentialWithPrf(
|
||||
assertionOptions,
|
||||
await getLoginWithPrfSalt(),
|
||||
[credentialId]
|
||||
await getLoginWithPrfSalt()
|
||||
);
|
||||
const prfResult = readPrfFirstResult(assertion);
|
||||
if (!prfResult) {
|
||||
|
||||
Reference in New Issue
Block a user