Skip to content

Commit 4db6c9d

Browse files
refactor(validator): remove hardcoded dummy key for timing protection (#18)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent a17f08c commit 4db6c9d

File tree

5 files changed

+102
-39
lines changed

5 files changed

+102
-39
lines changed

crates/api-keys-simplified/src/domain.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::error::ConfigError;
1+
use crate::error::InitError;
22
use crate::validator::KeyStatus;
33
use crate::{
44
config::{Environment, KeyConfig, KeyPrefix},
@@ -53,12 +53,17 @@ impl ApiKeyManagerV0 {
5353
prefix: impl Into<String>,
5454
config: KeyConfig,
5555
hash_config: HashConfig,
56-
) -> std::result::Result<Self, ConfigError> {
56+
) -> std::result::Result<Self, InitError> {
5757
let include_checksum = *config.checksum_length() != 0;
5858
let prefix = KeyPrefix::new(prefix)?;
59-
let generator = KeyGenerator::new(prefix, config);
60-
let validator = KeyValidator::new(&hash_config, include_checksum)?;
61-
let hasher = KeyHasher::new(hash_config);
59+
let generator = KeyGenerator::new(prefix, config)?;
60+
let hasher = KeyHasher::new(hash_config.clone());
61+
62+
// Generate dummy key and its hash for timing attack protection
63+
let dummy_key = generator.dummy_key().clone();
64+
let dummy_hash = hasher.hash(&dummy_key)?;
65+
66+
let validator = KeyValidator::new(&hash_config, include_checksum, dummy_key, dummy_hash)?;
6267

6368
Ok(Self {
6469
generator,
@@ -68,14 +73,12 @@ impl ApiKeyManagerV0 {
6873
})
6974
}
7075

71-
pub fn init_default_config(
72-
prefix: impl Into<String>,
73-
) -> std::result::Result<Self, ConfigError> {
76+
pub fn init_default_config(prefix: impl Into<String>) -> std::result::Result<Self, InitError> {
7477
Self::init(prefix, KeyConfig::default(), HashConfig::default())
7578
}
7679
pub fn init_high_security_config(
7780
prefix: impl Into<String>,
78-
) -> std::result::Result<Self, ConfigError> {
81+
) -> std::result::Result<Self, InitError> {
7982
Self::init(
8083
prefix,
8184
KeyConfig::high_security(),

crates/api-keys-simplified/src/error.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ pub enum OperationError {
8080

8181
pub type Result<T> = std::result::Result<T, Error>;
8282

83+
/// Error type for initialization operations that can fail with either
84+
/// configuration errors or runtime operation errors.
85+
#[derive(Debug, Error)]
86+
pub enum InitError {
87+
/// Configuration error during initialization
88+
#[error(transparent)]
89+
Config(#[from] ConfigError),
90+
91+
/// Operation error during initialization (i.e., dummy key generation)
92+
#[error(transparent)]
93+
Operation(#[from] Error),
94+
}
95+
8396
#[cfg(test)]
8497
mod tests {
8598
use super::*;

crates/api-keys-simplified/src/generator.rs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,38 @@ use zeroize::Zeroizing;
1313
// Prevent DoS: Validate input length before processing
1414
const MAX_KEY_LENGTH: usize = 512;
1515
const CHECKSUM_SEPARATOR: u8 = b'.';
16-
static DUMMY_KEY: &str = "dummy_key_for_timing_protection";
1716

1817
#[derive(Clone)]
1918
pub struct KeyGenerator {
2019
prefix: KeyPrefix,
2120
config: KeyConfig,
21+
/// Dummy key for timing attack protection
22+
dummy_key: SecureString,
2223
}
2324

2425
impl KeyGenerator {
25-
pub fn new(prefix: KeyPrefix, config: KeyConfig) -> KeyGenerator {
26-
Self { prefix, config }
26+
pub fn new(prefix: KeyPrefix, config: KeyConfig) -> Result<KeyGenerator> {
27+
// Generate a dummy key for timing attack protection
28+
// This is used in verify_checksum when input is invalid
29+
let dummy_generator = Self {
30+
prefix: prefix.clone(),
31+
config: config.clone(),
32+
dummy_key: SecureString::from(String::new()), // Temporary placeholder
33+
};
34+
35+
let dummy_key = dummy_generator.generate(Environment::Production, None)?;
36+
37+
Ok(Self {
38+
prefix,
39+
config,
40+
dummy_key,
41+
})
42+
}
43+
44+
/// Returns a reference to the dummy key for timing attack protection.
45+
/// This is used by KeyValidator to perform dummy work.
46+
pub(crate) fn dummy_key(&self) -> &SecureString {
47+
&self.dummy_key
2748
}
2849

2950
fn generate_key(&self) -> Result<Zeroizing<Vec<u8>>> {
@@ -153,7 +174,7 @@ impl KeyGenerator {
153174
if key_bytes.len() > MAX_KEY_LENGTH {
154175
// Perform fake work to prevent timing side-channel attacks
155176
// This ensures rejection takes similar time as actual verification
156-
let _ = self.compute_checksum(DUMMY_KEY, None);
177+
let _ = self.compute_checksum(self.dummy_key.expose_secret(), None);
157178
return Err(Error::InvalidFormat);
158179
}
159180

@@ -163,7 +184,7 @@ impl KeyGenerator {
163184
Ok((_, parts)) => parts,
164185
Err(_) => {
165186
// If parsing fails, perform dummy work for timing consistency
166-
let _ = self.compute_checksum(DUMMY_KEY, None);
187+
let _ = self.compute_checksum(self.dummy_key.expose_secret(), None);
167188
return Ok(false);
168189
}
169190
};
@@ -172,15 +193,15 @@ impl KeyGenerator {
172193
let checksum_bytes = match parts.checksum {
173194
Some(c) => c,
174195
None => {
175-
let _ = self.compute_checksum(DUMMY_KEY, None);
196+
let _ = self.compute_checksum(self.dummy_key.expose_secret(), None);
176197
return Ok(false);
177198
}
178199
};
179200

180201
let computed = match self.compute_checksum(parts.key, parts.expiry_b64) {
181202
Some(computed) => computed,
182203
None => {
183-
let _ = self.compute_checksum(DUMMY_KEY, None);
204+
let _ = self.compute_checksum(self.dummy_key.expose_secret(), None);
184205
return Ok(false);
185206
}
186207
};
@@ -285,7 +306,7 @@ mod tests {
285306
let config = KeyConfig::default();
286307
let checksum_len = *config.checksum_length();
287308

288-
let generator = KeyGenerator::new(prefix, config);
309+
let generator = KeyGenerator::new(prefix, config).unwrap();
289310
let key = generator.generate(env, None).unwrap();
290311
assert!(key.expose_secret().starts_with("sk-live-"));
291312

@@ -332,7 +353,7 @@ mod tests {
332353
let env = Environment::Test;
333354
let config = KeyConfig::default();
334355

335-
let generator = KeyGenerator::new(prefix, config);
356+
let generator = KeyGenerator::new(prefix, config).unwrap();
336357
let key = generator.generate(env, None).unwrap();
337358

338359
// Verify checksum is separated by '.' (enabled by default)
@@ -375,11 +396,11 @@ mod tests {
375396
let env = Environment::Development;
376397

377398
let config16 = KeyConfig::new().with_entropy(16).unwrap();
378-
let generator16 = KeyGenerator::new(prefix.clone(), config16);
399+
let generator16 = KeyGenerator::new(prefix.clone(), config16).unwrap();
379400
let key16 = generator16.generate(env.clone(), None).unwrap();
380401

381402
let config32 = KeyConfig::new().with_entropy(32).unwrap();
382-
let generator32 = KeyGenerator::new(prefix, config32);
403+
let generator32 = KeyGenerator::new(prefix, config32).unwrap();
383404
let key32 = generator32.generate(env, None).unwrap();
384405

385406
assert!(key32.len() > key16.len());
@@ -392,7 +413,7 @@ mod tests {
392413
let config = KeyConfig::default();
393414
let checksum_len = *config.checksum_length();
394415

395-
let generator = KeyGenerator::new(prefix, config);
416+
let generator = KeyGenerator::new(prefix, config).unwrap();
396417
let key = generator.generate(env, None).unwrap();
397418

398419
// With dash separator and checksum (default): test-live-data.checksum
@@ -432,15 +453,15 @@ mod tests {
432453

433454
// Test with Slash
434455
let config_slash = KeyConfig::default().with_separator(Separator::Slash);
435-
let generator_slash = KeyGenerator::new(prefix.clone(), config_slash);
456+
let generator_slash = KeyGenerator::new(prefix.clone(), config_slash).unwrap();
436457
let key_slash = generator_slash.generate(env.clone(), None).unwrap();
437458
assert!(key_slash.expose_secret().contains('/'));
438459
assert!(!key_slash.expose_secret().contains('~'));
439460
assert!(generator_slash.verify_checksum(&key_slash).unwrap());
440461

441462
// Test with Dash (default)
442463
let config_dash = KeyConfig::default().with_separator(Separator::Dash);
443-
let generator_dash = KeyGenerator::new(prefix.clone(), config_dash);
464+
let generator_dash = KeyGenerator::new(prefix.clone(), config_dash).unwrap();
444465
let key_dash = generator_dash.generate(env.clone(), None).unwrap();
445466
assert!(key_dash.expose_secret().contains('-'));
446467
// Checksum is always separated by dot
@@ -450,7 +471,7 @@ mod tests {
450471

451472
// Test with Tilde
452473
let config_tilde = KeyConfig::default().with_separator(Separator::Tilde);
453-
let generator_tilde = KeyGenerator::new(prefix, config_tilde);
474+
let generator_tilde = KeyGenerator::new(prefix, config_tilde).unwrap();
454475
let key_tilde = generator_tilde.generate(env, None).unwrap();
455476
assert!(key_tilde.expose_secret().contains('~'));
456477
assert!(key_tilde.len() > 10);

crates/api-keys-simplified/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub use config::{
4444
ChecksumAlgo, Environment, HashConfig, KeyConfig, KeyPrefix, KeyVersion, Separator,
4545
};
4646
pub use domain::{ApiKey, ApiKeyManagerV0, Hash, NoHash};
47-
pub use error::{ConfigError, Error, Result};
47+
pub use error::{ConfigError, Error, InitError, Result};
4848
pub use secure::{SecureString, SecureStringExt};
4949
pub use validator::KeyStatus;
5050

crates/api-keys-simplified/src/validator.rs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::error::{ConfigError, Error, Result};
22
use crate::token_parser::{parse_token, Parts};
3-
use crate::HashConfig;
3+
use crate::SecureString;
44
use argon2::{
55
password_hash::{PasswordHash, PasswordVerifier},
66
Argon2,
@@ -13,6 +13,8 @@ use password_hash::PasswordHashString;
1313
pub struct KeyValidator {
1414
hash: PasswordHashString,
1515
has_checksum: bool,
16+
/// Dummy password for timing attack protection (should be a generated API key)
17+
dummy_password: SecureString,
1618
}
1719

1820
/// Represents the status of an API key after verification
@@ -31,14 +33,19 @@ impl KeyValidator {
3133
const MAX_HASH_LENGTH: usize = 512;
3234

3335
pub fn new(
34-
hash_config: &HashConfig,
36+
_hash_config: &crate::HashConfig,
3537
has_checksum: bool,
38+
dummy_key: SecureString,
39+
dummy_hash: String,
3640
) -> std::result::Result<KeyValidator, ConfigError> {
37-
let dummy_hash = format!("$argon2id$v=19$m={},t={},p={}$0bJKH8iokgID0PWXnrsXvw$oef42xfOKBQMkCpvoQTeVHLhsYf+EQWMc2u4Ebn1MUo", hash_config.memory_cost(), hash_config.time_cost(), hash_config.parallelism());
3841
let hash =
3942
PasswordHashString::new(&dummy_hash).map_err(|_| ConfigError::InvalidArgon2Hash)?;
4043

41-
Ok(KeyValidator { hash, has_checksum })
44+
Ok(KeyValidator {
45+
hash,
46+
has_checksum,
47+
dummy_password: dummy_key,
48+
})
4249
}
4350

4451
fn verify_expiry(&self, parts: Parts) -> Result<KeyStatus> {
@@ -113,12 +120,12 @@ impl KeyValidator {
113120
// SECURITY: Perform dummy Argon2 verification to match timing of real verification
114121
// This prevents timing attacks that could distinguish between "invalid hash format"
115122
// and "valid hash but wrong password" errors
116-
let dummy_password =
117-
b"text-v1-test-okphUY-aqllb-qHoZDC9mVlm5sY9lvmm.AAAAAGk2Mvg.a54368d6331bf42dc18c";
118-
parse_token(dummy_password, self.has_checksum).ok();
123+
use crate::ExposeSecret;
124+
let dummy_bytes = self.dummy_password.expose_secret().as_bytes();
125+
parse_token(dummy_bytes, self.has_checksum).ok();
119126

120127
Argon2::default()
121-
.verify_password(dummy_password, &self.hash.password_hash())
128+
.verify_password(dummy_bytes, &self.hash.password_hash())
122129
.ok();
123130
}
124131
}
@@ -129,13 +136,22 @@ mod tests {
129136
use crate::ExposeSecret;
130137
use crate::{config::HashConfig, hasher::KeyHasher, SecureString};
131138

139+
fn dummy_key_and_hash() -> (SecureString, String) {
140+
let key = SecureString::from("sk-live-dummy123test".to_string());
141+
let hasher = KeyHasher::new(HashConfig::default());
142+
let hash = hasher.hash(&key).unwrap();
143+
(key, hash)
144+
}
145+
132146
#[test]
133147
fn test_verification() {
134148
let key = SecureString::from("sk_live_testkey123".to_string());
135149
let hasher = KeyHasher::new(HashConfig::default());
136150
let hash = hasher.hash(&key).unwrap();
137151

138-
let validator = KeyValidator::new(&HashConfig::default(), true).unwrap();
152+
let (dummy_key, dummy_hash) = dummy_key_and_hash();
153+
let validator =
154+
KeyValidator::new(&HashConfig::default(), true, dummy_key, dummy_hash).unwrap();
139155
assert_eq!(
140156
validator
141157
.verify(key.expose_secret(), hash.as_ref())
@@ -150,7 +166,9 @@ mod tests {
150166

151167
#[test]
152168
fn test_invalid_hash_format() {
153-
let validator = KeyValidator::new(&HashConfig::default(), true).unwrap();
169+
let (dummy_key, dummy_hash) = dummy_key_and_hash();
170+
let validator =
171+
KeyValidator::new(&HashConfig::default(), true, dummy_key, dummy_hash).unwrap();
154172
let result = validator.verify("any_key", "invalid_hash");
155173
// After timing oracle fix: invalid hash format returns Ok(Invalid) instead of Err
156174
// to prevent timing-based user enumeration attacks
@@ -165,7 +183,9 @@ mod tests {
165183
let hasher = KeyHasher::new(HashConfig::default());
166184
let hash = hasher.hash(&valid_key).unwrap();
167185

168-
let validator = KeyValidator::new(&HashConfig::default(), true).unwrap();
186+
let (dummy_key, dummy_hash) = dummy_key_and_hash();
187+
let validator =
188+
KeyValidator::new(&HashConfig::default(), true, dummy_key, dummy_hash).unwrap();
169189
let result = validator.verify(&oversized_key, hash.as_ref());
170190
assert!(result.is_err());
171191
assert!(matches!(result.unwrap_err(), Error::InvalidFormat));
@@ -175,7 +195,9 @@ mod tests {
175195
fn test_oversized_hash_rejection() {
176196
let oversized_hash = "a".repeat(513); // Exceeds MAX_HASH_LENGTH
177197

178-
let validator = KeyValidator::new(&HashConfig::default(), true).unwrap();
198+
let (dummy_key, dummy_hash) = dummy_key_and_hash();
199+
let validator =
200+
KeyValidator::new(&HashConfig::default(), true, dummy_key, dummy_hash).unwrap();
179201
let result = validator.verify("valid_key", &oversized_hash);
180202
assert!(result.is_err());
181203
assert!(matches!(result.unwrap_err(), Error::InvalidFormat));
@@ -187,7 +209,9 @@ mod tests {
187209
let hasher = KeyHasher::new(HashConfig::default());
188210
let hash = hasher.hash(&valid_key).unwrap();
189211

190-
let validator = KeyValidator::new(&HashConfig::default(), true).unwrap();
212+
let (dummy_key, dummy_hash) = dummy_key_and_hash();
213+
let validator =
214+
KeyValidator::new(&HashConfig::default(), true, dummy_key, dummy_hash).unwrap();
191215

192216
// Test at boundary (512 chars - should pass)
193217
let max_key = "a".repeat(512);
@@ -207,7 +231,9 @@ mod tests {
207231
let hasher = KeyHasher::new(HashConfig::default());
208232
let valid_hash = hasher.hash(&valid_key).unwrap();
209233

210-
let validator = KeyValidator::new(&HashConfig::default(), true).unwrap();
234+
let (dummy_key, dummy_hash) = dummy_key_and_hash();
235+
let validator =
236+
KeyValidator::new(&HashConfig::default(), true, dummy_key, dummy_hash).unwrap();
211237

212238
let result1 = validator.verify("wrong_key", valid_hash.as_ref());
213239
assert!(result1.is_ok());

0 commit comments

Comments
 (0)