-
-
Notifications
You must be signed in to change notification settings - Fork 8
Implement AES-256-GCM encryption, secure initialization, and improve SkyFi tool robustness #751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2e1aceb
33fcb76
bb90e0d
b722344
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,54 @@ | ||
| import crypto from 'crypto'; | ||
|
|
||
| const ALGORITHM = 'aes-256-cbc'; | ||
| const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'default_32_byte_secret_key_placeholder_for_local_dev'; // Must be 32 bytes | ||
| const ALGORITHM_GCM = 'aes-256-gcm'; | ||
| const ALGORITHM_CBC = 'aes-256-cbc'; | ||
|
|
||
| if (!process.env.ENCRYPTION_KEY) { | ||
| throw new Error('ENCRYPTION_KEY environment variable is not set'); | ||
| } | ||
| const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; | ||
|
|
||
| export function encrypt(text: string | null | undefined): string | null { | ||
| if (!text) return null; | ||
| const iv = crypto.randomBytes(16); | ||
| const iv = crypto.randomBytes(12); // Standard IV size for GCM is 12 bytes | ||
| // Ensure the key is exactly 32 bytes | ||
| const key = crypto.createHash('sha256').update(ENCRYPTION_KEY).digest(); | ||
| const cipher = crypto.createCipheriv(ALGORITHM, key, iv); | ||
| const cipher = crypto.createCipheriv(ALGORITHM_GCM, key, iv); | ||
| let encrypted = cipher.update(text, 'utf8', 'hex'); | ||
| encrypted += cipher.final('hex'); | ||
| return `${iv.toString('hex')}:${encrypted}`; | ||
| const tag = cipher.getAuthTag().toString('hex'); | ||
| return `${iv.toString('hex')}:${encrypted}:${tag}`; | ||
| } | ||
|
|
||
| export function decrypt(encryptedText: string | null | undefined): string | null { | ||
| if (!encryptedText) return null; | ||
| try { | ||
| const [ivHex, encrypted] = encryptedText.split(':'); | ||
| if (!ivHex || !encrypted) return null; | ||
| const iv = Buffer.from(ivHex, 'hex'); | ||
| const parts = encryptedText.split(':'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Preserve legacy ciphertexts during the format migration. Before this change, persisted SkyFi secrets were serialized as |
||
| const key = crypto.createHash('sha256').update(ENCRYPTION_KEY).digest(); | ||
| const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); | ||
| let decrypted = decipher.update(encrypted, 'hex', 'utf8'); | ||
| decrypted += decipher.final('utf8'); | ||
| return decrypted; | ||
|
|
||
| if (parts.length === 3) { | ||
| // New AES-256-GCM format | ||
| const [ivHex, encrypted, tagHex] = parts; | ||
| if (!ivHex || !encrypted || !tagHex) return null; | ||
| const iv = Buffer.from(ivHex, 'hex'); | ||
| const tag = Buffer.from(tagHex, 'hex'); | ||
| const decipher = crypto.createDecipheriv(ALGORITHM_GCM, key, iv); | ||
| decipher.setAuthTag(tag); | ||
| let decrypted = decipher.update(encrypted, 'hex', 'utf8'); | ||
| decrypted += decipher.final('utf8'); | ||
| return decrypted; | ||
| } else if (parts.length === 2) { | ||
| // Legacy AES-256-CBC format | ||
| const [ivHex, encrypted] = parts; | ||
| if (!ivHex || !encrypted) return null; | ||
| const iv = Buffer.from(ivHex, 'hex'); | ||
| const decipher = crypto.createDecipheriv(ALGORITHM_CBC, key, iv); | ||
| let decrypted = decipher.update(encrypted, 'hex', 'utf8'); | ||
| decrypted += decipher.final('utf8'); | ||
| return decrypted; | ||
| } | ||
|
|
||
| return null; | ||
| } catch (error) { | ||
| console.error('[Encryption] Decryption failed:', error); | ||
| return null; | ||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import crypto from 'crypto'; | ||
| import { encrypt, decrypt } from '../lib/utils/encryption'; | ||
|
|
||
| console.log("Starting encryption tests..."); | ||
|
|
||
| const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'my-super-secret-key-32-chars-long-or-more'; | ||
|
|
||
| // Test basic GCM encrypt/decrypt | ||
| const originalText = "Hello, GCM authenticated encryption!"; | ||
| const encrypted = encrypt(originalText); | ||
|
|
||
| if (!encrypted) { | ||
| throw new Error("Encryption failed: returned null"); | ||
| } | ||
|
|
||
| console.log("Encrypted GCM Text:", encrypted); | ||
|
|
||
| const parts = encrypted.split(':'); | ||
| if (parts.length !== 3) { | ||
| throw new Error(`Encrypted text does not have 3 parts separated by colons. Got: ${parts.length}`); | ||
| } | ||
|
|
||
| const decrypted = decrypt(encrypted); | ||
| if (decrypted !== originalText) { | ||
| throw new Error(`GCM Decryption failed! Expected: "${originalText}", got: "${decrypted}"`); | ||
| } | ||
| console.log("GCM Decryption verified successfully!"); | ||
|
|
||
| // Test legacy CBC decryption backwards compatibility | ||
| const legacyText = "Hello, legacy CBC encryption!"; | ||
| const ivCbc = crypto.randomBytes(16); | ||
| const key = crypto.createHash('sha256').update(ENCRYPTION_KEY).digest(); | ||
| const cipherCbc = crypto.createCipheriv('aes-256-cbc', key, ivCbc); | ||
| let encryptedCbc = cipherCbc.update(legacyText, 'utf8', 'hex'); | ||
| encryptedCbc += cipherCbc.final('hex'); | ||
| const legacyPayload = `${ivCbc.toString('hex')}:${encryptedCbc}`; | ||
|
|
||
| console.log("Legacy CBC payload:", legacyPayload); | ||
| const decryptedLegacy = decrypt(legacyPayload); | ||
| if (decryptedLegacy !== legacyText) { | ||
| throw new Error(`Legacy CBC Decryption failed! Expected: "${legacyText}", got: "${decryptedLegacy}"`); | ||
| } | ||
| console.log("Legacy CBC Decryption backwards compatibility verified successfully!"); | ||
|
|
||
| // Test integrity protection (GCM authentication tag verification) | ||
| // Modify one byte of the ciphertext | ||
| const [iv, ciphertext, tag] = parts; | ||
| const modifiedCiphertext = ciphertext.substring(0, ciphertext.length - 2) + (ciphertext.endsWith('0') ? '1' : '0'); | ||
| const modifiedEncrypted = `${iv}:${modifiedCiphertext}:${tag}`; | ||
|
|
||
| // Decrypting tampered or invalid hex string might log an error and return null. | ||
| // We disable console.error temporarily to keep the test logs clean. | ||
| const originalConsoleError = console.error; | ||
| console.error = () => {}; | ||
| try { | ||
| const decryptedModified = decrypt(modifiedEncrypted); | ||
| if (decryptedModified !== null) { | ||
| throw new Error("Decryption of modified ciphertext did not return null! GCM authentication tag failed to catch tamper."); | ||
| } | ||
| } finally { | ||
| console.error = originalConsoleError; | ||
| } | ||
| console.log("GCM Authentication tag verification passed!"); | ||
|
|
||
| // Test null/undefined handling | ||
| if (encrypt(null) !== null || encrypt(undefined) !== null) { | ||
| throw new Error("encrypt(null/undefined) did not return null"); | ||
| } | ||
| if (decrypt(null) !== null || decrypt(undefined) !== null) { | ||
| throw new Error("decrypt(null/undefined) did not return null"); | ||
| } | ||
| console.log("Null/undefined handling passed!"); | ||
|
|
||
| console.log("All encryption tests passed!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Ensure the new required secret is supplied by supported deployments. This top-level throw makes routes importing
SkyfiOAuthProviderfail whenENCRYPTION_KEYis missing. The checked-in.env.local.example,docker-compose.yaml, andCLOUD_DEPLOYMENT.mdomit this variable, so documented local/container/cloud setups can no longer use SkyFi. Add the variable/secret to those paths (or a deploy-time validation that prevents an invalid deployment) and document key generation/rotation.