Refactor: Remove passkey-related functionality and types

- Deleted passkey-related interfaces and types from index.ts and types.ts.
- Removed passkey handling from App component, including related state and functions.
- Cleaned up API calls in auth.ts, removing passkey registration and login functions.
- Updated vault and import formats to eliminate passkey references.
- Removed passkey support checks and UI elements from AuthViews and SettingsPage.
- Cleaned up unused passkey helper functions and constants.
- Adjusted related components and hooks to ensure consistent functionality without passkey support.
This commit is contained in:
shuaiplus
2026-04-06 00:46:13 +08:00
parent 90a7731351
commit 76623d7201
28 changed files with 28 additions and 1064 deletions
-87
View File
@@ -8,7 +8,6 @@ import type {
TokenSuccess,
} from '../types';
import { parseJson, type AuthedFetch, type SessionSetter } from './shared';
import { createPasskeyCredential, requestPasskeyAssertion } from '../passkey';
const SESSION_KEY = 'nodewarden.web.session.v4';
const DEVICE_IDENTIFIER_KEY = 'nodewarden.web.device.identifier.v1';
@@ -27,14 +26,6 @@ export interface PreloginKdfConfig {
kdfParallelism: number | null;
}
export interface AccountPasskey {
id: string;
name: string;
creationDate: string;
revisionDate: string;
lastUsedDate: string | null;
}
function randomHex(length: number): string {
const bytes = crypto.getRandomValues(new Uint8Array(Math.max(1, Math.ceil(length / 2))));
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('').slice(0, length);
@@ -206,84 +197,6 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenSuc
return json || null;
}
export async function listAccountPasskeys(authedFetch: AuthedFetch): Promise<AccountPasskey[]> {
const resp = await authedFetch('/api/accounts/passkeys');
if (!resp.ok) throw new Error('Failed to load passkeys');
const body = (await parseJson<{ data?: AccountPasskey[] }>(resp)) || {};
return Array.isArray(body.data) ? body.data : [];
}
export async function registerAccountPasskey(authedFetch: AuthedFetch, name: string, session: SessionState): Promise<void> {
const beginResp = await authedFetch('/api/accounts/passkeys/begin-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
});
if (!beginResp.ok) throw new Error('Failed to start passkey registration');
const begin = (await parseJson<{ challengeId: string; publicKey: Record<string, any> }>(beginResp)) || {};
if (!begin.challengeId || !begin.publicKey) throw new Error('Invalid registration challenge');
const credential = await createPasskeyCredential(begin.publicKey);
const finishResp = await authedFetch('/api/accounts/passkeys/finish-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challengeId: begin.challengeId,
name,
wrappedVaultKeys: JSON.stringify({
symEncKey: session.symEncKey || '',
symMacKey: session.symMacKey || '',
}),
credential,
}),
});
if (!finishResp.ok) {
const err = await parseJson<TokenError>(finishResp);
throw new Error(err?.error_description || err?.error || 'Failed to finish passkey registration');
}
}
export async function renameAccountPasskey(authedFetch: AuthedFetch, passkeyId: string, name: string): Promise<void> {
const resp = await authedFetch(`/api/accounts/passkeys/${passkeyId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
if (!resp.ok) throw new Error('Failed to rename passkey');
}
export async function deleteAccountPasskey(authedFetch: AuthedFetch, passkeyId: string): Promise<void> {
const resp = await authedFetch(`/api/accounts/passkeys/${passkeyId}`, { method: 'DELETE' });
if (!resp.ok && resp.status !== 204) throw new Error('Failed to delete passkey');
}
export async function loginWithPasskey(email?: string, totpCode?: string): Promise<TokenSuccess | TokenError> {
const beginResp = await fetch('/identity/passkeys/begin-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: String(email || '').trim().toLowerCase() || undefined }),
});
if (!beginResp.ok) return ((await parseJson<TokenError>(beginResp)) || {});
const begin = (await parseJson<{ challengeId: string; publicKey: Record<string, any> }>(beginResp)) || {};
if (!begin.challengeId || !begin.publicKey) return { error: 'Passkey challenge missing' };
const credential = await requestPasskeyAssertion(begin.publicKey);
const finishResp = await fetch('/identity/passkeys/finish-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challengeId: begin.challengeId,
credential,
deviceIdentifier: getOrCreateDeviceIdentifier(),
deviceName: guessDeviceName(),
deviceType: '14',
twoFactorToken: totpCode || undefined,
}),
});
const result = (await parseJson<TokenSuccess & TokenError>(finishResp)) || {};
return result;
}
export async function registerAccount(args: {
email: string;
name: string;
-55
View File
@@ -392,56 +392,6 @@ function toIsoDateOrNow(value: unknown): string {
return parsed.toISOString();
}
async function encryptMaybeFidoValue(
value: unknown,
enc: Uint8Array,
mac: Uint8Array,
fallback = ''
): Promise<string> {
const normalized = String(value ?? '').trim() || fallback;
if (looksLikeCipherString(normalized)) return normalized;
return encryptBw(new TextEncoder().encode(normalized), enc, mac);
}
async function encryptMaybeNullableFidoValue(
value: unknown,
enc: Uint8Array,
mac: Uint8Array
): Promise<string | null> {
const normalized = String(value ?? '').trim();
if (!normalized) return null;
if (looksLikeCipherString(normalized)) return normalized;
return encryptBw(new TextEncoder().encode(normalized), enc, mac);
}
async function normalizeFido2Credentials(
credentials: Array<Record<string, unknown>> | null | undefined,
enc: Uint8Array,
mac: Uint8Array
): Promise<Array<Record<string, unknown>> | null> {
if (!Array.isArray(credentials) || credentials.length === 0) return null;
const out: Array<Record<string, unknown>> = [];
for (const credential of credentials) {
if (!credential || typeof credential !== 'object') continue;
out.push({
credentialId: await encryptMaybeFidoValue(credential.credentialId, enc, mac),
keyType: await encryptMaybeFidoValue(credential.keyType, enc, mac, 'public-key'),
keyAlgorithm: await encryptMaybeFidoValue(credential.keyAlgorithm, enc, mac, 'ECDSA'),
keyCurve: await encryptMaybeFidoValue(credential.keyCurve, enc, mac, 'P-256'),
keyValue: await encryptMaybeFidoValue(credential.keyValue, enc, mac),
rpId: await encryptMaybeFidoValue(credential.rpId, enc, mac),
rpName: await encryptMaybeNullableFidoValue(credential.rpName, enc, mac),
userHandle: await encryptMaybeNullableFidoValue(credential.userHandle, enc, mac),
userName: await encryptMaybeNullableFidoValue(credential.userName, enc, mac),
userDisplayName: await encryptMaybeNullableFidoValue(credential.userDisplayName, enc, mac),
counter: await encryptMaybeFidoValue(credential.counter, enc, mac, '0'),
discoverable: await encryptMaybeFidoValue(credential.discoverable, enc, mac, 'false'),
creationDate: toIsoDateOrNow(credential.creationDate),
});
}
return out.length ? out : null;
}
async function getCipherKeys(
cipher: Cipher | null,
userEnc: Uint8Array,
@@ -490,15 +440,10 @@ async function buildCipherPayload(
}
if (type === 1) {
const existingFido2 =
cipher?.login && Array.isArray((cipher.login as any).fido2Credentials)
? (cipher.login as any).fido2Credentials
: draft.loginFido2Credentials;
payload.login = {
username: await encryptTextValue(draft.loginUsername, keys.enc, keys.mac),
password: await encryptTextValue(draft.loginPassword, keys.enc, keys.mac),
totp: await encryptTextValue(draft.loginTotp, keys.enc, keys.mac),
fido2Credentials: await normalizeFido2Credentials(existingFido2, keys.enc, keys.mac),
uris: await encryptUris(draft.loginUris || [], keys.enc, keys.mac),
};
} else if (type === 3) {
-32
View File
@@ -4,7 +4,6 @@ import {
getProfile,
loadSession,
loginWithPassword,
loginWithPasskey,
refreshAccessToken,
recoverTwoFactor,
registerAccount,
@@ -47,11 +46,6 @@ export type PasswordLoginResult =
| { kind: 'totp'; pendingTotp: PendingTotp }
| { kind: 'error'; message: string };
export type PasskeyLoginResult =
| { kind: 'success'; login: CompletedLogin }
| { kind: 'totp' }
| { kind: 'error'; message: string };
export interface RecoverTwoFactorResult {
login: CompletedLogin | null;
newRecoveryCode: string | null;
@@ -366,29 +360,3 @@ export async function performUnlock(
return { ...refreshedSession, ...keys };
}
export async function performPasskeyLogin(email: string, totpCode?: string): Promise<PasskeyLoginResult> {
const token = await loginWithPasskey(email, totpCode);
if ('access_token' in token && token.access_token) {
const normalizedEmail = String(email || '').trim().toLowerCase();
const baseSession: SessionState = {
accessToken: token.access_token,
refreshToken: token.refresh_token,
email: normalizedEmail,
symEncKey: token.VaultKeys?.symEncKey,
symMacKey: token.VaultKeys?.symMacKey,
};
const tempFetch = createAuthedFetch(() => baseSession, () => {});
const profile = buildTransientProfile(token, normalizedEmail);
return {
kind: 'success',
login: {
session: baseSession,
profile,
profilePromise: getProfile(tempFetch),
},
};
}
const tokenError = token as { TwoFactorProviders?: unknown; error_description?: string; error?: string };
if (tokenError.TwoFactorProviders) return { kind: 'totp' };
return { kind: 'error', message: tokenError.error_description || tokenError.error || 'Passkey login failed' };
}
-6
View File
@@ -99,7 +99,6 @@ export function buildEmptyImportDraft(type: number): VaultDraft {
loginPassword: '',
loginTotp: '',
loginUris: [{ uri: '', match: null }],
loginFido2Credentials: [],
cardholderName: '',
cardNumber: '',
cardBrand: '',
@@ -161,11 +160,6 @@ export function importCipherToDraft(cipher: Record<string, unknown>, folderId: s
draft.loginUsername = asText(login.username);
draft.loginPassword = asText(login.password);
draft.loginTotp = asText(login.totp);
draft.loginFido2Credentials = Array.isArray(login.fido2Credentials)
? login.fido2Credentials
.filter((credential): credential is Record<string, unknown> => !!credential && typeof credential === 'object')
.map((credential) => ({ ...credential }))
: [];
const urisRaw = Array.isArray(login.uris) ? login.uris : [];
const uris = urisRaw
.map((u) => {
-4
View File
@@ -198,7 +198,6 @@ function mapCipherEncrypted(cipher: Cipher): Record<string, unknown> {
match: (uri as { match?: unknown })?.match ?? null,
}))
: [],
fido2Credentials: Array.isArray(login.fido2Credentials) ? cloneValue(login.fido2Credentials) : [],
}
: null;
@@ -292,9 +291,6 @@ async function mapCipherPlain(cipher: Cipher, userEnc: Uint8Array, userMac: Uint
}))
)
: [],
fido2Credentials: Array.isArray(cipher.login.fido2Credentials)
? await Promise.all(cipher.login.fido2Credentials.map((credential) => deepDecryptUnknown(credential, keyParts.enc, keyParts.mac)))
: [],
};
} else {
out.login = null;
-4
View File
@@ -671,8 +671,6 @@ const messages: Record<Locale, Record<string, string>> = {
txt_total_items_count: "{count} items",
txt_totp_secret: "TOTP Secret",
txt_totp_verify_failed: "TOTP verify failed",
txt_passkey: "Passkey",
txt_passkey_created_at_value: "Created at {value}",
txt_attachments: "Attachments",
txt_upload_attachments: "Upload attachments",
txt_new_attachments: "New attachments",
@@ -1431,8 +1429,6 @@ zhCNOverrides.txt_lock = '锁定';
zhCNOverrides.txt_menu = '菜单';
zhCNOverrides.txt_settings = '设置';
zhCNOverrides.txt_back = '返回';
zhCNOverrides.txt_passkey = 'Passkey';
zhCNOverrides.txt_passkey_created_at_value = '创建于 {value}';
zhCNOverrides.txt_attachments = '附件';
zhCNOverrides.txt_upload_attachments = '上传附件';
zhCNOverrides.txt_new_attachments = '待上传附件';
+1 -1
View File
@@ -223,7 +223,7 @@ export function makeLoginCipher(): Record<string, unknown> {
favorite: false,
reprompt: 0,
key: null,
login: { username: null, password: null, totp: null, fido2Credentials: null, uris: null },
login: { username: null, password: null, totp: null, uris: null },
card: null,
identity: null,
secureNote: null,
@@ -31,7 +31,6 @@ export interface BitwardenCipherInput {
username?: string | null;
password?: string | null;
totp?: string | null;
fido2Credentials?: Array<Record<string, unknown>> | null;
} | null;
card?: Record<string, unknown> | null;
identity?: Record<string, unknown> | null;
@@ -90,7 +89,6 @@ export function normalizeBitwardenImport(raw: unknown): CiphersImportPayload {
username: item.login.username ?? null,
password: item.login.password ?? null,
totp: item.login.totp ?? null,
fido2Credentials: Array.isArray(item.login.fido2Credentials) ? item.login.fido2Credentials : null,
uris: Array.isArray(item.login.uris)
? item.login.uris.map((u) => ({ uri: u?.uri ?? null, match: u?.match ?? null }))
: null,
-72
View File
@@ -1,72 +0,0 @@
function base64UrlToBytes(input: string): Uint8Array {
const normalized = String(input || '').replace(/-/g, '+').replace(/_/g, '/');
const padded = normalized + '='.repeat((4 - (normalized.length % 4 || 4)) % 4);
const binary = atob(padded);
const out = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
return out;
}
function bytesToBase64Url(bytes: ArrayBuffer | Uint8Array): string {
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
let binary = '';
for (const b of view) binary += String.fromCharCode(b);
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
export function passkeySupported(): boolean {
return typeof window !== 'undefined' && !!window.PublicKeyCredential;
}
export async function createPasskeyCredential(publicKey: Record<string, any>): Promise<any> {
const options: PublicKeyCredentialCreationOptions = {
...(publicKey as PublicKeyCredentialCreationOptions),
challenge: base64UrlToBytes(publicKey.challenge),
user: {
...publicKey.user,
id: base64UrlToBytes(publicKey.user.id),
},
excludeCredentials: Array.isArray(publicKey.excludeCredentials)
? publicKey.excludeCredentials.map((item: any) => ({ ...item, id: base64UrlToBytes(item.id) }))
: [],
};
const credential = (await navigator.credentials.create({ publicKey: options })) as PublicKeyCredential | null;
if (!credential) throw new Error('Passkey creation was cancelled');
const response = credential.response as AuthenticatorAttestationResponse;
return {
id: credential.id,
rawId: bytesToBase64Url(credential.rawId),
type: credential.type,
response: {
clientDataJSON: bytesToBase64Url(response.clientDataJSON),
attestationObject: bytesToBase64Url(response.attestationObject),
},
};
}
export async function requestPasskeyAssertion(publicKey: Record<string, any>): Promise<any> {
const options: PublicKeyCredentialRequestOptions = {
...(publicKey as PublicKeyCredentialRequestOptions),
challenge: base64UrlToBytes(publicKey.challenge),
allowCredentials: Array.isArray(publicKey.allowCredentials)
? publicKey.allowCredentials.map((item: any) => ({ ...item, id: base64UrlToBytes(item.id) }))
: undefined,
};
const credential = (await navigator.credentials.get({ publicKey: options })) as PublicKeyCredential | null;
if (!credential) throw new Error('Passkey login was cancelled');
const response = credential.response as AuthenticatorAssertionResponse;
return {
id: credential.id,
rawId: bytesToBase64Url(credential.rawId),
type: credential.type,
response: {
clientDataJSON: bytesToBase64Url(response.clientDataJSON),
authenticatorData: bytesToBase64Url(response.authenticatorData),
signature: bytesToBase64Url(response.signature),
userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,
},
};
}
-7
View File
@@ -48,17 +48,11 @@ export interface CipherAttachment {
object?: string;
}
export interface CipherLoginPasskey {
creationDate?: string | null;
[key: string]: unknown;
}
export interface CipherLogin {
username?: string | null;
password?: string | null;
totp?: string | null;
uris?: CipherLoginUri[] | null;
fido2Credentials?: CipherLoginPasskey[] | null;
decUsername?: string;
decPassword?: string;
decTotp?: string;
@@ -228,7 +222,6 @@ export interface VaultDraft {
loginPassword: string;
loginTotp: string;
loginUris: VaultDraftLoginUri[];
loginFido2Credentials: Array<Record<string, unknown>>;
cardholderName: string;
cardNumber: string;
cardBrand: string;