(plaintext: string)
| 54 | * base64). The IV is fresh per call; never reuse a (key, iv) pair. |
| 55 | */ |
| 56 | export const encryptString = (plaintext: string): string => { |
| 57 | const key = decodeKey(env.MFA_ENCRYPTION_KEY); |
| 58 | const iv = randomBytes(AES_GCM_IV_BYTES); |
| 59 | /* |
| 60 | * Pinning `authTagLength` matters on decrypt — a missing option lets |
| 61 | * Node accept a shorter-than-expected tag, which an attacker could |
| 62 | * use to forge ciphertext. We pass it on encrypt too so the value |
| 63 | * the cipher emits and the value the decipher will verify are the |
| 64 | * same constant. |
| 65 | */ |
| 66 | const cipher = createCipheriv(CIPHER_ALGORITHM, key, iv, { |
| 67 | authTagLength: AES_GCM_TAG_BYTES, |
| 68 | }); |
| 69 | const ciphertext = Buffer.concat([ |
| 70 | cipher.update(plaintext, "utf8"), |
| 71 | cipher.final(), |
| 72 | ]); |
| 73 | const tag = cipher.getAuthTag(); |
| 74 | |
| 75 | return [ |
| 76 | CIPHERTEXT_VERSION, |
| 77 | iv.toString("base64"), |
| 78 | ciphertext.toString("base64"), |
| 79 | tag.toString("base64"), |
| 80 | ].join("$"); |
| 81 | }; |
| 82 | |
| 83 | /** |
| 84 | * Inverse of `encryptString`. Throws on any failure (bad version, |
no test coverage detected