Update README and configuration to replace RSA with Ed25519 for federation security
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m30s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m30s
This commit is contained in:
@@ -10,7 +10,7 @@ const FEDERATION_DOMAIN = process.env.FEDERATION_DOMAIN || '';
|
||||
let privateKeyPem = process.env.FEDERATION_PRIVATE_KEY || '';
|
||||
let publicKeyPem = '';
|
||||
|
||||
// Load or generate RSA keys
|
||||
// Load or generate Ed25519 keys
|
||||
if (FEDERATION_DOMAIN) {
|
||||
const keyPath = path.join(__dirname, 'federation_key.pem');
|
||||
|
||||
@@ -19,9 +19,8 @@ if (FEDERATION_DOMAIN) {
|
||||
}
|
||||
|
||||
if (!privateKeyPem) {
|
||||
console.log('Generating new RSA federation key pair...');
|
||||
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
console.log('Generating new Ed25519 federation key pair...');
|
||||
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
|
||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
||||
});
|
||||
@@ -47,7 +46,7 @@ export function getFederationDomain() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this instance's RSA public key (PEM format).
|
||||
* Get this instance's Ed25519 public key (PEM format).
|
||||
*/
|
||||
export function getPublicKey() {
|
||||
return publicKeyPem;
|
||||
@@ -61,21 +60,18 @@ export function isFederationEnabled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA sign a JSON payload.
|
||||
* Ed25519 sign a JSON payload.
|
||||
* @param {object} payload
|
||||
* @returns {string} base64 signature
|
||||
*/
|
||||
export function signPayload(payload) {
|
||||
if (!privateKeyPem) throw new Error("Federation private key not available");
|
||||
const data = Buffer.from(JSON.stringify(payload));
|
||||
const sign = crypto.createSign('SHA256');
|
||||
sign.update(data);
|
||||
sign.end();
|
||||
return sign.sign(privateKeyPem, 'base64');
|
||||
return crypto.sign(null, data, privateKeyPem).toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify an RSA signature against a JSON payload using a remote public key.
|
||||
* Verify an Ed25519 signature against a JSON payload using a remote public key.
|
||||
* @param {object} payload
|
||||
* @param {string} signature base64 signature
|
||||
* @param {string} remotePublicKeyPem
|
||||
@@ -85,10 +81,7 @@ export function verifyPayload(payload, signature, remotePublicKeyPem) {
|
||||
if (!remotePublicKeyPem || !signature) return false;
|
||||
try {
|
||||
const data = Buffer.from(JSON.stringify(payload));
|
||||
const verify = crypto.createVerify('SHA256');
|
||||
verify.update(data);
|
||||
verify.end();
|
||||
return verify.verify(remotePublicKeyPem, signature, 'base64');
|
||||
return crypto.verify(null, data, remotePublicKeyPem, Buffer.from(signature, 'base64'));
|
||||
} catch (e) {
|
||||
console.error('Signature verification error:', e.message);
|
||||
return false;
|
||||
|
||||
@@ -49,19 +49,25 @@ export function isMailerConfigured() {
|
||||
* @param {string} verifyUrl – full verification URL
|
||||
* @param {string} appName – branding app name (default "Redlight")
|
||||
*/
|
||||
// S3: sanitize name for use in email From header (strip quotes, newlines, control chars)
|
||||
function sanitizeHeaderValue(str) {
|
||||
return String(str).replace(/["\\\r\n\x00-\x1f]/g, '').trim().slice(0, 100);
|
||||
}
|
||||
|
||||
export async function sendVerificationEmail(to, name, verifyUrl, appName = 'Redlight') {
|
||||
if (!transporter) {
|
||||
throw new Error('SMTP not configured');
|
||||
}
|
||||
|
||||
const from = process.env.SMTP_FROM || process.env.SMTP_USER;
|
||||
const headerAppName = sanitizeHeaderValue(appName);
|
||||
const safeName = escapeHtml(name);
|
||||
const safeAppName = escapeHtml(appName);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${appName}" <${from}>`,
|
||||
from: `"${headerAppName}" <${from}>`,
|
||||
to,
|
||||
subject: `${appName} – Verify your email`,
|
||||
subject: `${headerAppName} – Verify your email`,
|
||||
html: `
|
||||
<div style="font-family:Arial,sans-serif;max-width:520px;margin:0 auto;padding:32px;background:#1e1e2e;color:#cdd6f4;border-radius:12px;">
|
||||
<h2 style="color:#cba6f7;margin-top:0;">Hey ${safeName} 👋</h2>
|
||||
@@ -99,6 +105,7 @@ export async function sendFederationInviteEmail(to, name, fromUser, roomName, me
|
||||
if (!transporter) return; // silently skip if SMTP not configured
|
||||
|
||||
const from = process.env.SMTP_FROM || process.env.SMTP_USER;
|
||||
const headerAppName = sanitizeHeaderValue(appName);
|
||||
const safeName = escapeHtml(name);
|
||||
const safeFromUser = escapeHtml(fromUser);
|
||||
const safeRoomName = escapeHtml(roomName);
|
||||
@@ -106,9 +113,9 @@ export async function sendFederationInviteEmail(to, name, fromUser, roomName, me
|
||||
const safeAppName = escapeHtml(appName);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${appName}" <${from}>`,
|
||||
from: `"${headerAppName}" <${from}>`,
|
||||
to,
|
||||
subject: `${appName} – Meeting invitation from ${fromUser}`,
|
||||
subject: `${headerAppName} – Meeting invitation from ${sanitizeHeaderValue(fromUser)}`,
|
||||
html: `
|
||||
<div style="font-family:Arial,sans-serif;max-width:520px;margin:0 auto;padding:32px;background:#1e1e2e;color:#cdd6f4;border-radius:12px;">
|
||||
<h2 style="color:#cba6f7;margin-top:0;">Hey ${safeName} 👋</h2>
|
||||
|
||||
Reference in New Issue
Block a user