mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 14:50:11 +00:00
fix: address security issue
This commit is contained in:
@@ -49,6 +49,11 @@ export interface BackupSettingsRepairStateResponse {
|
||||
portable: BackupSettingsPortablePayload | null;
|
||||
}
|
||||
|
||||
export interface BackupUserVerificationPayload {
|
||||
masterPasswordHash?: string | null;
|
||||
userVerificationToken?: string | null;
|
||||
}
|
||||
|
||||
export interface AdminBackupRunResponse {
|
||||
object: 'backup-run';
|
||||
result: {
|
||||
@@ -173,12 +178,13 @@ async function applyBackupFileIntegrityName(fileName: string, bytes: Uint8Array)
|
||||
|
||||
export async function exportAdminBackup(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string,
|
||||
includeAttachments: boolean = false
|
||||
): Promise<AdminBackupExportPayload> {
|
||||
const resp = await authedFetch('/api/admin/backup/export', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ includeAttachments }),
|
||||
body: JSON.stringify({ includeAttachments, masterPasswordHash }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_export_failed')));
|
||||
|
||||
@@ -201,10 +207,11 @@ export async function downloadAdminBackupAttachmentBlob(
|
||||
|
||||
export async function buildCompleteAdminBackupExport(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string,
|
||||
includeAttachments: boolean = false,
|
||||
onProgress?: (event: BackupExportClientProgressEvent) => void | Promise<void>
|
||||
): Promise<AdminBackupExportPayload> {
|
||||
const payload = await exportAdminBackup(authedFetch, includeAttachments);
|
||||
const payload = await exportAdminBackup(authedFetch, masterPasswordHash, includeAttachments);
|
||||
if (!includeAttachments) {
|
||||
await onProgress?.({
|
||||
operation: 'backup-export',
|
||||
@@ -278,12 +285,13 @@ export async function getAdminBackupSettings(authedFetch: AuthedFetch): Promise<
|
||||
|
||||
export async function saveAdminBackupSettings(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string,
|
||||
settings: AdminBackupSettings
|
||||
): Promise<AdminBackupSettings> {
|
||||
const resp = await authedFetch('/api/admin/backup/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
body: JSON.stringify({ ...settings, masterPasswordHash }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_settings_save_failed')));
|
||||
const body = await parseJson<AdminBackupSettings>(resp);
|
||||
@@ -305,12 +313,13 @@ export async function getAdminBackupSettingsRepairState(
|
||||
|
||||
export async function repairAdminBackupSettings(
|
||||
authedFetch: AuthedFetch,
|
||||
verification: BackupUserVerificationPayload,
|
||||
settings: AdminBackupSettings
|
||||
): Promise<AdminBackupSettings> {
|
||||
const resp = await authedFetch('/api/admin/backup/settings/repair', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
body: JSON.stringify({ ...settings, ...verification }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_settings_save_failed')));
|
||||
const body = await parseJson<AdminBackupSettings>(resp);
|
||||
@@ -320,12 +329,13 @@ export async function repairAdminBackupSettings(
|
||||
|
||||
export async function runAdminBackupNow(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string,
|
||||
destinationId?: string | null
|
||||
): Promise<AdminBackupRunResponse> {
|
||||
const resp = await authedFetch('/api/admin/backup/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(destinationId ? { destinationId } : {}),
|
||||
body: JSON.stringify(destinationId ? { destinationId, masterPasswordHash } : { masterPasswordHash }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_run_failed')));
|
||||
const body = await parseJson<AdminBackupRunResponse>(resp);
|
||||
@@ -351,14 +361,16 @@ export async function listRemoteBackups(
|
||||
|
||||
export async function downloadRemoteBackup(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string,
|
||||
destinationId: string,
|
||||
path: string,
|
||||
onProgress?: (percent: number | null) => void
|
||||
): Promise<AdminBackupExportPayload> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('destinationId', destinationId);
|
||||
params.set('path', path);
|
||||
const resp = await authedFetch(`/api/admin/backup/remote/download?${params.toString()}`, { method: 'GET' });
|
||||
const resp = await authedFetch('/api/admin/backup/remote/download', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ destinationId, path, masterPasswordHash }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_download_failed')));
|
||||
const mimeType = String(resp.headers.get('Content-Type') || 'application/zip').trim() || 'application/zip';
|
||||
const fileName = parseContentDispositionFileName(resp, 'nodewarden_remote_backup.zip');
|
||||
@@ -418,6 +430,7 @@ export async function inspectRemoteBackupIntegrity(
|
||||
|
||||
export async function restoreRemoteBackup(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string,
|
||||
destinationId: string,
|
||||
path: string,
|
||||
replaceExisting: boolean = false,
|
||||
@@ -426,7 +439,7 @@ export async function restoreRemoteBackup(
|
||||
const resp = await authedFetch('/api/admin/backup/remote/restore', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ destinationId, path, replaceExisting, allowChecksumMismatch }),
|
||||
body: JSON.stringify({ destinationId, path, replaceExisting, allowChecksumMismatch, masterPasswordHash }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_restore_failed')));
|
||||
const body = await parseJson<AdminBackupImportResponse>(resp);
|
||||
@@ -436,12 +449,14 @@ export async function restoreRemoteBackup(
|
||||
|
||||
export async function importAdminBackup(
|
||||
authedFetch: AuthedFetch,
|
||||
masterPasswordHash: string,
|
||||
file: File,
|
||||
replaceExisting: boolean = false,
|
||||
allowChecksumMismatch: boolean = false
|
||||
): Promise<AdminBackupImportResponse> {
|
||||
const formData = new FormData();
|
||||
formData.set('file', file, file.name || 'nodewarden_backup.zip');
|
||||
formData.set('masterPasswordHash', masterPasswordHash);
|
||||
if (replaceExisting) {
|
||||
formData.set('replaceExisting', '1');
|
||||
}
|
||||
|
||||
@@ -66,6 +66,12 @@ export interface CompletedLogin {
|
||||
session: SessionState;
|
||||
profile: Profile;
|
||||
profilePromise: Promise<Profile>;
|
||||
freshMasterPasswordHash?: string | null;
|
||||
freshUserVerificationToken?: string | null;
|
||||
}
|
||||
|
||||
function readTokenUserVerificationToken(token: TokenSuccess): string | null {
|
||||
return String(token.UserVerificationToken || token.userVerificationToken || '').trim() || null;
|
||||
}
|
||||
|
||||
export type PasswordLoginResult =
|
||||
@@ -319,7 +325,8 @@ export async function completeLogin(
|
||||
token: TokenSuccess,
|
||||
email: string,
|
||||
masterKey: Uint8Array,
|
||||
fallbackKdfIterations: number
|
||||
fallbackKdfIterations: number,
|
||||
freshMasterPasswordHash?: string | null
|
||||
): Promise<CompletedLogin> {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const fallbackProfile = loadProfileSnapshot(normalizedEmail);
|
||||
@@ -348,6 +355,8 @@ export async function completeLogin(
|
||||
session: { ...baseSession, ...keys },
|
||||
profile,
|
||||
profilePromise: getProfile(tempFetch),
|
||||
freshMasterPasswordHash: freshMasterPasswordHash || null,
|
||||
freshUserVerificationToken: readTokenUserVerificationToken(token),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -360,7 +369,8 @@ async function completeLoginWithVaultKeys(
|
||||
token: TokenSuccess,
|
||||
email: string,
|
||||
keys: { symEncKey: string; symMacKey: string },
|
||||
fallbackKdfIterations: number
|
||||
fallbackKdfIterations: number,
|
||||
freshMasterPasswordHash?: string | null
|
||||
): Promise<CompletedLogin> {
|
||||
const normalizedEmail = email.trim().toLowerCase();
|
||||
const fallbackProfile = loadProfileSnapshot(normalizedEmail);
|
||||
@@ -385,6 +395,8 @@ async function completeLoginWithVaultKeys(
|
||||
session: { ...baseSession, ...keys },
|
||||
profile,
|
||||
profilePromise: getProfile(tempFetch),
|
||||
freshMasterPasswordHash: freshMasterPasswordHash || null,
|
||||
freshUserVerificationToken: readTokenUserVerificationToken(token),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -400,7 +412,7 @@ export async function performPasswordLogin(
|
||||
if ('access_token' in token && token.access_token) {
|
||||
return {
|
||||
kind: 'success',
|
||||
login: await completeLogin(token, normalizedEmail, derived.masterKey, derived.kdfIterations),
|
||||
login: await completeLogin(token, normalizedEmail, derived.masterKey, derived.kdfIterations, derived.hash),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -476,7 +488,7 @@ export async function completePasskeyPasswordLogin(
|
||||
password: string
|
||||
): Promise<CompletedLogin> {
|
||||
const derived = await deriveLoginHashLocally(pending.email, password, pending.kdfIterations);
|
||||
return completeLogin(pending.token, pending.email, derived.masterKey, pending.kdfIterations);
|
||||
return completeLogin(pending.token, pending.email, derived.masterKey, pending.kdfIterations, derived.hash);
|
||||
}
|
||||
|
||||
export async function performTotpLogin(
|
||||
@@ -489,7 +501,7 @@ export async function performTotpLogin(
|
||||
rememberDevice,
|
||||
});
|
||||
if ('access_token' in token && token.access_token) {
|
||||
return completeLogin(token, pendingTotp.email, pendingTotp.masterKey, pendingTotp.kdfIterations);
|
||||
return completeLogin(token, pendingTotp.email, pendingTotp.masterKey, pendingTotp.kdfIterations, pendingTotp.passwordHash);
|
||||
}
|
||||
const tokenError = token as { error_description?: string; error?: string };
|
||||
throw new Error(translateServerError(tokenError.error_description || tokenError.error, t('txt_totp_verify_failed')));
|
||||
@@ -508,7 +520,7 @@ export async function performRecoverTwoFactorLogin(
|
||||
|
||||
if ('access_token' in token && token.access_token) {
|
||||
return {
|
||||
login: await completeLogin(token, normalizedEmail, derived.masterKey, derived.kdfIterations),
|
||||
login: await completeLogin(token, normalizedEmail, derived.masterKey, derived.kdfIterations, derived.hash),
|
||||
newRecoveryCode: recovered.newRecoveryCode || null,
|
||||
};
|
||||
}
|
||||
@@ -557,6 +569,7 @@ export async function performUnlock(
|
||||
session: offline.session,
|
||||
profile: offline.profile,
|
||||
profilePromise: Promise.resolve(offline.profile),
|
||||
freshMasterPasswordHash: null,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
@@ -589,7 +602,7 @@ export async function performUnlock(
|
||||
if ('access_token' in token && token.access_token) {
|
||||
return {
|
||||
kind: 'success',
|
||||
login: await completeLogin(token, normalizedEmail, derived.masterKey, derived.kdfIterations),
|
||||
login: await completeLogin(token, normalizedEmail, derived.masterKey, derived.kdfIterations, derived.hash),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import type { Profile, SessionState } from './types';
|
||||
|
||||
export async function silentlyRepairBackupSettingsIfNeeded(
|
||||
activeSession: SessionState,
|
||||
activeProfile: Profile
|
||||
activeProfile: Profile,
|
||||
verification?: { masterPasswordHash?: string | null; userVerificationToken?: string | null } | null
|
||||
): Promise<void> {
|
||||
if (activeProfile.role !== 'admin') return;
|
||||
if (!activeSession.accessToken || !activeSession.symEncKey || !activeSession.symMacKey) return;
|
||||
@@ -14,8 +15,9 @@ export async function silentlyRepairBackupSettingsIfNeeded(
|
||||
try {
|
||||
const state = await getAdminBackupSettingsRepairState(tempFetch);
|
||||
if (!state.needsRepair || !state.portable) return;
|
||||
if (!verification?.masterPasswordHash && !verification?.userVerificationToken) return;
|
||||
const repairedSettings = await decryptPortableBackupSettings(state.portable, activeProfile, activeSession);
|
||||
await repairAdminBackupSettings(tempFetch, repairedSettings);
|
||||
await repairAdminBackupSettings(tempFetch, verification, repairedSettings);
|
||||
} catch (error) {
|
||||
console.error('Backup settings auto-repair failed:', error);
|
||||
}
|
||||
|
||||
@@ -1156,32 +1156,32 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
||||
notify('success', t('txt_logs_cleared'));
|
||||
return 0;
|
||||
},
|
||||
onExportBackup: async () => {
|
||||
onExportBackup: async (_masterPassword: string) => {
|
||||
notify('success', t('txt_backup_export_success'));
|
||||
},
|
||||
onImportBackup: async () => {
|
||||
onImportBackup: async (_masterPassword: string, _file: File, _replaceExisting?: boolean) => {
|
||||
resetDemoVaultState(state);
|
||||
notify('success', t('txt_backup_import_success_relogin'));
|
||||
return createDemoImportBackupResult();
|
||||
},
|
||||
onImportBackupAllowingChecksumMismatch: async () => {
|
||||
onImportBackupAllowingChecksumMismatch: async (_masterPassword: string, _file: File, _replaceExisting?: boolean) => {
|
||||
resetDemoVaultState(state);
|
||||
notify('success', t('txt_backup_import_success_relogin'));
|
||||
return createDemoImportBackupResult();
|
||||
},
|
||||
onLoadBackupSettings: async () => state.backupSettings,
|
||||
onSaveBackupSettings: async (settings) => {
|
||||
onSaveBackupSettings: async (_masterPassword: string, settings) => {
|
||||
const next = cloneJson(settings);
|
||||
state.setBackupSettings(next);
|
||||
notify('success', t('txt_backup_settings_saved'));
|
||||
return next;
|
||||
},
|
||||
onRunRemoteBackup: async (destinationId?: string | null) => {
|
||||
onRunRemoteBackup: async (_masterPassword: string, destinationId?: string | null) => {
|
||||
notify('success', t('txt_backup_remote_run_success'));
|
||||
return createDemoBackupRun(state.backupSettings, destinationId);
|
||||
},
|
||||
onListRemoteBackups: async (destinationId: string, path: string) => createDemoRemoteBrowser(destinationId, path),
|
||||
onDownloadRemoteBackup: async () => {
|
||||
onDownloadRemoteBackup: async (_masterPassword: string, _destinationId: string, _path: string, _onProgress?: (percent: number | null) => void) => {
|
||||
notify('success', t('txt_demo_download_prepared'));
|
||||
},
|
||||
onInspectRemoteBackup: async (_destinationId: string, path: string) => ({
|
||||
@@ -1199,13 +1199,13 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
||||
onDeleteRemoteBackup: async () => {
|
||||
notify('success', t('txt_backup_remote_delete_success'));
|
||||
},
|
||||
onRestoreRemoteBackup: async (_destinationId, path) => {
|
||||
onRestoreRemoteBackup: async (_masterPassword: string, _destinationId, path) => {
|
||||
await runDemoRemoteRestoreProgress(path.split('/').pop() || path || 'nodewarden_backup_demo.zip');
|
||||
resetDemoVaultState(state);
|
||||
notify('success', t('txt_backup_remote_restore_completed_verified'));
|
||||
return createDemoImportBackupResult();
|
||||
},
|
||||
onRestoreRemoteBackupAllowingChecksumMismatch: async (_destinationId, path) => {
|
||||
onRestoreRemoteBackupAllowingChecksumMismatch: async (_masterPassword: string, _destinationId, path) => {
|
||||
await runDemoRemoteRestoreProgress(path.split('/').pop() || path || 'nodewarden_backup_demo.zip');
|
||||
resetDemoVaultState(state);
|
||||
notify('success', t('txt_backup_remote_restore_completed_verified'));
|
||||
|
||||
@@ -314,6 +314,8 @@ export interface TokenSuccess {
|
||||
ResetMasterPassword?: boolean;
|
||||
scope?: string;
|
||||
unofficialServer?: boolean;
|
||||
UserVerificationToken?: string;
|
||||
userVerificationToken?: string;
|
||||
UserDecryptionOptions?: unknown;
|
||||
userDecryptionOptions?: unknown;
|
||||
VaultKeys?: {
|
||||
|
||||
Reference in New Issue
Block a user