JUNOHUB-test/src/lib/encryption.ts
root 284fb12576 Set up JUNOHUB app and EC2 deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 20:12:52 +05:00

103 lines
2.8 KiB
TypeScript

import crypto from "crypto"
const ALGORITHM = "aes-256-gcm"
const IV_LENGTH = 12 // 96-bit nonce is recommended for GCM
const AUTH_TAG_LENGTH = 16
/**
* Returns a 32-byte encryption key derived from the MFA_ENCRYPTION_KEY env var.
* Throws if the key is missing or too weak.
*/
function getKey(): Buffer {
const raw = process.env.MFA_ENCRYPTION_KEY
if (!raw) {
throw new Error("MFA_ENCRYPTION_KEY is not set")
}
// Derive a 32-byte key from the provided secret using SHA-256
return crypto.createHash("sha256").update(raw, "utf8").digest()
}
export interface EncryptedPayload {
v: number
iv: string
tag: string
data: string
}
/**
* Encrypts a UTF-8 string value using AES-256-GCM.
* Returns a compact JSON string that can be stored in the DB.
*/
export function encryptString(plaintext: string): string {
const key = getKey()
const iv = crypto.randomBytes(IV_LENGTH)
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
authTagLength: AUTH_TAG_LENGTH,
})
const encrypted = Buffer.concat([
cipher.update(plaintext, "utf8"),
cipher.final(),
])
const tag = cipher.getAuthTag()
const payload: EncryptedPayload = {
v: 1,
iv: iv.toString("base64"),
tag: tag.toString("base64"),
data: encrypted.toString("base64"),
}
return JSON.stringify(payload)
}
/**
* Decrypts a value previously produced by encryptString.
* For backward compatibility, if the input is not a valid payload,
* it will be returned as-is (assuming it is plaintext).
*/
export function decryptString(value: string | null): string | null {
if (!value) return null
try {
const parsed = JSON.parse(value) as EncryptedPayload
if (
typeof parsed !== "object" ||
parsed === null ||
parsed.v !== 1 ||
typeof parsed.iv !== "string" ||
typeof parsed.tag !== "string" ||
typeof parsed.data !== "string"
) {
// Not our expected structure; treat as plaintext
return value
}
const key = getKey()
const iv = Buffer.from(parsed.iv, "base64")
const tag = Buffer.from(parsed.tag, "base64")
const encrypted = Buffer.from(parsed.data, "base64")
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
authTagLength: AUTH_TAG_LENGTH,
})
decipher.setAuthTag(tag)
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final(),
])
return decrypted.toString("utf8")
} catch {
// If anything goes wrong (invalid JSON, missing env, crypto error),
// fall back to returning the original value to avoid locking users out.
return value
}
}