This commit was merged in pull request #335.
This commit is contained in:
Karmaa
2025-10-03 00:02:10 -05:00
committed by GitHub
parent a7fa40393d
commit 937e04fa5c
26 changed files with 877 additions and 186 deletions

View File

@@ -2,6 +2,12 @@ import { FieldCrypto } from "./field-crypto.js";
import { databaseLogger } from "./logger.js";
export class LazyFieldEncryption {
private static readonly LEGACY_FIELD_NAME_MAP: Record<string, string> = {
key_password: "keyPassword",
private_key: "privateKey",
public_key: "publicKey",
};
static isPlaintextField(value: string): boolean {
if (!value) return false;
@@ -44,6 +50,35 @@ export class LazyFieldEncryption {
);
return decrypted;
} catch (error) {
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
if (legacyFieldName) {
try {
const decrypted = FieldCrypto.decryptField(
fieldValue,
userKEK,
recordId,
legacyFieldName,
);
return decrypted;
} catch (legacyError) {}
}
const sensitiveFields = [
"totp_secret",
"totp_backup_codes",
"password",
"key",
"key_password",
"private_key",
"public_key",
"client_secret",
"oidc_identifier",
];
if (sensitiveFields.includes(fieldName)) {
return "";
}
databaseLogger.error("Failed to decrypt field", error, {
operation: "lazy_encryption_decrypt_failed",
recordId,
@@ -60,9 +95,13 @@ export class LazyFieldEncryption {
userKEK: Buffer,
recordId: string,
fieldName: string,
): { encrypted: string; wasPlaintext: boolean } {
): {
encrypted: string;
wasPlaintext: boolean;
wasLegacyEncryption: boolean;
} {
if (!fieldValue) {
return { encrypted: "", wasPlaintext: false };
return { encrypted: "", wasPlaintext: false, wasLegacyEncryption: false };
}
if (this.isPlaintextField(fieldValue)) {
@@ -74,7 +113,7 @@ export class LazyFieldEncryption {
fieldName,
);
return { encrypted, wasPlaintext: true };
return { encrypted, wasPlaintext: true, wasLegacyEncryption: false };
} catch (error) {
databaseLogger.error("Failed to encrypt plaintext field", error, {
operation: "lazy_encryption_migrate_failed",
@@ -85,7 +124,42 @@ export class LazyFieldEncryption {
throw error;
}
} else {
return { encrypted: fieldValue, wasPlaintext: false };
try {
FieldCrypto.decryptField(fieldValue, userKEK, recordId, fieldName);
return {
encrypted: fieldValue,
wasPlaintext: false,
wasLegacyEncryption: false,
};
} catch (error) {
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
if (legacyFieldName) {
try {
const decrypted = FieldCrypto.decryptField(
fieldValue,
userKEK,
recordId,
legacyFieldName,
);
const reencrypted = FieldCrypto.encryptField(
decrypted,
userKEK,
recordId,
fieldName,
);
return {
encrypted: reencrypted,
wasPlaintext: false,
wasLegacyEncryption: true,
};
} catch (legacyError) {}
}
return {
encrypted: fieldValue,
wasPlaintext: false,
wasLegacyEncryption: false,
};
}
}
}
@@ -106,18 +180,21 @@ export class LazyFieldEncryption {
for (const fieldName of sensitiveFields) {
const fieldValue = record[fieldName];
if (fieldValue && this.isPlaintextField(fieldValue)) {
if (fieldValue) {
try {
const { encrypted } = this.migrateFieldToEncrypted(
fieldValue,
userKEK,
recordId,
fieldName,
);
const { encrypted, wasPlaintext, wasLegacyEncryption } =
this.migrateFieldToEncrypted(
fieldValue,
userKEK,
recordId,
fieldName,
);
updatedRecord[fieldName] = encrypted;
migratedFields.push(fieldName);
needsUpdate = true;
if (wasPlaintext || wasLegacyEncryption) {
updatedRecord[fieldName] = encrypted;
migratedFields.push(fieldName);
needsUpdate = true;
}
} catch (error) {
databaseLogger.error("Failed to migrate record field", error, {
operation: "lazy_encryption_record_field_failed",
@@ -134,13 +211,53 @@ export class LazyFieldEncryption {
static getSensitiveFieldsForTable(tableName: string): string[] {
const sensitiveFieldsMap: Record<string, string[]> = {
ssh_data: ["password", "key", "key_password"],
ssh_credentials: ["password", "key", "key_password", "private_key"],
ssh_credentials: [
"password",
"key",
"key_password",
"private_key",
"public_key",
],
users: ["totp_secret", "totp_backup_codes"],
};
return sensitiveFieldsMap[tableName] || [];
}
static fieldNeedsMigration(
fieldValue: string,
userKEK: Buffer,
recordId: string,
fieldName: string,
): boolean {
if (!fieldValue) return false;
if (this.isPlaintextField(fieldValue)) {
return true;
}
try {
FieldCrypto.decryptField(fieldValue, userKEK, recordId, fieldName);
return false;
} catch (error) {
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
if (legacyFieldName) {
try {
FieldCrypto.decryptField(
fieldValue,
userKEK,
recordId,
legacyFieldName,
);
return true;
} catch (legacyError) {
return false;
}
}
return false;
}
}
static async checkUserNeedsMigration(
userId: string,
userKEK: Buffer,
@@ -169,7 +286,15 @@ export class LazyFieldEncryption {
const hostPlaintextFields: string[] = [];
for (const field of sensitiveFields) {
if (host[field] && this.isPlaintextField(host[field])) {
if (
host[field] &&
this.fieldNeedsMigration(
host[field],
userKEK,
host.id.toString(),
field,
)
) {
hostPlaintextFields.push(field);
needsMigration = true;
}
@@ -193,7 +318,15 @@ export class LazyFieldEncryption {
const credentialPlaintextFields: string[] = [];
for (const field of sensitiveFields) {
if (credential[field] && this.isPlaintextField(credential[field])) {
if (
credential[field] &&
this.fieldNeedsMigration(
credential[field],
userKEK,
credential.id.toString(),
field,
)
) {
credentialPlaintextFields.push(field);
needsMigration = true;
}
@@ -214,7 +347,10 @@ export class LazyFieldEncryption {
const userPlaintextFields: string[] = [];
for (const field of sensitiveFields) {
if (user[field] && this.isPlaintextField(user[field])) {
if (
user[field] &&
this.fieldNeedsMigration(user[field], userKEK, userId, field)
) {
userPlaintextFields.push(field);
needsMigration = true;
}