const crypto = require('crypto') const ALGORITHM = 'aes-256-gcm' const IV_LENGTH = 12 const SALT = 'modstagram-instagram-salt' let cachedKey = null function getKey () { if (cachedKey) return cachedKey const secret = process.env.INSTAGRAM_TOKEN_ENC_KEY || process.env.APP_SECRET if (!secret) { throw new Error( '[secretCipher] Neither INSTAGRAM_TOKEN_ENC_KEY nor APP_SECRET is configured' ) } if (!process.env.INSTAGRAM_TOKEN_ENC_KEY) { console.warn( '[secretCipher] INSTAGRAM_TOKEN_ENC_KEY is not set; falling back to APP_SECRET. Set a dedicated key in production.' ) } cachedKey = crypto.scryptSync(secret, SALT, 32) return cachedKey } /** * Encrypt a plaintext secret (e.g. Instagram access token) for at-rest storage. * Returns `${iv}:${authTag}:${ciphertext}` (all hex), or null for empty input. */ function encryptSecret (plaintext) { if (!plaintext) return null const key = getKey() const iv = crypto.randomBytes(IV_LENGTH) const cipher = crypto.createCipheriv(ALGORITHM, key, iv) const encrypted = Buffer.concat([ cipher.update(String(plaintext), 'utf8'), cipher.final() ]) const authTag = cipher.getAuthTag() return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}` } /** * Decrypt a value produced by encryptSecret. Returns null on missing/invalid input * instead of throwing, so a corrupt/legacy value never crashes a caller. */ function decryptSecret (encoded) { if (!encoded || typeof encoded !== 'string') return null const parts = encoded.split(':') if (parts.length !== 3) return null try { const [ivHex, authTagHex, dataHex] = parts const key = getKey() const decipher = crypto.createDecipheriv(ALGORITHM, key, Buffer.from(ivHex, 'hex')) decipher.setAuthTag(Buffer.from(authTagHex, 'hex')) const decrypted = Buffer.concat([ decipher.update(Buffer.from(dataHex, 'hex')), decipher.final() ]) return decrypted.toString('utf8') } catch (err) { console.error('[secretCipher] decrypt failed:', err.message) return null } } module.exports = { encryptSecret, decryptSecret }