Skip to main content

fraiseql_server/encryption/
query_builder.rs

1// Phase 12.3 Cycle 3: Query Builder Integration (REFACTOR)
2//! Query builder integration for transparent field-level encryption/decryption
3//!
4//! Provides automatic encryption on write operations and decryption on read
5//! operations at the query layer without application code changes.
6//!
7//! # Overview
8//!
9//! This module validates SQL queries to ensure encrypted fields are not used in
10//! operations that require plaintext comparison or ordering:
11//!
12//! - **WHERE clauses**: Encrypted fields cannot be directly compared
13//! - **ORDER BY clauses**: Encrypted ciphertext does not preserve plaintext order
14//! - **JOIN conditions**: Encrypted fields are not comparable
15//! - **GROUP BY clauses**: Encrypted ciphertext values are not stable
16//! - **IS NULL**: Allowed (NULL stored at database level, not encrypted)
17//!
18//! # Alternatives for Encrypted Field Queries
19//!
20//! When you need to query encrypted fields, consider:
21//!
22//! 1. **Deterministic Hash Index**
23//!    - Hash plaintext to deterministic value
24//!    - Store hash in separate index column
25//!    - Query using hash value
26//!    - Vulnerable to rainbow table attacks - only use for non-sensitive data
27//!
28//! 2. **Plaintext Copy Column**
29//!    - Store plaintext in unencrypted column (for non-sensitive subsets)
30//!    - Encrypt separate column for full value
31//!    - Query plaintext column, decrypt full value when needed
32//!
33//! 3. **Application-Level Filtering**
34//!    - SELECT all rows with encryption keys available
35//!    - Decrypt in application
36//!    - Filter in memory
37//!    - Works for reasonable result sets
38//!
39//! 4. **Vault-Native Encryption**
40//!    - Use Vault Transit engine's encrypt/decrypt
41//!    - Store encrypted data in separate "searchable" column
42//!    - Query using Vault API for pattern matching
43
44use std::collections::HashSet;
45
46use crate::secrets_manager::SecretsError;
47
48/// Query type for validation
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum QueryType {
51    /// INSERT operation
52    Insert,
53    /// SELECT operation
54    Select,
55    /// UPDATE operation
56    Update,
57    /// DELETE operation
58    Delete,
59}
60
61/// Clause type for validation
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ClauseType {
64    /// WHERE clause
65    Where,
66    /// ORDER BY clause
67    OrderBy,
68    /// JOIN condition
69    Join,
70    /// GROUP BY clause
71    GroupBy,
72}
73
74/// Query builder integration for encrypted field validation
75///
76/// Validates queries to ensure encrypted fields are not used in
77/// operations that require plaintext comparison (WHERE, ORDER BY, JOIN).
78pub struct QueryBuilderIntegration {
79    /// Set of encrypted field names
80    encrypted_fields: HashSet<String>,
81}
82
83impl QueryBuilderIntegration {
84    /// Create new query builder integration
85    pub fn new(encrypted_fields: Vec<String>) -> Self {
86        Self {
87            encrypted_fields: encrypted_fields.into_iter().collect(),
88        }
89    }
90
91    /// Validate that encrypted fields are not used in WHERE clause
92    ///
93    /// Encrypted fields cannot be directly compared due to non-deterministic
94    /// encryption (random nonces). Queries using encrypted fields in WHERE
95    /// must use alternative approaches like:
96    /// - Deterministic hash of plaintext
97    /// - Separate plaintext index
98    /// - Application-level filtering
99    pub fn validate_where_clause(&self, fields: &[&str]) -> Result<(), SecretsError> {
100        for field in fields {
101            if self.encrypted_fields.contains(&field.to_string()) {
102                return Err(SecretsError::ValidationError(format!(
103                    "Cannot use encrypted field '{}' in WHERE clause. \
104                     Encrypted fields are not queryable due to non-deterministic encryption. \
105                     Consider using: (1) deterministic hash of plaintext, \
106                     (2) separate plaintext index, or (3) application-level filtering.",
107                    field
108                )));
109            }
110        }
111
112        Ok(())
113    }
114
115    /// Validate that encrypted fields are not used in ORDER BY clause
116    ///
117    /// Encrypted fields cannot be compared for sorting because ciphertext
118    /// does not preserve plaintext order.
119    pub fn validate_order_by_clause(&self, fields: &[&str]) -> Result<(), SecretsError> {
120        for field in fields {
121            if self.encrypted_fields.contains(&field.to_string()) {
122                return Err(SecretsError::ValidationError(format!(
123                    "Cannot use encrypted field '{}' in ORDER BY clause. \
124                     Encrypted ciphertext does not preserve plaintext sort order. \
125                     Consider ordering by unencrypted fields instead.",
126                    field
127                )));
128            }
129        }
130
131        Ok(())
132    }
133
134    /// Validate that encrypted fields are not used in JOIN condition
135    ///
136    /// Encrypted fields cannot be compared in JOIN conditions because
137    /// ciphertext is non-deterministic (different every time even for same plaintext).
138    pub fn validate_join_condition(&self, fields: &[&str]) -> Result<(), SecretsError> {
139        for field in fields {
140            if self.encrypted_fields.contains(&field.to_string()) {
141                return Err(SecretsError::ValidationError(format!(
142                    "Cannot use encrypted field '{}' in JOIN condition. \
143                     Encrypted fields are not comparable. \
144                     JOIN on unencrypted fields instead, or denormalize data.",
145                    field
146                )));
147            }
148        }
149
150        Ok(())
151    }
152
153    /// Validate that encrypted fields are not used in GROUP BY clause
154    pub fn validate_group_by_clause(&self, fields: &[&str]) -> Result<(), SecretsError> {
155        for field in fields {
156            if self.encrypted_fields.contains(&field.to_string()) {
157                return Err(SecretsError::ValidationError(format!(
158                    "Cannot use encrypted field '{}' in GROUP BY clause. \
159                     Encrypted ciphertext values are not stable for grouping.",
160                    field
161                )));
162            }
163        }
164
165        Ok(())
166    }
167
168    /// Validate IS NULL can be used on encrypted fields
169    ///
170    /// IS NULL checks NULL at database level (before encryption),
171    /// so it works correctly with encrypted fields.
172    pub fn validate_is_null_on_encrypted(&self, _field: &str) -> Result<(), SecretsError> {
173        // IS NULL is always allowed on encrypted fields
174        // NULL is stored at database level, not encrypted
175        Ok(())
176    }
177
178    /// Validate clause type contains allowed fields
179    pub fn validate_clause(
180        &self,
181        clause_type: ClauseType,
182        fields: &[&str],
183    ) -> Result<(), SecretsError> {
184        match clause_type {
185            ClauseType::Where => self.validate_where_clause(fields),
186            ClauseType::OrderBy => self.validate_order_by_clause(fields),
187            ClauseType::Join => self.validate_join_condition(fields),
188            ClauseType::GroupBy => self.validate_group_by_clause(fields),
189        }
190    }
191
192    /// Get encrypted field names
193    pub fn encrypted_fields(&self) -> Vec<String> {
194        self.encrypted_fields.iter().cloned().collect()
195    }
196
197    /// Check if field is encrypted
198    pub fn is_encrypted(&self, field: &str) -> bool {
199        self.encrypted_fields.contains(&field.to_string())
200    }
201
202    /// Get encrypted fields that appear in field list
203    pub fn get_encrypted_fields_in_list(&self, fields: &[&str]) -> Vec<String> {
204        fields.iter().filter(|f| self.is_encrypted(f)).map(|f| f.to_string()).collect()
205    }
206
207    /// Validate entire query structure
208    ///
209    /// Performs comprehensive validation across multiple clauses.
210    pub fn validate_query(
211        &self,
212        query_type: QueryType,
213        where_fields: &[&str],
214        order_by_fields: &[&str],
215        join_fields: &[&str],
216    ) -> Result<(), SecretsError> {
217        // Validate based on query type
218        match query_type {
219            QueryType::Insert | QueryType::Update => {
220                // For INSERT/UPDATE, encrypted fields must be handled by adapter
221                // No clause restrictions for write operations
222                Ok(())
223            },
224            QueryType::Select => {
225                // For SELECT, validate all clause restrictions
226                self.validate_where_clause(where_fields)?;
227                self.validate_order_by_clause(order_by_fields)?;
228                self.validate_join_condition(join_fields)?;
229                Ok(())
230            },
231            QueryType::Delete => {
232                // For DELETE, encrypted fields not needed, no restrictions
233                Ok(())
234            },
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_validate_where_clause_unencrypted_field() {
245        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
246        let result = qbi.validate_where_clause(&["name"]);
247        assert!(result.is_ok());
248    }
249
250    #[test]
251    fn test_validate_where_clause_encrypted_field_rejects() {
252        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
253        let result = qbi.validate_where_clause(&["email"]);
254        assert!(result.is_err());
255        assert!(matches!(result, Err(SecretsError::ValidationError(_))));
256    }
257
258    #[test]
259    fn test_validate_order_by_unencrypted_field() {
260        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
261        let result = qbi.validate_order_by_clause(&["name"]);
262        assert!(result.is_ok());
263    }
264
265    #[test]
266    fn test_validate_order_by_encrypted_field_rejects() {
267        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
268        let result = qbi.validate_order_by_clause(&["email"]);
269        assert!(result.is_err());
270    }
271
272    #[test]
273    fn test_validate_join_unencrypted_field() {
274        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
275        let result = qbi.validate_join_condition(&["user_id"]);
276        assert!(result.is_ok());
277    }
278
279    #[test]
280    fn test_validate_join_encrypted_field_rejects() {
281        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
282        let result = qbi.validate_join_condition(&["email"]);
283        assert!(result.is_err());
284    }
285
286    #[test]
287    fn test_validate_group_by_unencrypted_field() {
288        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
289        let result = qbi.validate_group_by_clause(&["status"]);
290        assert!(result.is_ok());
291    }
292
293    #[test]
294    fn test_validate_group_by_encrypted_field_rejects() {
295        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
296        let result = qbi.validate_group_by_clause(&["email"]);
297        assert!(result.is_err());
298    }
299
300    #[test]
301    fn test_validate_is_null_on_encrypted_field() {
302        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
303        let result = qbi.validate_is_null_on_encrypted("email");
304        assert!(result.is_ok());
305    }
306
307    #[test]
308    fn test_validate_mixed_encrypted_unencrypted_fields() {
309        let qbi = QueryBuilderIntegration::new(vec!["email".to_string(), "phone".to_string()]);
310        // Should reject if any field is encrypted
311        let result = qbi.validate_where_clause(&["name", "email"]);
312        assert!(result.is_err());
313    }
314
315    #[test]
316    fn test_validate_clause_with_type() {
317        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
318        let result = qbi.validate_clause(ClauseType::Where, &["email"]);
319        assert!(result.is_err());
320
321        let result = qbi.validate_clause(ClauseType::OrderBy, &["email"]);
322        assert!(result.is_err());
323
324        let result = qbi.validate_clause(ClauseType::Join, &["email"]);
325        assert!(result.is_err());
326    }
327
328    #[test]
329    fn test_encrypted_fields_list() {
330        let qbi = QueryBuilderIntegration::new(vec!["email".to_string(), "phone".to_string()]);
331        let fields = qbi.encrypted_fields();
332        assert_eq!(fields.len(), 2);
333        assert!(fields.contains(&"email".to_string()));
334        assert!(fields.contains(&"phone".to_string()));
335    }
336
337    #[test]
338    fn test_is_encrypted() {
339        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
340        assert!(qbi.is_encrypted("email"));
341        assert!(!qbi.is_encrypted("name"));
342    }
343
344    #[test]
345    fn test_get_encrypted_fields_in_list() {
346        let qbi = QueryBuilderIntegration::new(vec!["email".to_string(), "phone".to_string()]);
347        let result = qbi.get_encrypted_fields_in_list(&["name", "email", "phone"]);
348        assert_eq!(result.len(), 2);
349        assert!(result.contains(&"email".to_string()));
350        assert!(result.contains(&"phone".to_string()));
351    }
352
353    #[test]
354    fn test_validate_query_insert_allows_encrypted() {
355        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
356        let result = qbi.validate_query(QueryType::Insert, &[], &[], &[]);
357        assert!(result.is_ok());
358    }
359
360    #[test]
361    fn test_validate_query_select_rejects_encrypted_where() {
362        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
363        let result = qbi.validate_query(QueryType::Select, &["email"], &[], &[]);
364        assert!(result.is_err());
365    }
366
367    #[test]
368    fn test_validate_query_delete_allows_encrypted() {
369        let qbi = QueryBuilderIntegration::new(vec!["email".to_string()]);
370        let result = qbi.validate_query(QueryType::Delete, &[], &[], &[]);
371        assert!(result.is_ok());
372    }
373}