| 87 | * branch. |
| 88 | */ |
| 89 | export const decryptString = (payload: string): string => { |
| 90 | const parts = payload.split("$"); |
| 91 | |
| 92 | if (parts.length !== 4) { |
| 93 | throw ApiErrors.internal("Encrypted payload is malformed"); |
| 94 | } |
| 95 | |
| 96 | const [version, ivB64, ciphertextB64, tagB64] = parts; |
| 97 | |
| 98 | if (version !== CIPHERTEXT_VERSION) { |
| 99 | throw ApiErrors.internal( |
| 100 | `Unsupported ciphertext version: ${version ?? "<empty>"}` |
| 101 | ); |
| 102 | } |
| 103 | |
| 104 | if ( |
| 105 | ivB64 === undefined || |
| 106 | ciphertextB64 === undefined || |
| 107 | tagB64 === undefined |
| 108 | ) { |
| 109 | throw ApiErrors.internal("Encrypted payload is malformed"); |
| 110 | } |
| 111 | |
| 112 | const iv = Buffer.from(ivB64, "base64"); |
| 113 | const ciphertext = Buffer.from(ciphertextB64, "base64"); |
| 114 | const tag = Buffer.from(tagB64, "base64"); |
| 115 | |
| 116 | if (iv.length !== AES_GCM_IV_BYTES || tag.length !== AES_GCM_TAG_BYTES) { |
| 117 | throw ApiErrors.internal("Encrypted payload has wrong IV or tag length"); |
| 118 | } |
| 119 | |
| 120 | const key = decodeKey(env.MFA_ENCRYPTION_KEY); |
| 121 | const decipher = createDecipheriv(CIPHER_ALGORITHM, key, iv, { |
| 122 | authTagLength: AES_GCM_TAG_BYTES, |
| 123 | }); |
| 124 | |
| 125 | decipher.setAuthTag(tag); |
| 126 | |
| 127 | try { |
| 128 | const plaintext = Buffer.concat([ |
| 129 | decipher.update(ciphertext), |
| 130 | decipher.final(), |
| 131 | ]); |
| 132 | |
| 133 | return plaintext.toString("utf8"); |
| 134 | } catch { |
| 135 | throw ApiErrors.internal("Failed to decrypt payload"); |
| 136 | } |
| 137 | }; |
| 138 | |
| 139 | /** |
| 140 | * Constant-time comparison for two same-length strings. Wraps |