fix: Merge metadata and DB into 1 file
This commit is contained in:
@@ -27,14 +27,11 @@ class DatabaseFileEncryption {
|
|||||||
targetPath: string,
|
targetPath: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const tmpPath = `${targetPath}.tmp-${Date.now()}-${process.pid}`;
|
const tmpPath = `${targetPath}.tmp-${Date.now()}-${process.pid}`;
|
||||||
const tmpMetadataPath = `${tmpPath}${this.METADATA_FILE_SUFFIX}`;
|
|
||||||
const metadataPath = `${targetPath}${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();
|
||||||
|
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
|
|
||||||
const cipher = crypto.createCipheriv(
|
const cipher = crypto.createCipheriv(
|
||||||
this.ALGORITHM,
|
this.ALGORITHM,
|
||||||
key,
|
key,
|
||||||
@@ -43,12 +40,6 @@ 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"),
|
||||||
@@ -59,11 +50,34 @@ class DatabaseFileEncryption {
|
|||||||
dataSize: encrypted.length,
|
dataSize: encrypted.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.writeFileSync(tmpPath, encrypted);
|
const metadataJson = JSON.stringify(metadata, null, 2);
|
||||||
fs.writeFileSync(tmpMetadataPath, JSON.stringify(metadata, null, 2));
|
const metadataBuffer = Buffer.from(metadataJson, "utf8");
|
||||||
|
const metadataLengthBuffer = Buffer.alloc(4);
|
||||||
|
metadataLengthBuffer.writeUInt32BE(metadataBuffer.length, 0);
|
||||||
|
|
||||||
|
const finalBuffer = Buffer.concat([
|
||||||
|
metadataLengthBuffer,
|
||||||
|
metadataBuffer,
|
||||||
|
encrypted,
|
||||||
|
]);
|
||||||
|
|
||||||
|
fs.writeFileSync(tmpPath, finalBuffer);
|
||||||
fs.renameSync(tmpPath, targetPath);
|
fs.renameSync(tmpPath, targetPath);
|
||||||
fs.renameSync(tmpMetadataPath, metadataPath);
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(metadataPath)) {
|
||||||
|
fs.unlinkSync(metadataPath);
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
databaseLogger.warn("Failed to cleanup old metadata file", {
|
||||||
|
operation: "old_meta_cleanup_failed",
|
||||||
|
path: metadataPath,
|
||||||
|
error:
|
||||||
|
cleanupError instanceof Error
|
||||||
|
? cleanupError.message
|
||||||
|
: "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return targetPath;
|
return targetPath;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -71,9 +85,6 @@ class DatabaseFileEncryption {
|
|||||||
if (fs.existsSync(tmpPath)) {
|
if (fs.existsSync(tmpPath)) {
|
||||||
fs.unlinkSync(tmpPath);
|
fs.unlinkSync(tmpPath);
|
||||||
}
|
}
|
||||||
if (fs.existsSync(tmpMetadataPath)) {
|
|
||||||
fs.unlinkSync(tmpMetadataPath);
|
|
||||||
}
|
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
databaseLogger.warn("Failed to cleanup temporary files", {
|
databaseLogger.warn("Failed to cleanup temporary files", {
|
||||||
operation: "temp_file_cleanup_failed",
|
operation: "temp_file_cleanup_failed",
|
||||||
@@ -197,21 +208,54 @@ class DatabaseFileEncryption {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
|
let metadata: EncryptedFileMetadata;
|
||||||
if (!fs.existsSync(metadataPath)) {
|
let encryptedData: Buffer;
|
||||||
throw new Error(`Metadata file does not exist: ${metadataPath}`);
|
|
||||||
|
const fileBuffer = fs.readFileSync(encryptedPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const metadataLength = fileBuffer.readUInt32BE(0);
|
||||||
|
const metadataEnd = 4 + metadataLength;
|
||||||
|
|
||||||
|
if (
|
||||||
|
metadataLength <= 0 ||
|
||||||
|
metadataEnd > fileBuffer.length ||
|
||||||
|
metadataEnd <= 4
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid metadata length in single-file format");
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataJson = fileBuffer.slice(4, metadataEnd).toString("utf8");
|
||||||
|
metadata = JSON.parse(metadataJson);
|
||||||
|
encryptedData = fileBuffer.slice(metadataEnd);
|
||||||
|
|
||||||
|
if (!metadata.iv || !metadata.tag || !metadata.version) {
|
||||||
|
throw new Error("Invalid metadata structure in single-file format");
|
||||||
|
}
|
||||||
|
} catch (singleFileError) {
|
||||||
|
const metadataPath = `${encryptedPath}${this.METADATA_FILE_SUFFIX}`;
|
||||||
|
if (!fs.existsSync(metadataPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not read database: Not a valid single-file format and metadata file is missing: ${metadataPath}. Error: ${singleFileError.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
||||||
|
metadata = JSON.parse(metadataContent);
|
||||||
|
encryptedData = fileBuffer;
|
||||||
|
} catch (twoFileError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to read database using both single-file and two-file formats. Error: ${twoFileError.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dataFileStats = fs.statSync(encryptedPath);
|
if (
|
||||||
const metaFileStats = fs.statSync(metadataPath);
|
metadata.dataSize !== undefined &&
|
||||||
|
encryptedData.length !== metadata.dataSize
|
||||||
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
) {
|
||||||
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
|
||||||
|
|
||||||
const encryptedData = fs.readFileSync(encryptedPath);
|
|
||||||
|
|
||||||
if (metadata.dataSize !== undefined && encryptedData.length !== metadata.dataSize) {
|
|
||||||
databaseLogger.error(
|
databaseLogger.error(
|
||||||
"Encrypted file size mismatch - possible corrupted write or mismatched metadata",
|
"Encrypted file size mismatch - possible corrupted write or mismatched metadata",
|
||||||
null,
|
null,
|
||||||
@@ -220,9 +264,6 @@ class DatabaseFileEncryption {
|
|||||||
encryptedPath,
|
encryptedPath,
|
||||||
actualSize: encryptedData.length,
|
actualSize: encryptedData.length,
|
||||||
expectedSize: metadata.dataSize,
|
expectedSize: metadata.dataSize,
|
||||||
difference: encryptedData.length - metadata.dataSize,
|
|
||||||
dataFileMtime: dataFileStats.mtime.toISOString(),
|
|
||||||
metaFileMtime: metaFileStats.mtime.toISOString(),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -253,12 +294,6 @@ 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,
|
||||||
@@ -300,7 +335,6 @@ class DatabaseFileEncryption {
|
|||||||
{
|
{
|
||||||
operation: "database_buffer_decryption_auth_failed",
|
operation: "database_buffer_decryption_auth_failed",
|
||||||
encryptedPath,
|
encryptedPath,
|
||||||
metadataPath,
|
|
||||||
dataDir,
|
dataDir,
|
||||||
envPath,
|
envPath,
|
||||||
envFileExists,
|
envFileExists,
|
||||||
@@ -358,7 +392,10 @@ class DatabaseFileEncryption {
|
|||||||
|
|
||||||
const encryptedData = fs.readFileSync(encryptedPath);
|
const encryptedData = fs.readFileSync(encryptedPath);
|
||||||
|
|
||||||
if (metadata.dataSize !== undefined && encryptedData.length !== metadata.dataSize) {
|
if (
|
||||||
|
metadata.dataSize !== undefined &&
|
||||||
|
encryptedData.length !== metadata.dataSize
|
||||||
|
) {
|
||||||
databaseLogger.error(
|
databaseLogger.error(
|
||||||
"Encrypted file size mismatch - possible corrupted write or mismatched metadata",
|
"Encrypted file size mismatch - possible corrupted write or mismatched metadata",
|
||||||
null,
|
null,
|
||||||
@@ -434,18 +471,43 @@ class DatabaseFileEncryption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static isEncryptedDatabaseFile(filePath: string): boolean {
|
static isEncryptedDatabaseFile(filePath: string): boolean {
|
||||||
const metadataPath = `${filePath}${this.METADATA_FILE_SUFFIX}`;
|
if (!fs.existsSync(filePath)) {
|
||||||
|
|
||||||
if (!fs.existsSync(filePath) || !fs.existsSync(metadataPath)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metadataPath = `${filePath}${this.METADATA_FILE_SUFFIX}`;
|
||||||
|
if (fs.existsSync(metadataPath)) {
|
||||||
|
try {
|
||||||
|
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
||||||
|
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
||||||
|
return (
|
||||||
|
metadata.version === this.VERSION &&
|
||||||
|
metadata.algorithm === this.ALGORITHM
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metadataContent = fs.readFileSync(metadataPath, "utf8");
|
const fileBuffer = fs.readFileSync(filePath);
|
||||||
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
|
if (fileBuffer.length < 4) return false;
|
||||||
|
|
||||||
|
const metadataLength = fileBuffer.readUInt32BE(0);
|
||||||
|
const metadataEnd = 4 + metadataLength;
|
||||||
|
|
||||||
|
if (metadataLength <= 0 || metadataEnd > fileBuffer.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataJson = fileBuffer.slice(4, metadataEnd).toString("utf8");
|
||||||
|
const metadata: EncryptedFileMetadata = JSON.parse(metadataJson);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
metadata.version === this.VERSION &&
|
metadata.version === this.VERSION &&
|
||||||
metadata.algorithm === this.ALGORITHM
|
metadata.algorithm === this.ALGORITHM &&
|
||||||
|
!!metadata.iv &&
|
||||||
|
!!metadata.tag
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user