| const crypto = require('crypto'); |
|
|
| const PREFIX = 'enc:v1'; |
|
|
| function getSecretKey() { |
| const source = |
| process.env.PAYMENT_CREDENTIAL_SECRET || |
| process.env.JWT_SECRET || |
| process.env.SNIPPE_API_KEY || |
| 'development-only-payment-credential-secret'; |
|
|
| return crypto.createHash('sha256').update(String(source)).digest(); |
| } |
|
|
| function encryptSecret(value) { |
| if (!value) return null; |
| const text = String(value); |
| if (text.startsWith(`${PREFIX}:`)) return text; |
|
|
| const iv = crypto.randomBytes(12); |
| const cipher = crypto.createCipheriv('aes-256-gcm', getSecretKey(), iv); |
| const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); |
| const tag = cipher.getAuthTag(); |
|
|
| return [ |
| PREFIX, |
| iv.toString('base64url'), |
| tag.toString('base64url'), |
| ciphertext.toString('base64url'), |
| ].join(':'); |
| } |
|
|
| function decryptSecret(value) { |
| if (!value) return null; |
| const text = String(value); |
| if (!text.startsWith(`${PREFIX}:`)) return text; |
|
|
| const parts = text.split(':'); |
| if (parts.length !== 5) { |
| throw new Error('Invalid encrypted secret format'); |
| } |
|
|
| const [, , ivValue, tagValue, ciphertextValue] = parts; |
| const decipher = crypto.createDecipheriv( |
| 'aes-256-gcm', |
| getSecretKey(), |
| Buffer.from(ivValue, 'base64url') |
| ); |
| decipher.setAuthTag(Buffer.from(tagValue, 'base64url')); |
|
|
| return Buffer.concat([ |
| decipher.update(Buffer.from(ciphertextValue, 'base64url')), |
| decipher.final(), |
| ]).toString('utf8'); |
| } |
|
|
| function maskSecret(value) { |
| const secret = decryptSecret(value); |
| if (!secret) return null; |
| if (secret.length <= 8) return '********'; |
| return `${secret.slice(0, 4)}...${secret.slice(-4)}`; |
| } |
|
|
| module.exports = { |
| decryptSecret, |
| encryptSecret, |
| maskSecret, |
| }; |
|
|