fix: Resolve database encryption atomicity issues and enhance debugging (#430)
* fix: Resolve database encryption atomicity issues and enhance debugging This commit addresses critical data corruption issues caused by non-atomic file writes during database encryption, and adds comprehensive diagnostic logging to help debug encryption-related failures. **Problem:** Users reported "Unsupported state or unable to authenticate data" errors when starting the application after system crashes or Docker container restarts. The root cause was non-atomic writes of encrypted database files: 1. Encrypted data file written (step 1) 2. Metadata file written (step 2) → If process crashes between steps 1 and 2, files become inconsistent → New IV/tag in data file, old IV/tag in metadata → GCM authentication fails on next startup → User data permanently inaccessible **Solution - Atomic Writes:** 1. Write-to-temp + atomic-rename pattern: - Write to temporary files (*.tmp-timestamp-pid) - Perform atomic rename operations - Clean up temp files on failure 2. Data integrity validation: - Add dataSize field to metadata - Verify file size before decryption - Early detection of corrupted writes 3. Enhanced error diagnostics: - Key fingerprints (SHA256 prefix) for verification - File modification timestamps - Detailed GCM auth failure messages - Automatic diagnostic info generation **Changes:** database-file-encryption.ts: - Implement atomic write pattern in encryptDatabaseFromBuffer - Implement atomic write pattern in encryptDatabaseFile - Add dataSize field to EncryptedFileMetadata interface - Validate file size before decryption in decryptDatabaseToBuffer - Enhanced error messages for GCM auth failures - Add getDiagnosticInfo() function for comprehensive debugging - Add debug logging for all encryption/decryption operations system-crypto.ts: - Add detailed logging for DATABASE_KEY initialization - Log key source (env var vs .env file) - Add key fingerprints to all log messages - Better error messages when key loading fails db/index.ts: - Automatically generate diagnostic info on decryption failure - Log detailed debugging information to help users troubleshoot **Debugging Info Added:** - Key initialization: source, fingerprint, length, path - Encryption: original size, encrypted size, IV/tag prefixes, temp paths - Decryption: file timestamps, metadata content, key fingerprint matching - Auth failures: .env file status, key availability, file consistency - File diagnostics: existence, readability, size validation, mtime comparison **Backward Compatibility:** - dataSize field is optional (metadata.dataSize?: number) - Old encrypted files without dataSize continue to work - No migration required **Testing:** - Compiled successfully - No breaking changes to existing APIs - Graceful handling of legacy v1 encrypted files Fixes data loss issues reported by users experiencing container restarts and system crashes during database saves. * fix: Cleanup PR * Update src/backend/utils/database-file-encryption.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/backend/utils/database-file-encryption.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/backend/utils/database-file-encryption.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/backend/utils/database-file-encryption.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/backend/utils/database-file-encryption.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit was merged in pull request #430.
This commit is contained in:
@@ -24,11 +24,20 @@ import {
|
||||
verifyTOTPLogin,
|
||||
getServerConfig,
|
||||
isElectron,
|
||||
logoutUser,
|
||||
} from "../../main-axios.ts";
|
||||
import { ElectronServerConfig as ServerConfigComponent } from "@/ui/desktop/authentication/ElectronServerConfig.tsx";
|
||||
import { ElectronLoginForm } from "@/ui/desktop/authentication/ElectronLoginForm.tsx";
|
||||
|
||||
function getCookie(name: string): string | undefined {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(";").shift();
|
||||
}
|
||||
|
||||
interface ExtendedWindow extends Window {
|
||||
IS_ELECTRON_WEBVIEW?: boolean;
|
||||
}
|
||||
|
||||
interface AuthProps extends React.ComponentProps<"div"> {
|
||||
setLoggedIn: (loggedIn: boolean) => void;
|
||||
setIsAdmin: (isAdmin: boolean) => void;
|
||||
@@ -37,7 +46,6 @@ interface AuthProps extends React.ComponentProps<"div"> {
|
||||
loggedIn: boolean;
|
||||
authLoading: boolean;
|
||||
setDbError: (error: string | null) => void;
|
||||
dbError?: string | null;
|
||||
onAuthSuccess: (authData: {
|
||||
isAdmin: boolean;
|
||||
username: string | null;
|
||||
@@ -54,21 +62,20 @@ export function Auth({
|
||||
loggedIn,
|
||||
authLoading,
|
||||
setDbError,
|
||||
dbError,
|
||||
onAuthSuccess,
|
||||
...props
|
||||
}: AuthProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isInElectronWebView = () => {
|
||||
if ((window as any).IS_ELECTRON_WEBVIEW) {
|
||||
if ((window as ExtendedWindow).IS_ELECTRON_WEBVIEW) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (window.self !== window.top) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
@@ -126,7 +133,7 @@ export function Auth({
|
||||
userId: meRes.userId || null,
|
||||
});
|
||||
toast.success(t("messages.loginSuccess"));
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
toast.error(t("errors.failedUserInfo"));
|
||||
}
|
||||
}, [
|
||||
@@ -206,7 +213,7 @@ export function Auth({
|
||||
.finally(() => {
|
||||
setDbHealthChecking(false);
|
||||
});
|
||||
}, [setDbError, firstUserToastShown, showServerConfig]);
|
||||
}, [setDbError, firstUserToastShown, showServerConfig, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!registrationAllowed && !internalLoggedIn) {
|
||||
@@ -282,9 +289,9 @@ export function Auth({
|
||||
);
|
||||
setWebviewAuthSuccess(true);
|
||||
setTimeout(() => window.location.reload(), 100);
|
||||
setLoading(false);
|
||||
return;
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
console.error("Error posting auth success message:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const [meRes] = await Promise.all([getUserInfo()]);
|
||||
@@ -461,7 +468,9 @@ export function Auth({
|
||||
setTimeout(() => window.location.reload(), 100);
|
||||
setTotpLoading(false);
|
||||
return;
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
console.error("Error posting auth success message:", e);
|
||||
}
|
||||
}
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
@@ -569,7 +578,9 @@ export function Auth({
|
||||
setTimeout(() => window.location.reload(), 100);
|
||||
setOidcLoading(false);
|
||||
return;
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
console.error("Error posting auth success message:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,7 +618,16 @@ export function Auth({
|
||||
setOidcLoading(false);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}, [
|
||||
onAuthSuccess,
|
||||
setDbError,
|
||||
setIsAdmin,
|
||||
setLoggedIn,
|
||||
setUserId,
|
||||
setUsername,
|
||||
t,
|
||||
isInElectronWebView,
|
||||
]);
|
||||
|
||||
const Spinner = (
|
||||
<svg
|
||||
|
||||
Reference in New Issue
Block a user