mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: enhance account passkey functionality and improve error handling
This commit is contained in:
@@ -12,6 +12,7 @@ export interface AppConfirmState {
|
||||
cancelText?: string;
|
||||
hideCancel?: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
interface AppGlobalOverlaysProps {
|
||||
@@ -49,7 +50,7 @@ export default function AppGlobalOverlays(props: AppGlobalOverlaysProps) {
|
||||
cancelText={props.confirm?.cancelText}
|
||||
hideCancel={props.confirm?.hideCancel}
|
||||
onConfirm={() => props.confirm?.onConfirm()}
|
||||
onCancel={props.onCancelConfirm}
|
||||
onCancel={props.confirm?.onCancel || props.onCancelConfirm}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -113,7 +113,7 @@ export interface AppMainRoutesProps {
|
||||
onGetApiKey: (masterPassword: string) => Promise<string>;
|
||||
onRotateApiKey: (masterPassword: string) => Promise<string>;
|
||||
onListAccountPasskeys: () => Promise<AccountPasskeyCredential[]>;
|
||||
onCreateAccountPasskey: (name: string, masterPassword: string, directUnlock: boolean) => Promise<AccountPasskeyCredential>;
|
||||
onCreateAccountPasskey: (name: string, masterPassword: string, directUnlock: boolean) => Promise<AccountPasskeyCredential | null>;
|
||||
onEnableAccountPasskeyDirectUnlock: (id: string, masterPassword: string) => Promise<void>;
|
||||
onDeleteAccountPasskey: (id: string, masterPassword: string) => Promise<void>;
|
||||
onLockTimeoutChange: (minutes: 0 | 1 | 5 | 15 | 30) => void;
|
||||
|
||||
@@ -19,7 +19,7 @@ interface SettingsPageProps {
|
||||
onGetApiKey: (masterPassword: string) => Promise<string>;
|
||||
onRotateApiKey: (masterPassword: string) => Promise<string>;
|
||||
onListAccountPasskeys: () => Promise<AccountPasskeyCredential[]>;
|
||||
onCreateAccountPasskey: (name: string, masterPassword: string, directUnlock: boolean) => Promise<AccountPasskeyCredential>;
|
||||
onCreateAccountPasskey: (name: string, masterPassword: string, directUnlock: boolean) => Promise<AccountPasskeyCredential | null>;
|
||||
onEnableAccountPasskeyDirectUnlock: (id: string, masterPassword: string) => Promise<void>;
|
||||
onDeleteAccountPasskey: (id: string, masterPassword: string) => Promise<void>;
|
||||
onLockTimeoutChange: (minutes: 0 | 1 | 5 | 15 | 30) => void;
|
||||
@@ -179,8 +179,8 @@ export default function SettingsPage(props: SettingsPageProps) {
|
||||
setApiKeyDialogOpen(true);
|
||||
props.onNotify?.('success', t('txt_api_key_rotated'));
|
||||
} else if (masterPasswordPrompt === 'createPasskey') {
|
||||
await props.onCreateAccountPasskey(accountPasskeyName, masterPassword, accountPasskeyDirectUnlock);
|
||||
await refreshAccountPasskeys();
|
||||
const credential = await props.onCreateAccountPasskey(accountPasskeyName, masterPassword, accountPasskeyDirectUnlock);
|
||||
if (credential) await refreshAccountPasskeys();
|
||||
} else if (masterPasswordPrompt === 'enablePasskeyDirectUnlock') {
|
||||
if (!accountPasskeyPromptId) throw new Error(t('txt_account_passkey_not_found'));
|
||||
await props.onEnableAccountPasskeyDirectUnlock(accountPasskeyPromptId, masterPassword);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
updateProfile,
|
||||
} from '@/lib/api/auth';
|
||||
import {
|
||||
AccountPasskeyPrfUnavailableError,
|
||||
assertAccountPasskey,
|
||||
buildAccountPasskeyPrfKeySet,
|
||||
buildAccountPasskeyPrfKeySetFromPrfKey,
|
||||
@@ -66,7 +67,29 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct
|
||||
} = options;
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
() => {
|
||||
function confirmSaveLoginOnlyAccountPasskey(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = (shouldSave: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
onSetConfirm(null);
|
||||
resolve(shouldSave);
|
||||
};
|
||||
onSetConfirm({
|
||||
title: t('txt_account_passkey_direct_unlock_unavailable_title'),
|
||||
message: t('txt_account_passkey_direct_unlock_unavailable_message'),
|
||||
confirmText: t('txt_save_login_only_passkey'),
|
||||
cancelText: t('txt_do_not_save'),
|
||||
showIcon: true,
|
||||
onConfirm: () => finish(true),
|
||||
onCancel: () => finish(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return ({
|
||||
async changePassword(currentPassword: string, nextPassword: string, nextPassword2: string) {
|
||||
if (!profile) return;
|
||||
if (!currentPassword || !nextPassword) {
|
||||
@@ -188,7 +211,7 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct
|
||||
return listAccountPasskeys(authedFetch);
|
||||
},
|
||||
|
||||
async createAccountPasskey(name: string, masterPassword: string, directUnlock: boolean): Promise<AccountPasskeyCredential> {
|
||||
async createAccountPasskey(name: string, masterPassword: string, directUnlock: boolean): Promise<AccountPasskeyCredential | null> {
|
||||
if (!profile) throw new Error(t('txt_profile_unavailable'));
|
||||
const normalizedPassword = String(masterPassword || '');
|
||||
if (!normalizedPassword) throw new Error(t('txt_master_password_is_required'));
|
||||
@@ -197,21 +220,32 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct
|
||||
const options = await getAccountPasskeyAttestationOptions(authedFetch, derived.hash);
|
||||
const pending = await createAccountPasskeyCredential(options);
|
||||
let keySet = null;
|
||||
let savedWithoutDirectUnlock = false;
|
||||
if (directUnlock) {
|
||||
if (!session?.symEncKey || !session?.symMacKey) throw new Error(t('txt_vault_key_unavailable'));
|
||||
keySet = await buildAccountPasskeyPrfKeySet(pending, {
|
||||
symEncKey: session.symEncKey,
|
||||
symMacKey: session.symMacKey,
|
||||
});
|
||||
try {
|
||||
keySet = await buildAccountPasskeyPrfKeySet(pending, {
|
||||
symEncKey: session.symEncKey,
|
||||
symMacKey: session.symMacKey,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof AccountPasskeyPrfUnavailableError)) throw error;
|
||||
const shouldSaveLoginOnly = await confirmSaveLoginOnlyAccountPasskey();
|
||||
if (!shouldSaveLoginOnly) {
|
||||
onNotify('warning', t('txt_account_passkey_not_saved'));
|
||||
return null;
|
||||
}
|
||||
savedWithoutDirectUnlock = true;
|
||||
}
|
||||
}
|
||||
const credential = await saveAccountPasskey(authedFetch, {
|
||||
name: normalizedName,
|
||||
token: pending.token,
|
||||
deviceResponse: pending.request,
|
||||
supportsPrf: pending.supportsPrf,
|
||||
supportsPrf: keySet ? true : savedWithoutDirectUnlock ? false : pending.supportsPrf,
|
||||
keySet,
|
||||
});
|
||||
onNotify('success', t('txt_account_passkey_saved'));
|
||||
onNotify('success', savedWithoutDirectUnlock ? t('txt_account_passkey_saved_login_only') : t('txt_account_passkey_saved'));
|
||||
return credential;
|
||||
},
|
||||
|
||||
@@ -369,7 +403,8 @@ export default function useAccountSecurityActions(options: UseAccountSecurityAct
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[
|
||||
authedFetch,
|
||||
clearDisableTotpDialog,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { base64ToBytes, bytesToBase64, decryptBw, encryptBw, hkdfExpand, toBufferSource } from './crypto';
|
||||
import { t } from './i18n';
|
||||
import type { AccountPasskeyPrfOption } from './types';
|
||||
|
||||
const LOGIN_WITH_PRF_SALT = 'passwordless-login';
|
||||
@@ -23,6 +24,13 @@ export interface AccountPasskeyPrfKeySet {
|
||||
encryptedPrivateKey: string;
|
||||
}
|
||||
|
||||
export class AccountPasskeyPrfUnavailableError extends Error {
|
||||
constructor() {
|
||||
super(t('txt_account_passkey_direct_unlock_unavailable_error'));
|
||||
this.name = 'AccountPasskeyPrfUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
function bytesToBase64Url(bytes: Uint8Array): string {
|
||||
return bytesToBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
@@ -38,7 +46,7 @@ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
||||
}
|
||||
|
||||
function cloneCreationOptions(options: any): PublicKeyCredentialCreationOptions {
|
||||
if (!options || typeof options !== 'object') throw new Error('Invalid passkey creation options');
|
||||
if (!options || typeof options !== 'object') throw new Error(t('txt_invalid_passkey_creation_options'));
|
||||
return {
|
||||
...options,
|
||||
challenge: toArrayBuffer(base64UrlToBytes(options.challenge)),
|
||||
@@ -56,7 +64,7 @@ function cloneCreationOptions(options: any): PublicKeyCredentialCreationOptions
|
||||
}
|
||||
|
||||
function cloneRequestOptions(options: any): PublicKeyCredentialRequestOptions {
|
||||
if (!options || typeof options !== 'object') throw new Error('Invalid passkey assertion options');
|
||||
if (!options || typeof options !== 'object') throw new Error(t('txt_invalid_passkey_assertion_options'));
|
||||
return {
|
||||
...options,
|
||||
challenge: toArrayBuffer(base64UrlToBytes(options.challenge)),
|
||||
@@ -131,7 +139,7 @@ function publicKeyCredentialBase(credential: PublicKeyCredential): Record<string
|
||||
|
||||
function assertionRequest(credential: PublicKeyCredential): Record<string, unknown> {
|
||||
if (!(credential.response instanceof AuthenticatorAssertionResponse)) {
|
||||
throw new Error('Invalid passkey assertion response');
|
||||
throw new Error(t('txt_invalid_passkey_assertion_response'));
|
||||
}
|
||||
return {
|
||||
...publicKeyCredentialBase(credential),
|
||||
@@ -148,7 +156,7 @@ function assertionRequest(credential: PublicKeyCredential): Record<string, unkno
|
||||
|
||||
function attestationRequest(credential: PublicKeyCredential): Record<string, unknown> {
|
||||
if (!(credential.response instanceof AuthenticatorAttestationResponse)) {
|
||||
throw new Error('Invalid passkey registration response');
|
||||
throw new Error(t('txt_invalid_passkey_registration_response'));
|
||||
}
|
||||
const transports = typeof credential.response.getTransports === 'function'
|
||||
? credential.response.getTransports()
|
||||
@@ -167,7 +175,7 @@ export async function assertAccountPasskey(
|
||||
response: { options: unknown; token: string }
|
||||
): Promise<AccountPasskeyAssertion> {
|
||||
if (!window.PublicKeyCredential || !navigator.credentials) {
|
||||
throw new Error('Passkey is not supported in this browser');
|
||||
throw new Error(t('txt_passkey_browser_not_supported'));
|
||||
}
|
||||
const nativeOptions = cloneRequestOptions(response.options);
|
||||
(nativeOptions as any).extensions = {
|
||||
@@ -176,7 +184,7 @@ export async function assertAccountPasskey(
|
||||
};
|
||||
const credential = await navigator.credentials.get({ publicKey: nativeOptions });
|
||||
if (!(credential instanceof PublicKeyCredential)) {
|
||||
throw new Error('No passkey was selected');
|
||||
throw new Error(t('txt_no_passkey_selected'));
|
||||
}
|
||||
const prfResult = (credential.getClientExtensionResults() as any).prf?.results?.first;
|
||||
return {
|
||||
@@ -190,7 +198,7 @@ export async function createAccountPasskeyCredential(
|
||||
response: { options: unknown; token: string }
|
||||
): Promise<PendingAccountPasskeyCredential> {
|
||||
if (!window.PublicKeyCredential || !navigator.credentials) {
|
||||
throw new Error('Passkey is not supported in this browser');
|
||||
throw new Error(t('txt_passkey_browser_not_supported'));
|
||||
}
|
||||
const nativeOptions = cloneCreationOptions(response.options);
|
||||
(nativeOptions as any).extensions = {
|
||||
@@ -199,7 +207,7 @@ export async function createAccountPasskeyCredential(
|
||||
};
|
||||
const credential = await navigator.credentials.create({ publicKey: nativeOptions });
|
||||
if (!(credential instanceof PublicKeyCredential)) {
|
||||
throw new Error('No passkey was created');
|
||||
throw new Error(t('txt_no_passkey_created'));
|
||||
}
|
||||
const supportsPrf = !!(credential.getClientExtensionResults() as any).prf?.enabled;
|
||||
return {
|
||||
@@ -214,7 +222,7 @@ export async function createAccountPasskeyCredential(
|
||||
function parseRsaEncryptedUserKey(value: string): Uint8Array {
|
||||
const text = String(value || '').trim();
|
||||
const [type, payload] = text.split('.');
|
||||
if (type !== '4' || !payload) throw new Error('Unsupported encrypted user key');
|
||||
if (type !== '4' || !payload) throw new Error(t('txt_unsupported_encrypted_user_key'));
|
||||
return base64ToBytes(payload);
|
||||
}
|
||||
|
||||
@@ -236,11 +244,11 @@ export async function buildAccountPasskeyPrfKeySet(
|
||||
};
|
||||
const assertion = await navigator.credentials.get({ publicKey: assertionOptions });
|
||||
if (!(assertion instanceof PublicKeyCredential)) {
|
||||
throw new Error('Passkey verification failed');
|
||||
throw new Error(t('txt_passkey_verification_failed'));
|
||||
}
|
||||
const prfResult = (assertion.getClientExtensionResults() as any).prf?.results?.first;
|
||||
if (!prfResult) {
|
||||
throw new Error('This passkey does not support direct vault unlock');
|
||||
throw new AccountPasskeyPrfUnavailableError();
|
||||
}
|
||||
return buildAccountPasskeyPrfKeySetFromPrfKey(await prfOutputToKey(prfResult), userKey);
|
||||
}
|
||||
@@ -285,7 +293,7 @@ export async function unlockVaultKeyWithAccountPasskeyPrf(
|
||||
const encryptedPrivateKey = option.EncryptedPrivateKey || option.encryptedPrivateKey || '';
|
||||
const encryptedUserKey = option.EncryptedUserKey || option.encryptedUserKey || '';
|
||||
if (!encryptedPrivateKey || !encryptedUserKey) {
|
||||
throw new Error('Passkey cannot unlock this vault');
|
||||
throw new Error(t('txt_passkey_cannot_unlock_vault'));
|
||||
}
|
||||
const privateKeyBytes = await decryptBw(encryptedPrivateKey, prfKey.slice(0, 32), prfKey.slice(32, 64));
|
||||
const privateKey = await crypto.subtle.importKey(
|
||||
@@ -300,7 +308,7 @@ export async function unlockVaultKeyWithAccountPasskeyPrf(
|
||||
privateKey,
|
||||
toBufferSource(parseRsaEncryptedUserKey(encryptedUserKey))
|
||||
));
|
||||
if (userKeyBytes.length < 64) throw new Error('Invalid passkey vault key');
|
||||
if (userKeyBytes.length < 64) throw new Error(t('txt_invalid_passkey_vault_key'));
|
||||
return {
|
||||
symEncKey: bytesToBase64(userKeyBytes.slice(0, 32)),
|
||||
symMacKey: bytesToBase64(userKeyBytes.slice(32, 64)),
|
||||
|
||||
@@ -644,6 +644,13 @@ const en: Record<string, string> = {
|
||||
"txt_account_passkey_not_found": "Account passkey not found",
|
||||
"txt_account_passkey_prf_not_available": "This passkey cannot return a PRF key",
|
||||
"txt_account_passkey_direct_unlock_enabled": "Direct vault unlock enabled",
|
||||
"txt_account_passkey_direct_unlock_unavailable_title": "Direct unlock unavailable",
|
||||
"txt_account_passkey_direct_unlock_unavailable_message": "This passkey did not return a PRF key, so it cannot unlock the vault directly. You can still save it for account login; unlocking the vault will require your master password.",
|
||||
"txt_account_passkey_direct_unlock_unavailable_error": "This passkey cannot unlock the vault directly",
|
||||
"txt_account_passkey_saved_login_only": "Account passkey saved for login only",
|
||||
"txt_account_passkey_not_saved": "Account passkey was not saved",
|
||||
"txt_save_login_only_passkey": "Save for login only",
|
||||
"txt_do_not_save": "Do not save",
|
||||
"txt_add_account_passkey": "Add account passkey",
|
||||
"txt_delete_account_passkey": "Delete account passkey",
|
||||
"txt_direct_unlock": "Direct unlock",
|
||||
@@ -654,6 +661,17 @@ const en: Record<string, string> = {
|
||||
"txt_passkey_name": "Passkey name",
|
||||
"txt_passkey_requires_master_password": "Passkey verified. Enter your master password to unlock the vault.",
|
||||
"txt_prf_not_supported": "PRF not supported",
|
||||
"txt_invalid_passkey_creation_options": "Invalid passkey creation options",
|
||||
"txt_invalid_passkey_assertion_options": "Invalid passkey verification options",
|
||||
"txt_invalid_passkey_assertion_response": "Invalid passkey verification response",
|
||||
"txt_invalid_passkey_registration_response": "Invalid passkey registration response",
|
||||
"txt_passkey_browser_not_supported": "This browser does not support passkeys",
|
||||
"txt_no_passkey_selected": "No passkey was selected",
|
||||
"txt_no_passkey_created": "No passkey was created",
|
||||
"txt_unsupported_encrypted_user_key": "Unsupported encrypted account key",
|
||||
"txt_passkey_verification_failed": "Passkey verification failed",
|
||||
"txt_passkey_cannot_unlock_vault": "This passkey cannot unlock this vault",
|
||||
"txt_invalid_passkey_vault_key": "Invalid passkey vault key",
|
||||
"txt_phone": "Phone",
|
||||
"txt_please_input_email_and_password": "Please input email and password",
|
||||
"txt_please_input_master_password": "Please input master password",
|
||||
|
||||
@@ -644,6 +644,13 @@ const es: Record<string, string> = {
|
||||
"txt_account_passkey_not_found": "Passkey de cuenta no encontrada",
|
||||
"txt_account_passkey_prf_not_available": "Esta passkey no puede devolver una clave PRF",
|
||||
"txt_account_passkey_direct_unlock_enabled": "Desbloqueo directo activado",
|
||||
"txt_account_passkey_direct_unlock_unavailable_title": "Desbloqueo directo no disponible",
|
||||
"txt_account_passkey_direct_unlock_unavailable_message": "Esta passkey no devolvió una clave PRF, por lo que no puede desbloquear la bóveda directamente. Aun así puede guardarla para iniciar sesión; para desbloquear la bóveda necesitará la contraseña maestra.",
|
||||
"txt_account_passkey_direct_unlock_unavailable_error": "Esta passkey no puede desbloquear la bóveda directamente",
|
||||
"txt_account_passkey_saved_login_only": "Passkey de cuenta guardada solo para inicio de sesión",
|
||||
"txt_account_passkey_not_saved": "La passkey de cuenta no se guardó",
|
||||
"txt_save_login_only_passkey": "Guardar solo para inicio",
|
||||
"txt_do_not_save": "No guardar",
|
||||
"txt_add_account_passkey": "Añadir passkey de cuenta",
|
||||
"txt_delete_account_passkey": "Eliminar passkey de cuenta",
|
||||
"txt_direct_unlock": "Desbloqueo directo",
|
||||
@@ -654,6 +661,17 @@ const es: Record<string, string> = {
|
||||
"txt_passkey_name": "Nombre de passkey",
|
||||
"txt_passkey_requires_master_password": "Passkey verificada. Introduzca su contraseña maestra para desbloquear la bóveda.",
|
||||
"txt_prf_not_supported": "PRF no compatible",
|
||||
"txt_invalid_passkey_creation_options": "Opciones de creación de passkey no válidas",
|
||||
"txt_invalid_passkey_assertion_options": "Opciones de verificación de passkey no válidas",
|
||||
"txt_invalid_passkey_assertion_response": "Respuesta de verificación de passkey no válida",
|
||||
"txt_invalid_passkey_registration_response": "Respuesta de registro de passkey no válida",
|
||||
"txt_passkey_browser_not_supported": "Este navegador no admite passkeys",
|
||||
"txt_no_passkey_selected": "No se seleccionó ninguna passkey",
|
||||
"txt_no_passkey_created": "No se creó ninguna passkey",
|
||||
"txt_unsupported_encrypted_user_key": "Clave de cuenta cifrada no compatible",
|
||||
"txt_passkey_verification_failed": "Error al verificar la passkey",
|
||||
"txt_passkey_cannot_unlock_vault": "Esta passkey no puede desbloquear esta bóveda",
|
||||
"txt_invalid_passkey_vault_key": "Clave de bóveda de passkey no válida",
|
||||
"txt_phone": "Teléfono",
|
||||
"txt_please_input_email_and_password": "Por favor, introduzca correo y contraseña",
|
||||
"txt_please_input_master_password": "Por favor, introduzca contraseña maestra",
|
||||
|
||||
@@ -644,6 +644,13 @@ const ru: Record<string, string> = {
|
||||
"txt_account_passkey_not_found": "Passkey аккаунта не найдена",
|
||||
"txt_account_passkey_prf_not_available": "Эта passkey не может вернуть PRF-ключ",
|
||||
"txt_account_passkey_direct_unlock_enabled": "Прямая разблокировка включена",
|
||||
"txt_account_passkey_direct_unlock_unavailable_title": "Прямая разблокировка недоступна",
|
||||
"txt_account_passkey_direct_unlock_unavailable_message": "Эта passkey не вернула PRF-ключ, поэтому не может напрямую разблокировать хранилище. Ее все равно можно сохранить для входа в аккаунт; для разблокировки хранилища потребуется мастер-пароль.",
|
||||
"txt_account_passkey_direct_unlock_unavailable_error": "Эта passkey не может напрямую разблокировать хранилище",
|
||||
"txt_account_passkey_saved_login_only": "Passkey аккаунта сохранена только для входа",
|
||||
"txt_account_passkey_not_saved": "Passkey аккаунта не сохранена",
|
||||
"txt_save_login_only_passkey": "Сохранить только для входа",
|
||||
"txt_do_not_save": "Не сохранять",
|
||||
"txt_add_account_passkey": "Добавить passkey аккаунта",
|
||||
"txt_delete_account_passkey": "Удалить passkey аккаунта",
|
||||
"txt_direct_unlock": "Прямая разблокировка",
|
||||
@@ -654,6 +661,17 @@ const ru: Record<string, string> = {
|
||||
"txt_passkey_name": "Название passkey",
|
||||
"txt_passkey_requires_master_password": "Passkey подтвержден. Введите мастер-пароль, чтобы разблокировать хранилище.",
|
||||
"txt_prf_not_supported": "PRF не поддерживается",
|
||||
"txt_invalid_passkey_creation_options": "Недопустимые параметры создания passkey",
|
||||
"txt_invalid_passkey_assertion_options": "Недопустимые параметры проверки passkey",
|
||||
"txt_invalid_passkey_assertion_response": "Недопустимый ответ проверки passkey",
|
||||
"txt_invalid_passkey_registration_response": "Недопустимый ответ регистрации passkey",
|
||||
"txt_passkey_browser_not_supported": "Этот браузер не поддерживает passkeys",
|
||||
"txt_no_passkey_selected": "Passkey не выбрана",
|
||||
"txt_no_passkey_created": "Passkey не создана",
|
||||
"txt_unsupported_encrypted_user_key": "Неподдерживаемый зашифрованный ключ аккаунта",
|
||||
"txt_passkey_verification_failed": "Не удалось проверить passkey",
|
||||
"txt_passkey_cannot_unlock_vault": "Эта passkey не может разблокировать это хранилище",
|
||||
"txt_invalid_passkey_vault_key": "Недопустимый ключ хранилища passkey",
|
||||
"txt_phone": "Телефон",
|
||||
"txt_please_input_email_and_password": "Пожалуйста, введите адрес электронной почты и пароль",
|
||||
"txt_please_input_master_password": "Пожалуйста, введите мастер-пароль",
|
||||
|
||||
@@ -644,16 +644,34 @@ const zhCN: Record<string, string> = {
|
||||
"txt_account_passkey_not_found": "未找到账号通行密钥",
|
||||
"txt_account_passkey_prf_not_available": "这把通行密钥无法返回 PRF 密钥",
|
||||
"txt_account_passkey_direct_unlock_enabled": "已开启直接解锁",
|
||||
"txt_account_passkey_direct_unlock_unavailable_title": "无法直接解锁",
|
||||
"txt_account_passkey_direct_unlock_unavailable_message": "这把通行密钥没有返回 PRF 密钥,因此不能直接解锁密码库。你仍然可以把它保存为仅登录通行密钥;登录后需要输入主密码解锁。",
|
||||
"txt_account_passkey_direct_unlock_unavailable_error": "这把通行密钥无法直接解锁密码库",
|
||||
"txt_account_passkey_saved_login_only": "已保存为仅登录通行密钥",
|
||||
"txt_account_passkey_not_saved": "通行密钥未保存",
|
||||
"txt_save_login_only_passkey": "保存为仅登录",
|
||||
"txt_do_not_save": "不保存",
|
||||
"txt_add_account_passkey": "添加账号通行密钥",
|
||||
"txt_delete_account_passkey": "删除账号通行密钥",
|
||||
"txt_direct_unlock": "直接解锁",
|
||||
"txt_enable_passkey_direct_unlock": "开启直接解锁",
|
||||
"txt_login_only": "仅登录",
|
||||
"txt_login_with_passkey": "使用 Passkey 登录",
|
||||
"txt_login_with_passkey": "使用通行密钥登录",
|
||||
"txt_no_account_passkeys": "暂无账号通行密钥",
|
||||
"txt_passkey_name": "通行密钥名称",
|
||||
"txt_passkey_requires_master_password": "Passkey 已验证,请输入主密码解锁密码库。",
|
||||
"txt_passkey_requires_master_password": "通行密钥已验证,请输入主密码解锁密码库。",
|
||||
"txt_prf_not_supported": "不支持 PRF",
|
||||
"txt_invalid_passkey_creation_options": "通行密钥创建选项无效",
|
||||
"txt_invalid_passkey_assertion_options": "通行密钥验证选项无效",
|
||||
"txt_invalid_passkey_assertion_response": "通行密钥验证响应无效",
|
||||
"txt_invalid_passkey_registration_response": "通行密钥注册响应无效",
|
||||
"txt_passkey_browser_not_supported": "当前浏览器不支持通行密钥",
|
||||
"txt_no_passkey_selected": "未选择通行密钥",
|
||||
"txt_no_passkey_created": "未创建通行密钥",
|
||||
"txt_unsupported_encrypted_user_key": "不支持的加密账户密钥",
|
||||
"txt_passkey_verification_failed": "通行密钥验证失败",
|
||||
"txt_passkey_cannot_unlock_vault": "这把通行密钥无法解锁此密码库",
|
||||
"txt_invalid_passkey_vault_key": "通行密钥密码库密钥无效",
|
||||
"txt_phone": "电话",
|
||||
"txt_please_input_email_and_password": "请输入邮箱和密码",
|
||||
"txt_please_input_master_password": "请输入主密码",
|
||||
|
||||
@@ -644,16 +644,34 @@ const zhTW: Record<string, string> = {
|
||||
"txt_account_passkey_not_found": "未找到賬號通行密鑰",
|
||||
"txt_account_passkey_prf_not_available": "這把通行密鑰無法返回 PRF 密鑰",
|
||||
"txt_account_passkey_direct_unlock_enabled": "已開啟直接解鎖",
|
||||
"txt_account_passkey_direct_unlock_unavailable_title": "無法直接解鎖",
|
||||
"txt_account_passkey_direct_unlock_unavailable_message": "這把通行密鑰沒有返回 PRF 密鑰,因此不能直接解鎖密碼庫。你仍然可以把它保存為僅登錄通行密鑰;登錄後需要輸入主密碼解鎖。",
|
||||
"txt_account_passkey_direct_unlock_unavailable_error": "這把通行密鑰無法直接解鎖密碼庫",
|
||||
"txt_account_passkey_saved_login_only": "已保存為僅登錄通行密鑰",
|
||||
"txt_account_passkey_not_saved": "通行密鑰未保存",
|
||||
"txt_save_login_only_passkey": "保存為僅登錄",
|
||||
"txt_do_not_save": "不保存",
|
||||
"txt_add_account_passkey": "添加賬號通行密鑰",
|
||||
"txt_delete_account_passkey": "刪除賬號通行密鑰",
|
||||
"txt_direct_unlock": "直接解鎖",
|
||||
"txt_enable_passkey_direct_unlock": "開啟直接解鎖",
|
||||
"txt_login_only": "僅登錄",
|
||||
"txt_login_with_passkey": "使用 Passkey 登錄",
|
||||
"txt_login_with_passkey": "使用通行密鑰登錄",
|
||||
"txt_no_account_passkeys": "暫無賬號通行密鑰",
|
||||
"txt_passkey_name": "通行密鑰名稱",
|
||||
"txt_passkey_requires_master_password": "Passkey 已驗證,請輸入主密碼解鎖密碼庫。",
|
||||
"txt_passkey_requires_master_password": "通行密鑰已驗證,請輸入主密碼解鎖密碼庫。",
|
||||
"txt_prf_not_supported": "不支持 PRF",
|
||||
"txt_invalid_passkey_creation_options": "通行密鑰創建選項無效",
|
||||
"txt_invalid_passkey_assertion_options": "通行密鑰驗證選項無效",
|
||||
"txt_invalid_passkey_assertion_response": "通行密鑰驗證響應無效",
|
||||
"txt_invalid_passkey_registration_response": "通行密鑰註冊響應無效",
|
||||
"txt_passkey_browser_not_supported": "當前瀏覽器不支持通行密鑰",
|
||||
"txt_no_passkey_selected": "未選擇通行密鑰",
|
||||
"txt_no_passkey_created": "未創建通行密鑰",
|
||||
"txt_unsupported_encrypted_user_key": "不支持的加密賬號密鑰",
|
||||
"txt_passkey_verification_failed": "通行密鑰驗證失敗",
|
||||
"txt_passkey_cannot_unlock_vault": "這把通行密鑰無法解鎖此密碼庫",
|
||||
"txt_invalid_passkey_vault_key": "通行密鑰密碼庫密鑰無效",
|
||||
"txt_phone": "電話",
|
||||
"txt_please_input_email_and_password": "請輸入郵箱和密碼",
|
||||
"txt_please_input_master_password": "請輸入主密碼",
|
||||
|
||||
Reference in New Issue
Block a user