fix: Resolve database encryption atomicity issues and enhance debugging #430

Merged
ZacharyZcR merged 7 commits from fix/database-encryption-atomicity into dev-1.8.1 2025-11-06 03:21:21 +00:00
4 changed files with 35 additions and 149 deletions
Showing only changes of commit bb6214ca85 - Show all commits

View File

@@ -746,7 +746,6 @@ jobs:
mkdir -p homebrew-submission/Casks/t
cp homebrew/termix.rb homebrew-submission/Casks/t/termix.rb
cp homebrew/README.md homebrew-submission/
sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb
sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb

View File

@@ -59,29 +59,9 @@ class DatabaseFileEncryption {
dataSize: encrypted.length,
};
databaseLogger.debug("Starting atomic encryption write", {
operation: "database_buffer_encryption_start",
targetPath,
tmpPath,
originalSize: buffer.length,
encryptedSize: encrypted.length,
keyFingerprint,
ivPrefix: metadata.iv.substring(0, 8),
tagPrefix: metadata.tag.substring(0, 8),
});
fs.writeFileSync(tmpPath, encrypted);
fs.writeFileSync(tmpMetadataPath, JSON.stringify(metadata, null, 2));
databaseLogger.debug(
"Temporary files written, performing atomic rename",
{
operation: "database_buffer_encryption_rename",
tmpPath,
targetPath,
},
);
if (fs.existsSync(targetPath)) {
fs.unlinkSync(targetPath);
}
@@ -92,13 +72,6 @@ class DatabaseFileEncryption {
}
fs.renameSync(tmpMetadataPath, metadataPath);
databaseLogger.debug("Database buffer encrypted with atomic write", {
operation: "database_buffer_encryption_atomic",
targetPath,
encryptedSize: encrypted.length,
keyFingerprint,
});
return targetPath;
} catch (error) {
try {
@@ -177,29 +150,9 @@ class DatabaseFileEncryption {
dataSize: encrypted.length,
};
databaseLogger.debug("Starting atomic file encryption", {
operation: "database_file_encryption_start",
sourcePath,
encryptedPath,
tmpPath,
originalSize: sourceData.length,
encryptedSize: encrypted.length,
keyFingerprint,
ivPrefix: metadata.iv.substring(0, 8),
});
fs.writeFileSync(tmpPath, encrypted);
fs.writeFileSync(tmpMetadataPath, JSON.stringify(metadata, null, 2));
databaseLogger.debug(
"Temporary files written, performing atomic rename",
{
operation: "database_file_encryption_rename",
tmpPath,
encryptedPath,
},
);
if (fs.existsSync(encryptedPath)) {
fs.unlinkSync(encryptedPath);
}
@@ -267,31 +220,9 @@ class DatabaseFileEncryption {
const dataFileStats = fs.statSync(encryptedPath);
const metaFileStats = fs.statSync(metadataPath);
databaseLogger.debug("Starting database decryption", {
operation: "database_buffer_decryption_start",
encryptedPath,
metadataPath,
dataFileSize: dataFileStats.size,
dataFileMtime: dataFileStats.mtime.toISOString(),
metaFileMtime: metaFileStats.mtime.toISOString(),
dataDir: process.env.DATA_DIR || "./db/data",
});
const metadataContent = fs.readFileSync(metadataPath, "utf8");
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
databaseLogger.debug("Metadata loaded", {
operation: "database_metadata_loaded",
version: metadata.version,
algorithm: metadata.algorithm,
keySource: metadata.keySource,
fingerprint: metadata.fingerprint,
hasDataSize: !!metadata.dataSize,
expectedDataSize: metadata.dataSize,
ivPrefix: metadata.iv?.substring(0, 8),
tagPrefix: metadata.tag?.substring(0, 8),
});
const encryptedData = fs.readFileSync(encryptedPath);
if (metadata.dataSize && encryptedData.length !== metadata.dataSize) {
@@ -342,15 +273,6 @@ class DatabaseFileEncryption {
.digest("hex")
.substring(0, 16);
databaseLogger.debug("Starting decryption with loaded key", {
operation: "database_decryption_attempt",
keyFingerprint,
algorithm: metadata.algorithm,
ivPrefix: metadata.iv.substring(0, 8),
tagPrefix: metadata.tag.substring(0, 8),
dataSize: encryptedData.length,
});
const decipher = crypto.createDecipheriv(
metadata.algorithm,
key,
@@ -363,14 +285,6 @@ class DatabaseFileEncryption {
decipher.final(),
]);
databaseLogger.debug("Database decryption successful", {
operation: "database_buffer_decryption_success",
encryptedPath,
encryptedSize: encryptedData.length,
decryptedSize: decryptedBuffer.length,
keyFingerprint,
});
return decryptedBuffer;
} catch (error) {
const errorMessage =

View File

@@ -51,17 +51,8 @@ class SystemCrypto {
},
);
}
} catch (fileError) {
databaseLogger.warn("Failed to read .env file for JWT secret", {
operation: "jwt_init_file_read_failed",
error:
fileError instanceof Error ? fileError.message : "Unknown error",
});
}
} catch (fileError) {}
databaseLogger.warn("Generating new JWT secret", {
operation: "jwt_generating_new_secret",
});
await this.generateAndGuideUser();
} catch (error) {
databaseLogger.error("Failed to initialize JWT secret", error, {
@@ -92,23 +83,9 @@ class SystemCrypto {
.digest("hex")
.substring(0, 16);
databaseLogger.info("DATABASE_KEY loaded from environment variable", {
operation: "db_key_loaded_from_env",
keyFingerprint,
keyLength: envKey.length,
dataDir,
});
return;
}
databaseLogger.debug(
"DATABASE_KEY not found in environment, checking .env file",
{
operation: "db_key_checking_file",
envPath,
},
);
try {
const envContent = await fs.readFile(envPath, "utf8");
const dbKeyMatch = envContent.match(/^DATABASE_KEY=(.+)$/m);
@@ -122,34 +99,10 @@ class SystemCrypto {
.digest("hex")
.substring(0, 16);
databaseLogger.info("DATABASE_KEY loaded from .env file", {
operation: "db_key_loaded_from_file",
keyFingerprint,
keyLength: dbKeyMatch[1].length,
envPath,
});
return;
} else {
databaseLogger.warn(
"DATABASE_KEY found in .env but invalid or too short",
{
operation: "db_key_invalid_in_file",
envPath,
hasMatch: !!dbKeyMatch,
keyLength: dbKeyMatch?.[1]?.length || 0,
requiredLength: 64,
},
);
}
} catch (fileError) {
databaseLogger.warn("Failed to read .env file for DATABASE_KEY", {
operation: "db_key_file_read_failed",
envPath,
error:
fileError instanceof Error ? fileError.message : "Unknown error",
willGenerateNew: true,
});
}
} catch (fileError) {}
await this.generateAndGuideDatabaseKey();
} catch (error) {

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