diff --git a/migrations/20260209061552_encrypt_auth_tokens_with_iv.js b/migrations/20260209061552_encrypt_auth_tokens_with_iv.js new file mode 100644 index 000000000..81203a3b2 --- /dev/null +++ b/migrations/20260209061552_encrypt_auth_tokens_with_iv.js @@ -0,0 +1,98 @@ +const crypto = require("crypto"); + +// Defaults from config +const algorithm = "aes256"; +const inputEncoding = "utf8"; +const outputEncoding = "hex"; + +// Legacy decryption using createDecipher (no IV) +function legacyDecrypt(encrypted, secret) { + const decipher = crypto.createDecipher(algorithm, secret); + let decrypted = decipher.update(encrypted, outputEncoding, inputEncoding); + decrypted += decipher.final(inputEncoding); + return decrypted; +} + +// Legacy encryption using createCipher (no IV) +function legacyEncrypt(value, secret) { + const cipher = crypto.createCipher(algorithm, secret); + let encrypted = cipher.update(value, inputEncoding, outputEncoding); + encrypted += cipher.final(outputEncoding); + return encrypted; +} + +// New encryption using createCipheriv (with IV) +function encryptWithIv(value, secret) { + const key = crypto.createHash("sha256").update(secret).digest(); + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, key, iv); + + let encrypted = cipher.update(value, inputEncoding, outputEncoding); + encrypted += cipher.final(outputEncoding); + return `V2:${iv.toString(outputEncoding)}:${encrypted}`; +} + +// New decryption using createDecipheriv (with IV) +function decryptWithIv(encrypted, secret) { + const key = crypto.createHash("sha256").update(secret).digest(); + const parts = encrypted.split(":"); + const iv = Buffer.from(parts[1], outputEncoding); + const encryptedData = parts[2]; + + const decipher = crypto.createDecipheriv(algorithm, key, iv); + let decrypted = decipher.update(encryptedData, outputEncoding, inputEncoding); + decrypted += decipher.final(inputEncoding); + return decrypted; +} + +exports.up = function up(knex) { + const sessionSecret = process.env.SESSION_SECRET; + if (!sessionSecret) { + throw new Error("SESSION_SECRET must be set to run this migration"); + } + + return knex("messaging_service") + .select("messaging_service_sid", "encrypted_auth_token") + .whereNot("encrypted_auth_token", "") + .then((rows) => { + const updates = rows + .filter((row) => !row.encrypted_auth_token.startsWith("V2:")) + .map((row) => { + const decrypted = legacyDecrypt( + row.encrypted_auth_token, + sessionSecret + ); + const reEncrypted = encryptWithIv(decrypted, sessionSecret); + return knex("messaging_service") + .where("messaging_service_sid", row.messaging_service_sid) + .update({ encrypted_auth_token: reEncrypted }); + }); + return Promise.all(updates); + }); +}; + +exports.down = function down(knex) { + const sessionSecret = process.env.SESSION_SECRET; + if (!sessionSecret) { + throw new Error("SESSION_SECRET must be set to run this migration"); + } + + return knex("messaging_service") + .select("messaging_service_sid", "encrypted_auth_token") + .whereNot("encrypted_auth_token", "") + .then((rows) => { + const updates = rows + .filter((row) => row.encrypted_auth_token.startsWith("V2:")) + .map((row) => { + const decrypted = decryptWithIv( + row.encrypted_auth_token, + sessionSecret + ); + const reEncrypted = legacyEncrypt(decrypted, sessionSecret); + return knex("messaging_service") + .where("messaging_service_sid", row.messaging_service_sid) + .update({ encrypted_auth_token: reEncrypted }); + }); + return Promise.all(updates); + }); +}; diff --git a/src/server/api/lib/crypto.js b/src/server/api/lib/crypto.js index de7ccdda2..e54cbd623 100644 --- a/src/server/api/lib/crypto.js +++ b/src/server/api/lib/crypto.js @@ -1,7 +1,7 @@ const { config } = require("../../../config"); const crypto = require("crypto"); -const key = config.SESSION_SECRET; +const key = crypto.createHash("sha256").update(config.SESSION_SECRET).digest(); const algorithm = config.ENCRYPTION_ALGORITHM; const inputEncoding = config.ENCRYPTION_INPUT_ENCODING; const outputEncoding = config.ENCRYPTION_OUTPUT_ENCODING; @@ -13,15 +13,27 @@ if (!key) { } const symmetricEncrypt = (value) => { - const cipher = crypto.createCipher(algorithm, key); + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, key, iv); let encrypted = cipher.update(value, inputEncoding, outputEncoding); encrypted += cipher.final(outputEncoding); - return encrypted; + + // Prepend IV to encrypted data (IV is not secret) and prefix with V2 + return `V2:${iv.toString(outputEncoding)}:${encrypted}`; }; const symmetricDecrypt = (encrypted) => { - const decipher = crypto.createDecipher(algorithm, key); - let decrypted = decipher.update(encrypted, outputEncoding, inputEncoding); + // parts are V2, iv, and encrypted string + const parts = encrypted.split(":"); + if (parts.length !== 3) { + throw new Error("Invalid encrypted data format"); + } + + const iv = Buffer.from(parts[1], outputEncoding); + const encryptedData = parts[2]; + + const decipher = crypto.createDecipheriv(algorithm, key, iv); + let decrypted = decipher.update(encryptedData, outputEncoding, inputEncoding); decrypted += decipher.final(inputEncoding); return decrypted; };