v1.9.0 #437
1
.github/workflows/electron.yml
vendored
1
.github/workflows/electron.yml
vendored
@@ -746,7 +746,6 @@ jobs:
|
|||||||
mkdir -p homebrew-submission/Casks/t
|
mkdir -p homebrew-submission/Casks/t
|
||||||
|
|
||||||
cp homebrew/termix.rb homebrew-submission/Casks/t/termix.rb
|
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/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb
|
||||||
sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb
|
sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb
|
||||||
|
|||||||
@@ -95,6 +95,32 @@ async function initializeDatabaseAsync(): Promise<void> {
|
|||||||
databaseKeyLength: process.env.DATABASE_KEY?.length || 0,
|
databaseKeyLength: process.env.DATABASE_KEY?.length || 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
databaseLogger.info(
|
||||||
|
"Generating diagnostic information for database encryption failure",
|
||||||
|
{
|
||||||
|
operation: "db_encryption_diagnostic",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const diagnosticInfo =
|
||||||
|
DatabaseFileEncryption.getDiagnosticInfo(encryptedDbPath);
|
||||||
|
databaseLogger.error(
|
||||||
|
"Database encryption diagnostic completed - check logs above for details",
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
operation: "db_encryption_diagnostic_completed",
|
||||||
|
filesConsistent: diagnosticInfo.validation.filesConsistent,
|
||||||
|
sizeMismatch: diagnosticInfo.validation.sizeMismatch,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (diagError) {
|
||||||
|
databaseLogger.warn("Failed to generate diagnostic information", {
|
||||||
|
operation: "db_diagnostic_failed",
|
||||||
|
error:
|
||||||
|
diagError instanceof Error ? diagError.message : "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Database decryption failed: ${error instanceof Error ? error.message : "Unknown error"}. This prevents data loss.`,
|
`Database decryption failed: ${error instanceof Error ? error.message : "Unknown error"}. This prevents data loss.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface EncryptedFileMetadata {
|
|||||||
algorithm: string;
|
algorithm: string;
|
||||||
keySource?: string;
|
keySource?: string;
|
||||||
salt?: string;
|
salt?: string;
|
||||||
|
dataSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DatabaseFileEncryption {
|
class DatabaseFileEncryption {
|
||||||
@@ -25,6 +26,10 @@ class DatabaseFileEncryption {
|
|||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
targetPath: string,
|
targetPath: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const tmpPath = `${targetPath}.tmp-${Date.now()}-${process.pid}`;
|
||||||
|
const tmpMetadataPath = `${tmpPath}${this.METADATA_FILE_SUFFIX}`;
|
||||||
|
const metadataPath = `${targetPath}${this.METADATA_FILE_SUFFIX}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const key = await this.systemCrypto.getDatabaseKey();
|
const key = await this.systemCrypto.getDatabaseKey();
|
||||||
|
|
||||||
@@ -38,6 +43,12 @@ class DatabaseFileEncryption {
|
|||||||
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||||||
const tag = cipher.getAuthTag();
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
const keyFingerprint = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(key)
|
||||||
|
.digest("hex")
|
||||||
|
.substring(0, 16);
|
||||||
|
|
||||||
const metadata: EncryptedFileMetadata = {
|
const metadata: EncryptedFileMetadata = {
|
||||||
iv: iv.toString("hex"),
|
iv: iv.toString("hex"),
|
||||||
tag: tag.toString("hex"),
|
tag: tag.toString("hex"),
|
||||||
@@ -45,14 +56,35 @@ class DatabaseFileEncryption {
|
|||||||
fingerprint: "termix-v2-systemcrypto",
|
fingerprint: "termix-v2-systemcrypto",
|
||||||
algorithm: this.ALGORITHM,
|
algorithm: this.ALGORITHM,
|
||||||
keySource: "SystemCrypto",
|
keySource: "SystemCrypto",
|
||||||
|
dataSize: encrypted.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
const metadataPath = `${targetPath}${this.METADATA_FILE_SUFFIX}`;
|
fs.writeFileSync(tmpPath, encrypted);
|
||||||
fs.writeFileSync(targetPath, encrypted);
|
fs.writeFileSync(tmpMetadataPath, JSON.stringify(metadata, null, 2));
|
||||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
||||||
|
fs.renameSync(tmpPath, targetPath);
|
||||||
|
fs.renameSync(tmpMetadataPath, metadataPath);
|
||||||
|
|
||||||
return targetPath;
|
return targetPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(tmpPath)) {
|
||||||
|
fs.unlinkSync(tmpPath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(tmpMetadataPath)) {
|
||||||
|
fs.unlinkSync(tmpMetadataPath);
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
databaseLogger.warn("Failed to cleanup temporary files", {
|
||||||
|
operation: "temp_file_cleanup_failed",
|
||||||
|
tmpPath,
|
||||||
|
error:
|
||||||
|
cleanupError instanceof Error
|
||||||
|
? cleanupError.message
|
||||||
|
: "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
databaseLogger.error("Failed to encrypt database buffer", error, {
|
databaseLogger.error("Failed to encrypt database buffer", error, {
|
||||||
operation: "database_buffer_encryption_failed",
|
operation: "database_buffer_encryption_failed",
|
||||||
targetPath,
|
targetPath,
|
||||||
@@ -74,6 +106,8 @@ class DatabaseFileEncryption {
|
|||||||
const encryptedPath =
|
const encryptedPath =
|
||||||
targetPath || `${sourcePath}${this.ENCRYPTED_FILE_SUFFIX}`;
|
targetPath || `${sourcePath}${this.ENCRYPTED_FILE_SUFFIX}`;
|
||||||
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
|
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
|
||||||
|
const tmpPath = `${encryptedPath}.tmp-${Date.now()}-${process.pid}`;
|
||||||
|
const tmpMetadataPath = `${tmpPath}${this.METADATA_FILE_SUFFIX}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sourceData = fs.readFileSync(sourcePath);
|
const sourceData = fs.readFileSync(sourcePath);
|
||||||
@@ -93,6 +127,12 @@ class DatabaseFileEncryption {
|
|||||||
]);
|
]);
|
||||||
const tag = cipher.getAuthTag();
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
const keyFingerprint = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(key)
|
||||||
|
.digest("hex")
|
||||||
|
.substring(0, 16);
|
||||||
|
|
||||||
const metadata: EncryptedFileMetadata = {
|
const metadata: EncryptedFileMetadata = {
|
||||||
iv: iv.toString("hex"),
|
iv: iv.toString("hex"),
|
||||||
tag: tag.toString("hex"),
|
tag: tag.toString("hex"),
|
||||||
@@ -100,10 +140,14 @@ class DatabaseFileEncryption {
|
|||||||
fingerprint: "termix-v2-systemcrypto",
|
fingerprint: "termix-v2-systemcrypto",
|
||||||
algorithm: this.ALGORITHM,
|
algorithm: this.ALGORITHM,
|
||||||
keySource: "SystemCrypto",
|
keySource: "SystemCrypto",
|
||||||
|
dataSize: encrypted.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.writeFileSync(encryptedPath, encrypted);
|
fs.writeFileSync(tmpPath, encrypted);
|
||||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
fs.writeFileSync(tmpMetadataPath, JSON.stringify(metadata, null, 2));
|
||||||
|
|
||||||
|
fs.renameSync(tmpPath, encryptedPath);
|
||||||
|
fs.renameSync(tmpMetadataPath, metadataPath);
|
||||||
|
|
||||||
databaseLogger.info("Database file encrypted successfully", {
|
databaseLogger.info("Database file encrypted successfully", {
|
||||||
operation: "database_file_encryption",
|
operation: "database_file_encryption",
|
||||||
@@ -111,11 +155,30 @@ class DatabaseFileEncryption {
|
|||||||
encryptedPath,
|
encryptedPath,
|
||||||
fileSize: sourceData.length,
|
fileSize: sourceData.length,
|
||||||
encryptedSize: encrypted.length,
|
encryptedSize: encrypted.length,
|
||||||
|
keyFingerprint,
|
||||||
fingerprintPrefix: metadata.fingerprint,
|
fingerprintPrefix: metadata.fingerprint,
|
||||||
});
|
});
|
||||||
|
|
||||||
return encryptedPath;
|
return encryptedPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(tmpPath)) {
|
||||||
|
fs.unlinkSync(tmpPath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(tmpMetadataPath)) {
|
||||||
|
fs.unlinkSync(tmpMetadataPath);
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
databaseLogger.warn("Failed to cleanup temporary files", {
|
||||||
|
operation: "temp_file_cleanup_failed",
|
||||||
|
tmpPath,
|
||||||
|
error:
|
||||||
|
cleanupError instanceof Error
|
||||||
|
? cleanupError.message
|
||||||
|
: "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
databaseLogger.error("Failed to encrypt database file", error, {
|
databaseLogger.error("Failed to encrypt database file", error, {
|
||||||
operation: "database_file_encryption_failed",
|
operation: "database_file_encryption_failed",
|
||||||
sourcePath,
|
sourcePath,
|
||||||
@@ -140,11 +203,34 @@ class DatabaseFileEncryption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const dataFileStats = fs.statSync(encryptedPath);
|
||||||
|
const metaFileStats = fs.statSync(metadataPath);
|
||||||
|
|
||||||
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
||||||
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
||||||
|
|
||||||
const encryptedData = fs.readFileSync(encryptedPath);
|
const encryptedData = fs.readFileSync(encryptedPath);
|
||||||
|
|
||||||
|
if (metadata.dataSize !== undefined && encryptedData.length !== metadata.dataSize) {
|
||||||
|
databaseLogger.error(
|
||||||
|
"Encrypted file size mismatch - possible corrupted write or mismatched metadata",
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
operation: "database_file_size_mismatch",
|
||||||
|
encryptedPath,
|
||||||
|
actualSize: encryptedData.length,
|
||||||
|
expectedSize: metadata.dataSize,
|
||||||
|
difference: encryptedData.length - metadata.dataSize,
|
||||||
|
dataFileMtime: dataFileStats.mtime.toISOString(),
|
||||||
|
metaFileMtime: metaFileStats.mtime.toISOString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Encrypted file size mismatch: expected ${metadata.dataSize} bytes but got ${encryptedData.length} bytes. ` +
|
||||||
|
`This indicates corrupted files or interrupted write operation.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let key: Buffer;
|
let key: Buffer;
|
||||||
if (metadata.version === "v2") {
|
if (metadata.version === "v2") {
|
||||||
key = await this.systemCrypto.getDatabaseKey();
|
key = await this.systemCrypto.getDatabaseKey();
|
||||||
@@ -167,6 +253,12 @@ class DatabaseFileEncryption {
|
|||||||
throw new Error(`Unsupported encryption version: ${metadata.version}`);
|
throw new Error(`Unsupported encryption version: ${metadata.version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyFingerprint = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(key)
|
||||||
|
.digest("hex")
|
||||||
|
.substring(0, 16);
|
||||||
|
|
||||||
const decipher = crypto.createDecipheriv(
|
const decipher = crypto.createDecipheriv(
|
||||||
metadata.algorithm,
|
metadata.algorithm,
|
||||||
key,
|
key,
|
||||||
@@ -181,13 +273,64 @@ class DatabaseFileEncryption {
|
|||||||
|
|
||||||
return decryptedBuffer;
|
return decryptedBuffer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : "Unknown error";
|
||||||
|
const isAuthError =
|
||||||
|
errorMessage.includes("Unsupported state") ||
|
||||||
|
errorMessage.includes("authenticate data") ||
|
||||||
|
errorMessage.includes("auth");
|
||||||
|
|
||||||
|
if (isAuthError) {
|
||||||
|
const dataDir = process.env.DATA_DIR || "./db/data";
|
||||||
|
const envPath = path.join(dataDir, ".env");
|
||||||
|
|
||||||
|
let envFileExists = false;
|
||||||
|
let envFileReadable = false;
|
||||||
|
try {
|
||||||
|
envFileExists = fs.existsSync(envPath);
|
||||||
|
if (envFileExists) {
|
||||||
|
fs.accessSync(envPath, fs.constants.R_OK);
|
||||||
|
envFileReadable = true;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
databaseLogger.error(
|
||||||
|
"Database decryption authentication failed - possible causes: wrong DATABASE_KEY, corrupted files, or interrupted write",
|
||||||
|
error,
|
||||||
|
{
|
||||||
|
operation: "database_buffer_decryption_auth_failed",
|
||||||
|
encryptedPath,
|
||||||
|
metadataPath,
|
||||||
|
dataDir,
|
||||||
|
envPath,
|
||||||
|
envFileExists,
|
||||||
|
envFileReadable,
|
||||||
|
hasEnvKey: !!process.env.DATABASE_KEY,
|
||||||
|
envKeyLength: process.env.DATABASE_KEY?.length || 0,
|
||||||
|
suggestion:
|
||||||
|
"Check if DATABASE_KEY in .env matches the key used for encryption",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Database decryption authentication failed. This usually means:\n` +
|
||||||
|
`1. DATABASE_KEY has changed or is missing from ${dataDir}/.env\n` +
|
||||||
|
`2. Encrypted file was corrupted during write (system crash/restart)\n` +
|
||||||
|
`3. Metadata file does not match encrypted data\n` +
|
||||||
|
`\nDebug info:\n` +
|
||||||
|
`- DATA_DIR: ${dataDir}\n` +
|
||||||
|
`- .env file exists: ${envFileExists}\n` +
|
||||||
|
`- .env file readable: ${envFileReadable}\n` +
|
||||||
|
`- DATABASE_KEY in environment: ${!!process.env.DATABASE_KEY}\n` +
|
||||||
|
`Original error: ${errorMessage}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
databaseLogger.error("Failed to decrypt database to buffer", error, {
|
databaseLogger.error("Failed to decrypt database to buffer", error, {
|
||||||
operation: "database_buffer_decryption_failed",
|
operation: "database_buffer_decryption_failed",
|
||||||
encryptedPath,
|
encryptedPath,
|
||||||
|
errorMessage,
|
||||||
});
|
});
|
||||||
throw new Error(
|
throw new Error(`Database buffer decryption failed: ${errorMessage}`);
|
||||||
`Database buffer decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +358,23 @@ class DatabaseFileEncryption {
|
|||||||
|
|
||||||
const encryptedData = fs.readFileSync(encryptedPath);
|
const encryptedData = fs.readFileSync(encryptedPath);
|
||||||
|
|
||||||
|
if (metadata.dataSize !== undefined && encryptedData.length !== metadata.dataSize) {
|
||||||
|
databaseLogger.error(
|
||||||
|
"Encrypted file size mismatch - possible corrupted write or mismatched metadata",
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
operation: "database_file_size_mismatch",
|
||||||
|
encryptedPath,
|
||||||
|
actualSize: encryptedData.length,
|
||||||
|
expectedSize: metadata.dataSize,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Encrypted file size mismatch: expected ${metadata.dataSize} bytes but got ${encryptedData.length} bytes. ` +
|
||||||
|
`This indicates corrupted files or interrupted write operation.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let key: Buffer;
|
let key: Buffer;
|
||||||
if (metadata.version === "v2") {
|
if (metadata.version === "v2") {
|
||||||
key = await this.systemCrypto.getDatabaseKey();
|
key = await this.systemCrypto.getDatabaseKey();
|
||||||
@@ -322,6 +482,125 @@ class DatabaseFileEncryption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getDiagnosticInfo(encryptedPath: string): {
|
||||||
|
dataFile: {
|
||||||
|
exists: boolean;
|
||||||
|
size?: number;
|
||||||
|
mtime?: string;
|
||||||
|
readable?: boolean;
|
||||||
|
};
|
||||||
|
metadataFile: {
|
||||||
|
exists: boolean;
|
||||||
|
size?: number;
|
||||||
|
mtime?: string;
|
||||||
|
readable?: boolean;
|
||||||
|
content?: EncryptedFileMetadata;
|
||||||
|
};
|
||||||
|
environment: {
|
||||||
|
dataDir: string;
|
||||||
|
envPath: string;
|
||||||
|
envFileExists: boolean;
|
||||||
|
envFileReadable: boolean;
|
||||||
|
hasEnvKey: boolean;
|
||||||
|
envKeyLength: number;
|
||||||
|
};
|
||||||
|
validation: {
|
||||||
|
filesConsistent: boolean;
|
||||||
|
sizeMismatch?: boolean;
|
||||||
|
expectedSize?: number;
|
||||||
|
actualSize?: number;
|
||||||
|
};
|
||||||
|
} {
|
||||||
|
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
|
||||||
|
const dataDir = process.env.DATA_DIR || "./db/data";
|
||||||
|
const envPath = path.join(dataDir, ".env");
|
||||||
|
|
||||||
|
const result: ReturnType<typeof this.getDiagnosticInfo> = {
|
||||||
|
dataFile: { exists: false },
|
||||||
|
metadataFile: { exists: false },
|
||||||
|
environment: {
|
||||||
|
dataDir,
|
||||||
|
envPath,
|
||||||
|
envFileExists: false,
|
||||||
|
envFileReadable: false,
|
||||||
|
hasEnvKey: !!process.env.DATABASE_KEY,
|
||||||
|
envKeyLength: process.env.DATABASE_KEY?.length || 0,
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
filesConsistent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
result.dataFile.exists = fs.existsSync(encryptedPath);
|
||||||
|
if (result.dataFile.exists) {
|
||||||
|
try {
|
||||||
|
fs.accessSync(encryptedPath, fs.constants.R_OK);
|
||||||
|
result.dataFile.readable = true;
|
||||||
|
const stats = fs.statSync(encryptedPath);
|
||||||
|
result.dataFile.size = stats.size;
|
||||||
|
result.dataFile.mtime = stats.mtime.toISOString();
|
||||||
|
} catch {
|
||||||
|
result.dataFile.readable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.metadataFile.exists = fs.existsSync(metadataPath);
|
||||||
|
if (result.metadataFile.exists) {
|
||||||
|
try {
|
||||||
|
fs.accessSync(metadataPath, fs.constants.R_OK);
|
||||||
|
result.metadataFile.readable = true;
|
||||||
|
const stats = fs.statSync(metadataPath);
|
||||||
|
result.metadataFile.size = stats.size;
|
||||||
|
result.metadataFile.mtime = stats.mtime.toISOString();
|
||||||
|
|
||||||
|
const content = fs.readFileSync(metadataPath, "utf8");
|
||||||
|
result.metadataFile.content = JSON.parse(content);
|
||||||
|
} catch {
|
||||||
|
result.metadataFile.readable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.environment.envFileExists = fs.existsSync(envPath);
|
||||||
|
if (result.environment.envFileExists) {
|
||||||
|
try {
|
||||||
|
fs.accessSync(envPath, fs.constants.R_OK);
|
||||||
|
result.environment.envFileReadable = true;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.dataFile.exists &&
|
||||||
|
result.metadataFile.exists &&
|
||||||
|
result.metadataFile.content
|
||||||
|
) {
|
||||||
|
result.validation.filesConsistent = true;
|
||||||
|
|
||||||
|
if (result.metadataFile.content.dataSize !== undefined) {
|
||||||
|
result.validation.expectedSize = result.metadataFile.content.dataSize;
|
||||||
|
result.validation.actualSize = result.dataFile.size;
|
||||||
|
result.validation.sizeMismatch =
|
||||||
|
result.metadataFile.content.dataSize !== result.dataFile.size;
|
||||||
|
if (result.validation.sizeMismatch) {
|
||||||
|
result.validation.filesConsistent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
databaseLogger.error("Failed to generate diagnostic info", error, {
|
||||||
|
operation: "diagnostic_info_failed",
|
||||||
|
encryptedPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseLogger.info("Database encryption diagnostic info", {
|
||||||
|
operation: "diagnostic_info_generated",
|
||||||
|
...result,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static async createEncryptedBackup(
|
static async createEncryptedBackup(
|
||||||
databasePath: string,
|
databasePath: string,
|
||||||
backupDir: string,
|
backupDir: string,
|
||||||
|
|||||||
@@ -51,17 +51,8 @@ class SystemCrypto {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (fileError) {
|
} 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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseLogger.warn("Generating new JWT secret", {
|
|
||||||
operation: "jwt_generating_new_secret",
|
|
||||||
});
|
|
||||||
await this.generateAndGuideUser();
|
await this.generateAndGuideUser();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
databaseLogger.error("Failed to initialize JWT secret", error, {
|
databaseLogger.error("Failed to initialize JWT secret", error, {
|
||||||
@@ -80,29 +71,44 @@ class SystemCrypto {
|
|||||||
|
|
||||||
async initializeDatabaseKey(): Promise<void> {
|
async initializeDatabaseKey(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const dataDir = process.env.DATA_DIR || "./db/data";
|
||||||
|
const envPath = path.join(dataDir, ".env");
|
||||||
|
|
||||||
const envKey = process.env.DATABASE_KEY;
|
const envKey = process.env.DATABASE_KEY;
|
||||||
if (envKey && envKey.length >= 64) {
|
if (envKey && envKey.length >= 64) {
|
||||||
this.databaseKey = Buffer.from(envKey, "hex");
|
this.databaseKey = Buffer.from(envKey, "hex");
|
||||||
|
const keyFingerprint = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(this.databaseKey)
|
||||||
|
.digest("hex")
|
||||||
|
.substring(0, 16);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataDir = process.env.DATA_DIR || "./db/data";
|
|
||||||
const envPath = path.join(dataDir, ".env");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const envContent = await fs.readFile(envPath, "utf8");
|
const envContent = await fs.readFile(envPath, "utf8");
|
||||||
const dbKeyMatch = envContent.match(/^DATABASE_KEY=(.+)$/m);
|
const dbKeyMatch = envContent.match(/^DATABASE_KEY=(.+)$/m);
|
||||||
if (dbKeyMatch && dbKeyMatch[1] && dbKeyMatch[1].length >= 64) {
|
if (dbKeyMatch && dbKeyMatch[1] && dbKeyMatch[1].length >= 64) {
|
||||||
this.databaseKey = Buffer.from(dbKeyMatch[1], "hex");
|
this.databaseKey = Buffer.from(dbKeyMatch[1], "hex");
|
||||||
process.env.DATABASE_KEY = dbKeyMatch[1];
|
process.env.DATABASE_KEY = dbKeyMatch[1];
|
||||||
|
|
||||||
|
const keyFingerprint = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(this.databaseKey)
|
||||||
|
.digest("hex")
|
||||||
|
.substring(0, 16);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch (fileError) {}
|
||||||
|
|
||||||
await this.generateAndGuideDatabaseKey();
|
await this.generateAndGuideDatabaseKey();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
databaseLogger.error("Failed to initialize database key", error, {
|
databaseLogger.error("Failed to initialize database key", error, {
|
||||||
operation: "db_key_init_failed",
|
operation: "db_key_init_failed",
|
||||||
|
dataDir: process.env.DATA_DIR || "./db/data",
|
||||||
});
|
});
|
||||||
throw new Error("Database key initialization failed");
|
throw new Error("Database key initialization failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,20 @@ import {
|
|||||||
verifyTOTPLogin,
|
verifyTOTPLogin,
|
||||||
getServerConfig,
|
getServerConfig,
|
||||||
isElectron,
|
isElectron,
|
||||||
logoutUser,
|
|
||||||
} from "../../main-axios.ts";
|
} from "../../main-axios.ts";
|
||||||
import { ElectronServerConfig as ServerConfigComponent } from "@/ui/desktop/authentication/ElectronServerConfig.tsx";
|
import { ElectronServerConfig as ServerConfigComponent } from "@/ui/desktop/authentication/ElectronServerConfig.tsx";
|
||||||
import { ElectronLoginForm } from "@/ui/desktop/authentication/ElectronLoginForm.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"> {
|
interface AuthProps extends React.ComponentProps<"div"> {
|
||||||
setLoggedIn: (loggedIn: boolean) => void;
|
setLoggedIn: (loggedIn: boolean) => void;
|
||||||
setIsAdmin: (isAdmin: boolean) => void;
|
setIsAdmin: (isAdmin: boolean) => void;
|
||||||
@@ -37,7 +46,6 @@ interface AuthProps extends React.ComponentProps<"div"> {
|
|||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
authLoading: boolean;
|
authLoading: boolean;
|
||||||
setDbError: (error: string | null) => void;
|
setDbError: (error: string | null) => void;
|
||||||
dbError?: string | null;
|
|
||||||
onAuthSuccess: (authData: {
|
onAuthSuccess: (authData: {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
@@ -54,21 +62,20 @@ export function Auth({
|
|||||||
loggedIn,
|
loggedIn,
|
||||||
authLoading,
|
authLoading,
|
||||||
setDbError,
|
setDbError,
|
||||||
dbError,
|
|
||||||
onAuthSuccess,
|
onAuthSuccess,
|
||||||
...props
|
...props
|
||||||
}: AuthProps) {
|
}: AuthProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isInElectronWebView = () => {
|
const isInElectronWebView = () => {
|
||||||
if ((window as any).IS_ELECTRON_WEBVIEW) {
|
if ((window as ExtendedWindow).IS_ELECTRON_WEBVIEW) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (window.self !== window.top) {
|
if (window.self !== window.top) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -126,7 +133,7 @@ export function Auth({
|
|||||||
userId: meRes.userId || null,
|
userId: meRes.userId || null,
|
||||||
});
|
});
|
||||||
toast.success(t("messages.loginSuccess"));
|
toast.success(t("messages.loginSuccess"));
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
toast.error(t("errors.failedUserInfo"));
|
toast.error(t("errors.failedUserInfo"));
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -206,7 +213,7 @@ export function Auth({
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setDbHealthChecking(false);
|
setDbHealthChecking(false);
|
||||||
});
|
});
|
||||||
}, [setDbError, firstUserToastShown, showServerConfig]);
|
}, [setDbError, firstUserToastShown, showServerConfig, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!registrationAllowed && !internalLoggedIn) {
|
if (!registrationAllowed && !internalLoggedIn) {
|
||||||
@@ -282,9 +289,9 @@ export function Auth({
|
|||||||
);
|
);
|
||||||
setWebviewAuthSuccess(true);
|
setWebviewAuthSuccess(true);
|
||||||
setTimeout(() => window.location.reload(), 100);
|
setTimeout(() => window.location.reload(), 100);
|
||||||
setLoading(false);
|
} catch (e) {
|
||||||
return;
|
console.error("Error posting auth success message:", e);
|
||||||
} catch (e) {}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [meRes] = await Promise.all([getUserInfo()]);
|
const [meRes] = await Promise.all([getUserInfo()]);
|
||||||
@@ -461,7 +468,9 @@ export function Auth({
|
|||||||
setTimeout(() => window.location.reload(), 100);
|
setTimeout(() => window.location.reload(), 100);
|
||||||
setTotpLoading(false);
|
setTotpLoading(false);
|
||||||
return;
|
return;
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
console.error("Error posting auth success message:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInternalLoggedIn(true);
|
setInternalLoggedIn(true);
|
||||||
@@ -569,7 +578,9 @@ export function Auth({
|
|||||||
setTimeout(() => window.location.reload(), 100);
|
setTimeout(() => window.location.reload(), 100);
|
||||||
setOidcLoading(false);
|
setOidcLoading(false);
|
||||||
return;
|
return;
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
console.error("Error posting auth success message:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,7 +618,16 @@ export function Auth({
|
|||||||
setOidcLoading(false);
|
setOidcLoading(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [
|
||||||
|
onAuthSuccess,
|
||||||
|
setDbError,
|
||||||
|
setIsAdmin,
|
||||||
|
setLoggedIn,
|
||||||
|
setUserId,
|
||||||
|
setUsername,
|
||||||
|
t,
|
||||||
|
isInElectronWebView,
|
||||||
|
]);
|
||||||
|
|
||||||
const Spinner = (
|
const Spinner = (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
Reference in New Issue
Block a user