Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions migrations/20260209061552_encrypt_auth_tokens_with_iv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
const crypto = require("crypto");

This comment was marked as outdated.


// 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);
});
};
22 changes: 17 additions & 5 deletions src/server/api/lib/crypto.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
};
Expand Down
Loading