1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! Query hashing for APQ (Automatic Persisted Queries)
//!
//! Provides SHA-256 hashing for GraphQL queries to create persisted query IDs.
//!
//! **SECURITY CRITICAL**: Response cache keys MUST include variables to prevent
//! data leakage between requests with different variable values.
//!
//! Example vulnerability if variables not included in cache key:
//! - Client A: POST { user(id: "123") } → cached response for user 123
//! - Client B: POST { user(id: "456") } → receives cached response for user 123!
//!
//! Mitigation: Use `hash_query_with_variables()` for response caching.
use Value as JsonValue;
use ;
use ConstantTimeEq as _;
/// Compute SHA-256 hash of a GraphQL query
///
/// # Arguments
///
/// * `query` - The GraphQL query string
///
/// # Returns
///
/// A hexadecimal string representation of the SHA-256 hash (64 characters)
///
/// # Examples
///
/// ```
/// use fraiseql_core::apq::hasher::hash_query;
///
/// let query = "{ users { id name } }";
/// let hash = hash_query(query);
/// assert_eq!(hash.len(), 64); // SHA-256 produces 64 hex chars
/// ```
/// Verify that a query matches the provided hash.
///
/// Uses constant-time comparison (`subtle::ConstantTimeEq`) to prevent timing
/// oracles that could leak information about the hash value.
///
/// # Arguments
///
/// * `query` - The GraphQL query string
/// * `expected_hash` - The expected SHA-256 hash (hexadecimal, 64 chars)
///
/// # Returns
///
/// `true` if the query hash matches the expected hash, `false` otherwise.
/// Returns `false` immediately (without hashing) if `expected_hash` is not
/// exactly 64 hex characters — an invalid hash can never match.
///
/// # Examples
///
/// ```
/// use fraiseql_core::apq::hasher::{hash_query, verify_hash};
///
/// let query = "{ users { id name } }";
/// let hash = hash_query(query);
/// assert!(verify_hash(query, &hash));
/// assert!(!verify_hash(query, "invalid_hash"));
/// ```
/// Compute combined hash of query + variables for response caching
///
/// **SECURITY CRITICAL**: This function combines query hash with normalized
/// variables to create a cache key that prevents data leakage between requests
/// with different variable values.
///
/// # Arguments
///
/// * `query` - The GraphQL query string
/// * `variables` - Optional GraphQL variables as JSON object
///
/// # Returns
///
/// A hexadecimal string representing the combined SHA-256 hash
///
/// # Examples
///
/// ```
/// use fraiseql_core::apq::hasher::hash_query_with_variables;
/// use serde_json::json;
///
/// let query = "query getUser($id: ID!) { user(id: $id) { name } }";
/// let vars = json!({"id": "123"});
/// let cache_key = hash_query_with_variables(query, &vars);
/// assert_eq!(cache_key.len(), 64); // SHA-256 produces 64 hex chars
/// ```
///
/// # Security Notes
///
/// - Variables are normalized with sorted keys for consistent hashing
/// - Different variable values ALWAYS produce different hashes
/// - Empty/null variables fall back to query-only hash
/// - Safe for use as response cache key
///
/// # Panics
///
/// Cannot panic in practice — `serde_json::to_string` on a `serde_json::Value`
/// is infallible (all `Value` variants are serializable).
/// Recursively normalize a JSON value by sorting object keys at every level.
///
/// This makes hashing robust against key-order variance in the source (e.g.
/// if `serde_json`'s internal map type changes from `BTreeMap` to a non-sorted type).
/// Verify that query + variables match the provided combined hash.
///
/// **SECURITY CRITICAL**: Use this to validate APQ response cache hits.
///
/// Uses constant-time comparison (`subtle::ConstantTimeEq`) to prevent timing
/// oracles. Returns `false` immediately if `expected_hash` is not exactly 64
/// hex characters.
///
/// # Arguments
///
/// * `query` - The GraphQL query string
/// * `variables` - GraphQL variables as JSON object
/// * `expected_hash` - The expected combined hash (hexadecimal, 64 chars)
///
/// # Returns
///
/// `true` if the combined hash matches, `false` otherwise
///
/// # Examples
///
/// ```
/// use fraiseql_core::apq::hasher::{hash_query_with_variables, verify_hash_with_variables};
/// use serde_json::json;
///
/// let query = "{ users { id } }";
/// let vars = json!({"limit": 10});
/// let hash = hash_query_with_variables(query, &vars);
/// assert!(verify_hash_with_variables(query, &vars, &hash));
/// ```