From 4ae2e6486f70e35a08512b11a28002d033091d75 Mon Sep 17 00:00:00 2001 From: Aashish John Date: Tue, 10 Feb 2026 14:35:53 -0500 Subject: [PATCH 1/3] chore(crypto): convert encrypted tokens to node v24 supported format Co-Authored-By: Claude Opus 4.5 --- ...60209061552_encrypt_auth_tokens_with_iv.js | 95 +++++++++++++++++++ src/server/api/lib/crypto.js | 21 +++- 2 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 migrations/20260209061552_encrypt_auth_tokens_with_iv.js 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..3ec8a34a7 --- /dev/null +++ b/migrations/20260209061552_encrypt_auth_tokens_with_iv.js @@ -0,0 +1,95 @@ +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 `${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[0], outputEncoding); + const encryptedData = parts[1]; + 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") + .then((rows) => { + const updates = rows + .filter((row) => !row.encrypted_auth_token.includes(":")) + .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.includes(":")) + .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..7f8d8f6a7 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,26 @@ 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) + return `${iv.toString(outputEncoding)}:${encrypted}`; }; const symmetricDecrypt = (encrypted) => { - const decipher = crypto.createDecipher(algorithm, key); - let decrypted = decipher.update(encrypted, outputEncoding, inputEncoding); + const parts = encrypted.split(":"); + if (parts.length !== 2) { + throw new Error("Invalid encrypted data format"); + } + + const iv = Buffer.from(parts[0], outputEncoding); + const encryptedData = parts[1]; + + const decipher = crypto.createDecipheriv(algorithm, key, iv); + let decrypted = decipher.update(encryptedData, outputEncoding, inputEncoding); decrypted += decipher.final(inputEncoding); return decrypted; }; From 6aa95ecdc6a2446cad51f20f46f40352335b8b79 Mon Sep 17 00:00:00 2001 From: Aashish John Date: Tue, 10 Feb 2026 15:45:47 -0500 Subject: [PATCH 2/3] chore: check for empty token value --- migrations/20260209061552_encrypt_auth_tokens_with_iv.js | 1 + 1 file changed, 1 insertion(+) diff --git a/migrations/20260209061552_encrypt_auth_tokens_with_iv.js b/migrations/20260209061552_encrypt_auth_tokens_with_iv.js index 3ec8a34a7..d8172c01b 100644 --- a/migrations/20260209061552_encrypt_auth_tokens_with_iv.js +++ b/migrations/20260209061552_encrypt_auth_tokens_with_iv.js @@ -51,6 +51,7 @@ exports.up = function up(knex) { 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.includes(":")) From e4badb2cdfab617add2e3b81e1299a4659079cfd Mon Sep 17 00:00:00 2001 From: Aashish John Date: Tue, 3 Mar 2026 23:58:22 -0500 Subject: [PATCH 3/3] chore: add v2 prefix --- .../20260209061552_encrypt_auth_tokens_with_iv.js | 12 +++++++----- src/server/api/lib/crypto.js | 11 ++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/migrations/20260209061552_encrypt_auth_tokens_with_iv.js b/migrations/20260209061552_encrypt_auth_tokens_with_iv.js index d8172c01b..81203a3b2 100644 --- a/migrations/20260209061552_encrypt_auth_tokens_with_iv.js +++ b/migrations/20260209061552_encrypt_auth_tokens_with_iv.js @@ -26,17 +26,19 @@ 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 `${iv.toString(outputEncoding)}:${encrypted}`; + 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[0], outputEncoding); - const encryptedData = parts[1]; + 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); @@ -54,7 +56,7 @@ exports.up = function up(knex) { .whereNot("encrypted_auth_token", "") .then((rows) => { const updates = rows - .filter((row) => !row.encrypted_auth_token.includes(":")) + .filter((row) => !row.encrypted_auth_token.startsWith("V2:")) .map((row) => { const decrypted = legacyDecrypt( row.encrypted_auth_token, @@ -80,7 +82,7 @@ exports.down = function down(knex) { .whereNot("encrypted_auth_token", "") .then((rows) => { const updates = rows - .filter((row) => row.encrypted_auth_token.includes(":")) + .filter((row) => row.encrypted_auth_token.startsWith("V2:")) .map((row) => { const decrypted = decryptWithIv( row.encrypted_auth_token, diff --git a/src/server/api/lib/crypto.js b/src/server/api/lib/crypto.js index 7f8d8f6a7..e54cbd623 100644 --- a/src/server/api/lib/crypto.js +++ b/src/server/api/lib/crypto.js @@ -18,18 +18,19 @@ const symmetricEncrypt = (value) => { let encrypted = cipher.update(value, inputEncoding, outputEncoding); encrypted += cipher.final(outputEncoding); - // Prepend IV to encrypted data (IV is not secret) - return `${iv.toString(outputEncoding)}:${encrypted}`; + // Prepend IV to encrypted data (IV is not secret) and prefix with V2 + return `V2:${iv.toString(outputEncoding)}:${encrypted}`; }; const symmetricDecrypt = (encrypted) => { + // parts are V2, iv, and encrypted string const parts = encrypted.split(":"); - if (parts.length !== 2) { + if (parts.length !== 3) { throw new Error("Invalid encrypted data format"); } - const iv = Buffer.from(parts[0], outputEncoding); - const encryptedData = parts[1]; + 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);