Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| * Accepts any string and hashes it to a 32-byte key | ||
| */ | ||
| function getEncryptionKey(): Buffer { | ||
| const keyHex = process.env[ENCRYPTION_KEY_ENV]; |
There was a problem hiding this comment.
The change from hex-string-based key derivation to SHA256-hashing introduces a breaking change that could cause data loss if existing encrypted credentials are migrated to this new version.
View Details
📝 Patch Details
diff --git a/lib/db/integrations.ts b/lib/db/integrations.ts
index 7a186fe..313a63e 100644
--- a/lib/db/integrations.ts
+++ b/lib/db/integrations.ts
@@ -18,8 +18,9 @@ const ENCRYPTION_KEY_ENV = "INTEGRATION_ENCRYPTION_KEY";
/**
* Get or generate encryption key from environment
* Accepts any string and hashes it to a 32-byte key
+ * For backward compatibility, also supports 64-character hex strings (legacy format)
*/
-function getEncryptionKey(): Buffer {
+function getEncryptionKey(useLegacy: boolean = false): Buffer {
const keyString = process.env[ENCRYPTION_KEY_ENV];
if (!keyString) {
@@ -28,7 +29,17 @@ function getEncryptionKey(): Buffer {
);
}
- // Hash the input string to get a consistent 32-byte key
+ if (useLegacy) {
+ // Legacy format: treat as 64-character hex string
+ if (keyString.length !== 64) {
+ throw new Error(
+ `${ENCRYPTION_KEY_ENV} must be a 64-character hex string (32 bytes) when using legacy decryption`
+ );
+ }
+ return Buffer.from(keyString, "hex");
+ }
+
+ // New format: hash the input string to get a consistent 32-byte key
return createHash("sha256").update(keyString).digest();
}
@@ -52,9 +63,9 @@ export function encrypt(plaintext: string): string {
/**
* Decrypt encrypted data
+ * Attempts to decrypt with the new key format first, then falls back to legacy format
*/
export function decrypt(ciphertext: string): string {
- const key = getEncryptionKey();
const parts = ciphertext.split(":");
if (parts.length !== 3) {
@@ -65,13 +76,32 @@ export function decrypt(ciphertext: string): string {
const authTag = Buffer.from(parts[1], "hex");
const encrypted = parts[2];
- const decipher = createDecipheriv(ALGORITHM, key, iv);
- decipher.setAuthTag(authTag);
+ // Try new key format first
+ try {
+ const key = getEncryptionKey(false);
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
+ decipher.setAuthTag(authTag);
- let decrypted = decipher.update(encrypted, "hex", "utf8");
- decrypted += decipher.final("utf8");
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
+ decrypted += decipher.final("utf8");
- return decrypted;
+ return decrypted;
+ } catch (error) {
+ // If new format fails, try legacy format
+ try {
+ const key = getEncryptionKey(true);
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
+ decipher.setAuthTag(authTag);
+
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
+ decrypted += decipher.final("utf8");
+
+ return decrypted;
+ } catch (legacyError) {
+ // Both formats failed, throw the original error
+ throw error;
+ }
+ }
}
/**
Analysis
Breaking change in encryption key derivation causes silent data loss on upgrade
What fails: The getEncryptionKey() function was changed from treating the INTEGRATION_ENCRYPTION_KEY environment variable as a 64-character hex string to treating it as a plain string to hash with SHA256. This breaks decryption of existing encrypted credentials when upgrading between versions.
How to reproduce:
- With the old code, set
INTEGRATION_ENCRYPTION_KEY=abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234(64-char hex) - Create and encrypt integration credentials, storing them in the database
- Update to the new version (without changing the env var)
- Attempt to retrieve the credentials by calling
getIntegrations()orgetIntegration()
Result: The encrypted credentials are decrypted with the new SHA256-based key, which produces a completely different key than the old hex-based derivation. The authentication tag mismatch causes decryption to fail with "Unsupported state or unable to authenticate data". The error is silently caught in decryptConfig() (lines 87-94), returning an empty object {} instead of the actual config.
Expected behavior: Existing encrypted credentials should remain readable after upgrading. New credentials can use the improved key derivation, but old data should not become inaccessible.
Root cause:
- Old:
Buffer.from(keyHex, 'hex')interprets the env var as hex-encoded binary (32 bytes directly) - New:
createHash("sha256").update(keyString).digest()interprets the env var as a UTF-8 string to hash
For the same env var value, these produce entirely different 32-byte keys, rendering all old encrypted data unreadable.
Fix implemented: Modified decrypt() to support both key formats by attempting decryption with the new format first, then falling back to the legacy format if it fails. This allows seamless reading of both old encrypted data and new data, while all new encryptions use the improved format.
No description provided.