-- Enable pgcrypto for MFA secret encryption -- Wave 121 Agent 1: Fix CRITICAL security blocker (MFA plaintext secrets) -- CVSS Impact: Prevents unauthorized access to TOTP secrets -- Enable pgcrypto extension for encryption functions CREATE EXTENSION IF NOT EXISTS pgcrypto; -- Create encryption key table (single row, versioned keys for rotation) CREATE TABLE IF NOT EXISTS mfa_encryption_keys ( id SERIAL PRIMARY KEY, key_version INTEGER UNIQUE NOT NULL DEFAULT 1, encryption_key BYTEA NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, rotated_at TIMESTAMP WITH TIME ZONE, is_active BOOLEAN DEFAULT TRUE NOT NULL, CONSTRAINT ensure_single_active_key CHECK ( is_active = TRUE OR rotated_at IS NOT NULL ) ); -- Create index for active key lookup CREATE UNIQUE INDEX IF NOT EXISTS idx_mfa_encryption_keys_active ON mfa_encryption_keys(key_version) WHERE is_active = TRUE; -- Generate initial encryption key (256-bit AES key) INSERT INTO mfa_encryption_keys (key_version, encryption_key, is_active) VALUES ( 1, gen_random_bytes(32), -- 256-bit key TRUE ) ON CONFLICT (key_version) DO NOTHING; -- Function to get active encryption key CREATE OR REPLACE FUNCTION get_active_mfa_key() RETURNS BYTEA AS $$ DECLARE v_key BYTEA; BEGIN SELECT encryption_key INTO v_key FROM mfa_encryption_keys WHERE is_active = TRUE ORDER BY key_version DESC LIMIT 1; IF v_key IS NULL THEN RAISE EXCEPTION 'No active MFA encryption key found'; END IF; RETURN v_key; END; $$ LANGUAGE plpgsql STABLE SECURITY DEFINER; -- Function to encrypt TOTP secret using pgcrypto -- Uses AES-256-CBC with the active encryption key CREATE OR REPLACE FUNCTION encrypt_mfa_secret(p_secret TEXT) RETURNS BYTEA AS $$ DECLARE v_key BYTEA; BEGIN v_key := get_active_mfa_key(); -- Encrypt using AES-256-CBC -- Format: pgp_sym_encrypt(data, password, options) -- We use the raw encryption key directly for maximum security RETURN encrypt(p_secret::BYTEA, v_key, 'aes'); END; $$ LANGUAGE plpgsql STABLE SECURITY DEFINER; -- Function to decrypt TOTP secret using pgcrypto CREATE OR REPLACE FUNCTION decrypt_mfa_secret(p_encrypted BYTEA) RETURNS TEXT AS $$ DECLARE v_key BYTEA; v_decrypted BYTEA; BEGIN v_key := get_active_mfa_key(); -- Decrypt using AES-256-CBC v_decrypted := decrypt(p_encrypted, v_key, 'aes'); RETURN convert_from(v_decrypted, 'UTF8'); EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION 'Failed to decrypt MFA secret: %', SQLERRM; END; $$ LANGUAGE plpgsql STABLE SECURITY DEFINER; -- Function to rotate encryption key (for key management) CREATE OR REPLACE FUNCTION rotate_mfa_encryption_key() RETURNS INTEGER AS $$ DECLARE v_new_key_version INTEGER; v_new_key BYTEA; BEGIN -- Get next key version SELECT COALESCE(MAX(key_version), 0) + 1 INTO v_new_key_version FROM mfa_encryption_keys; -- Generate new 256-bit key v_new_key := gen_random_bytes(32); -- Mark old keys as inactive UPDATE mfa_encryption_keys SET is_active = FALSE, rotated_at = NOW() WHERE is_active = TRUE; -- Insert new key INSERT INTO mfa_encryption_keys (key_version, encryption_key, is_active) VALUES (v_new_key_version, v_new_key, TRUE); RAISE NOTICE 'MFA encryption key rotated to version %', v_new_key_version; RETURN v_new_key_version; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- Grant execute permissions to the application role GRANT EXECUTE ON FUNCTION get_active_mfa_key() TO foxhunt; GRANT EXECUTE ON FUNCTION encrypt_mfa_secret(TEXT) TO foxhunt; GRANT EXECUTE ON FUNCTION decrypt_mfa_secret(BYTEA) TO foxhunt; GRANT EXECUTE ON FUNCTION rotate_mfa_encryption_key() TO foxhunt; -- Prevent direct access to encryption keys table REVOKE ALL ON TABLE mfa_encryption_keys FROM PUBLIC; GRANT SELECT ON TABLE mfa_encryption_keys TO foxhunt; -- Add comment for documentation COMMENT ON FUNCTION encrypt_mfa_secret(TEXT) IS 'Encrypts TOTP secret using AES-256-CBC with pgcrypto. Uses active encryption key from mfa_encryption_keys table.'; COMMENT ON FUNCTION decrypt_mfa_secret(BYTEA) IS 'Decrypts TOTP secret using AES-256-CBC with pgcrypto. Requires active encryption key.'; COMMENT ON FUNCTION rotate_mfa_encryption_key() IS 'Rotates MFA encryption key. New key becomes active, old keys are marked inactive. Use during key rotation ceremonies.'; -- Verification query to ensure encryption works DO $$ DECLARE v_test_encrypted BYTEA; v_test_decrypted TEXT; BEGIN -- Test encryption v_test_encrypted := encrypt_mfa_secret('TEST_SECRET_12345'); -- Test decryption v_test_decrypted := decrypt_mfa_secret(v_test_encrypted); IF v_test_decrypted = 'TEST_SECRET_12345' THEN RAISE NOTICE '✅ MFA encryption verification PASSED'; ELSE RAISE EXCEPTION '❌ MFA encryption verification FAILED: expected "TEST_SECRET_12345", got "%"', v_test_decrypted; END IF; END; $$;