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:
ZacharyZcR
2025-11-06 11:21:21 +08:00
committed by GitHub
parent 9ca7df6542
commit bccfd596b8
5 changed files with 366 additions and 36 deletions

View File

@@ -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