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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
// Constant-time comparison utilities
// Prevents timing attacks on token validation
use subtle::ConstantTimeEq;
/// Constant-time comparison utilities for security tokens
/// Uses subtle crate to ensure comparisons take the same time regardless of where differences occur
pub struct ConstantTimeOps;
impl ConstantTimeOps {
/// Compare two byte slices in constant time
///
/// Returns true if equal, false otherwise.
/// Time is independent of where the difference occurs, preventing timing attacks.
///
/// # Arguments
/// * `expected` - The expected (correct/known) value
/// * `actual` - The actual (untrusted) value from the user/attacker
///
/// # Examples
/// ```ignore
/// let stored_token = b"secret_token_value";
/// let user_token = b"user_provided_token";
/// assert!(!ConstantTimeOps::compare(stored_token, user_token));
/// ```
pub fn compare(expected: &[u8], actual: &[u8]) -> bool {
expected.ct_eq(actual).into()
}
/// Compare two strings in constant time
///
/// Converts strings to bytes and performs constant-time comparison.
/// Useful for comparing JWT tokens, session tokens, or other string-based secrets.
///
/// # Arguments
/// * `expected` - The expected (correct/known) string value
/// * `actual` - The actual (untrusted) string value from the user/attacker
pub fn compare_str(expected: &str, actual: &str) -> bool {
Self::compare(expected.as_bytes(), actual.as_bytes())
}
/// Compare two slices with different lengths in constant time
///
/// If lengths differ, still compares as much as possible to avoid leaking
/// length information through timing.
///
/// # SECURITY WARNING
/// This function is vulnerable to timing attacks that measure comparison duration.
/// For JWT tokens or other security-sensitive values, use `compare_padded()` instead
/// which always compares at a fixed length to prevent length disclosure.
pub fn compare_len_safe(expected: &[u8], actual: &[u8]) -> bool {
// If lengths differ, still compare constant-time
// First compare what we can, then check length
let min_len = expected.len().min(actual.len());
let prefix_equal = expected[..min_len].ct_eq(&actual[..min_len]);
let length_equal = (expected.len() == actual.len()) as u8;
(prefix_equal.unwrap_u8() & length_equal) != 0
}
/// Compare two byte slices at a fixed/padded length for timing attack prevention
///
/// Always compares at `fixed_len` bytes, padding with zeros if necessary.
/// This prevents timing attacks that measure comparison duration to determine length.
///
/// # Arguments
/// * `expected` - The expected (correct/known) value
/// * `actual` - The actual (untrusted) value from the user/attacker
/// * `fixed_len` - The fixed length to use for comparison (e.g., 512 for JWT tokens)
///
/// # SECURITY
/// Prevents length-based timing attacks. Time is independent of actual input lengths.
///
/// # Example
/// ```ignore
/// let stored_jwt = "eyJhbGc...";
/// let user_jwt = "eyJhbGc...";
/// // Always compares at 512 bytes, padding with zeros if needed
/// let result = ConstantTimeOps::compare_padded(
/// stored_jwt.as_bytes(),
/// user_jwt.as_bytes(),
/// 512
/// );
/// ```
pub fn compare_padded(expected: &[u8], actual: &[u8], fixed_len: usize) -> bool {
// SECURITY: Pad both inputs to fixed length before comparison
// This ensures timing is independent of actual token lengths
// Use fixed-size buffers to ensure stack allocation (no heap allocs during comparison)
let mut expected_padded = [0u8; 1024];
let mut actual_padded = [0u8; 1024];
// Ensure we don't overflow the fixed buffers
let pad_len = fixed_len.min(1024);
// Copy and pad to fixed length
if expected.len() <= pad_len {
expected_padded[..expected.len()].copy_from_slice(expected);
} else {
// Token is longer than fixed_len - only compare up to fixed_len bytes
expected_padded[..pad_len].copy_from_slice(&expected[..pad_len]);
}
if actual.len() <= pad_len {
actual_padded[..actual.len()].copy_from_slice(actual);
} else {
// Token is longer than fixed_len - only compare up to fixed_len bytes
actual_padded[..pad_len].copy_from_slice(&actual[..pad_len]);
}
// Constant-time comparison at fixed length
expected_padded[..pad_len].ct_eq(&actual_padded[..pad_len]).into()
}
/// Compare JWT tokens in constant time with fixed-length padding
///
/// JWT tokens are typically 300-800 bytes. Using 512-byte fixed-length comparison
/// prevents attackers from determining token length through timing analysis.
pub fn compare_jwt_constant(expected: &str, actual: &str) -> bool {
// Use 512-byte fixed length for JWT comparison (typical JWT size)
Self::compare_padded(expected.as_bytes(), actual.as_bytes(), 512)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compare_equal_bytes() {
let token1 = b"equal_token_value";
let token2 = b"equal_token_value";
assert!(ConstantTimeOps::compare(token1, token2));
}
#[test]
fn test_compare_different_bytes() {
let token1 = b"expected_token";
let token2 = b"actual_token_x";
assert!(!ConstantTimeOps::compare(token1, token2));
}
#[test]
fn test_compare_equal_strings() {
let token1 = "equal_token_value";
let token2 = "equal_token_value";
assert!(ConstantTimeOps::compare_str(token1, token2));
}
#[test]
fn test_compare_different_strings() {
let token1 = "expected_token";
let token2 = "actual_token_x";
assert!(!ConstantTimeOps::compare_str(token1, token2));
}
#[test]
fn test_compare_empty() {
let token1 = b"";
let token2 = b"";
assert!(ConstantTimeOps::compare(token1, token2));
}
#[test]
fn test_compare_different_lengths() {
let token1 = b"short";
let token2 = b"much_longer_token";
assert!(!ConstantTimeOps::compare(token1, token2));
}
#[test]
fn test_compare_len_safe() {
let expected = b"abcdefghij";
let actual = b"abcdefghij";
assert!(ConstantTimeOps::compare_len_safe(expected, actual));
let different = b"abcdefghix";
assert!(!ConstantTimeOps::compare_len_safe(expected, different));
let shorter = b"abcdefgh";
assert!(!ConstantTimeOps::compare_len_safe(expected, shorter));
}
#[test]
fn test_null_bytes_comparison() {
let token1 = b"token\x00with\x00nulls";
let token2 = b"token\x00with\x00nulls";
assert!(ConstantTimeOps::compare(token1, token2));
let different = b"token\x00with\x00other";
assert!(!ConstantTimeOps::compare(token1, different));
}
#[test]
fn test_all_byte_values() {
let mut token1 = vec![0u8; 256];
let mut token2 = vec![0u8; 256];
for i in 0..256 {
token1[i] = i as u8;
token2[i] = i as u8;
}
assert!(ConstantTimeOps::compare(&token1, &token2));
token2[127] = token2[127].wrapping_add(1);
assert!(!ConstantTimeOps::compare(&token1, &token2));
}
#[test]
fn test_very_long_tokens() {
let token1: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
let token2 = token1.clone();
assert!(ConstantTimeOps::compare(&token1, &token2));
let mut token3 = token1.clone();
token3[5_000] = token3[5_000].wrapping_add(1);
assert!(!ConstantTimeOps::compare(&token1, &token3));
}
#[test]
fn test_compare_padded_equal_length() {
let token1 = b"same_token_value";
let token2 = b"same_token_value";
assert!(ConstantTimeOps::compare_padded(token1, token2, 512));
}
#[test]
fn test_compare_padded_different_length_shorter_actual() {
let expected = b"this_is_expected_token_value";
let actual = b"short";
// Should still reject because content differs when padded to fixed length
assert!(!ConstantTimeOps::compare_padded(expected, actual, 512));
}
#[test]
fn test_compare_padded_different_length_longer_actual() {
let expected = b"expected";
let actual = b"this_is_a_much_longer_actual_token_that_exceeds_expected";
// Should still reject because content differs
assert!(!ConstantTimeOps::compare_padded(expected, actual, 512));
}
#[test]
fn test_compare_padded_timing_consistency() {
// SECURITY TEST: Ensure padding prevents timing leaks on token length
let short_token = b"short";
let long_token = b"this_is_a_much_longer_token_value_with_more_content";
// Both should perform comparison at fixed 512-byte length
// If timing attack vulnerability existed, these would take different times
let _ = ConstantTimeOps::compare_padded(short_token, short_token, 512);
let _ = ConstantTimeOps::compare_padded(long_token, long_token, 512);
// Should both return true since they're comparing to themselves
assert!(ConstantTimeOps::compare_padded(short_token, short_token, 512));
assert!(ConstantTimeOps::compare_padded(long_token, long_token, 512));
}
#[test]
fn test_compare_jwt_constant() {
let jwt1 = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.signature123";
let jwt2 = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.signature123";
assert!(ConstantTimeOps::compare_jwt_constant(jwt1, jwt2));
}
#[test]
fn test_compare_jwt_constant_different() {
let jwt1 = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.signature123";
let jwt2 = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.signature999";
assert!(!ConstantTimeOps::compare_jwt_constant(jwt1, jwt2));
}
#[test]
fn test_compare_jwt_constant_prevents_length_attack() {
// SECURITY: Verify that short JWT is rejected even against long JWT
let short_invalid_jwt = "short";
let long_valid_jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIn0.sig123";
// Should reject because they're different
assert!(!ConstantTimeOps::compare_jwt_constant(short_invalid_jwt, long_valid_jwt));
// Both comparisons should take similar time despite length difference
// (constant-time due to padding to 512 bytes)
assert!(!ConstantTimeOps::compare_jwt_constant(short_invalid_jwt, long_valid_jwt));
}
#[test]
fn test_compare_padded_zero_length() {
// Edge case: comparing empty tokens at fixed length
let token1 = b"";
let token2 = b"";
assert!(ConstantTimeOps::compare_padded(token1, token2, 512));
}
#[test]
fn test_compare_padded_exact_fixed_length() {
// Tokens exactly matching fixed length
let token = b"a".repeat(512);
assert!(ConstantTimeOps::compare_padded(&token, &token, 512));
let mut different = token.clone();
different[256] = different[256].wrapping_add(1);
assert!(!ConstantTimeOps::compare_padded(&token, &different, 512));
}
#[test]
fn test_compare_padded_exceeds_max_buffer() {
// Edge case: fixed_len exceeds max buffer (1024)
let token1 = b"test";
let token2 = b"test";
// Should still work, capping at 1024
assert!(ConstantTimeOps::compare_padded(token1, token2, 2048));
}
#[test]
fn test_timing_attack_prevention_early_difference() {
// First byte different - timing attack would be fast on this
let token1 = b"XXXXXXX_correct_token";
let token2 = b"YYYYYYY_correct_token";
let result = ConstantTimeOps::compare(token1, token2);
assert!(!result);
// Should take same time as other comparisons due to constant-time implementation
}
#[test]
fn test_timing_attack_prevention_late_difference() {
// Last byte different - timing attack would be slow on this
let token1 = b"correct_token_XXXXXXX";
let token2 = b"correct_token_YYYYYYY";
let result = ConstantTimeOps::compare(token1, token2);
assert!(!result);
// Should take same time as early_difference due to constant-time implementation
}
#[test]
fn test_jwt_constant_padding() {
// Test that padded JWT comparison handles typical JWT sizes
let short_jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.abc";
let padded_jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.abc";
assert!(ConstantTimeOps::compare_jwt_constant(short_jwt, padded_jwt));
}
#[test]
fn test_jwt_constant_different_lengths() {
// Padded comparison prevents length-based timing attacks
let jwt1 = "short";
let jwt2 = "very_long_jwt_token_with_lots_of_data_making_it_much_longer";
let result = ConstantTimeOps::compare_jwt_constant(jwt1, jwt2);
assert!(!result);
// Comparison time is independent of length difference
}
}