use std::time::Instant;
use crate::algorithms::derive_key;
use crate::error::Result;
use crate::helpers::{
buffer_starts_with, build_password, bytes_to_hex, canonical_json, constant_time_equal_hex,
elapsed_ms, hex_to_bytes, hmac_sign, random_bytes_16,
};
use crate::types::{
Challenge, ChallengeParameters, CreateChallengeOptions, HmacAlgorithm, Solution,
SolveChallengeOptions, VerifySolutionOptions, VerifySolutionResult,
};
pub fn create_challenge(options: CreateChallengeOptions) -> Result<Challenge> {
let key_prefix_length = options.key_prefix_length.unwrap_or(options.key_length / 2);
let nonce_hex = bytes_to_hex(&random_bytes_16());
let salt_hex = bytes_to_hex(&random_bytes_16());
let mut parameters = ChallengeParameters {
algorithm: options.algorithm,
cost: options.cost,
data: options.data,
expires_at: options.expires_at,
key_length: options.key_length,
key_prefix: options.key_prefix,
key_signature: None,
memory_cost: options.memory_cost,
nonce: nonce_hex,
parallelism: options.parallelism,
salt: salt_hex,
};
let derived_key_bytes: Option<Vec<u8>> = if let Some(counter) = options.counter {
let nonce_bytes = hex_to_bytes(¶meters.nonce)?;
let salt_bytes = hex_to_bytes(¶meters.salt)?;
let password = build_password(&nonce_bytes, counter);
let key = derive_key(¶meters, &salt_bytes, &password)?;
parameters.key_prefix = bytes_to_hex(&key[..key_prefix_length]);
Some(key)
} else {
None
};
if options.hmac_signature_secret.is_none() {
return Ok(Challenge {
parameters,
signature: None,
});
}
sign_challenge(
&options.hmac_algorithm,
&mut parameters,
derived_key_bytes.as_deref(),
options.hmac_signature_secret.as_deref().unwrap(),
options.hmac_key_signature_secret.as_deref(),
)
}
pub fn sign_challenge(
algorithm: &HmacAlgorithm,
parameters: &mut ChallengeParameters,
derived_key: Option<&[u8]>,
hmac_signature_secret: &str,
hmac_key_signature_secret: Option<&str>,
) -> Result<Challenge> {
if let (Some(key), Some(key_secret)) = (derived_key, hmac_key_signature_secret) {
let key_sig = hmac_sign(algorithm, key, key_secret)?;
parameters.key_signature = Some(bytes_to_hex(&key_sig));
}
let json = canonical_json(parameters)?;
let sig = hmac_sign(algorithm, json.as_bytes(), hmac_signature_secret)?;
Ok(Challenge {
parameters: parameters.clone(),
signature: Some(bytes_to_hex(&sig)),
})
}
pub fn solve_challenge(options: SolveChallengeOptions<'_>) -> Result<Option<Solution>> {
let params = &options.challenge.parameters;
let nonce_bytes = hex_to_bytes(¶ms.nonce)?;
let salt_bytes = hex_to_bytes(¶ms.salt)?;
let key_prefix_bytes: Option<Vec<u8>> = if params.key_prefix.len() % 2 == 0 {
Some(hex_to_bytes(¶ms.key_prefix)?)
} else {
None
};
let start = Instant::now();
let timeout = std::time::Duration::from_millis(options.timeout_ms);
let mut counter = options.counter_start;
loop {
if counter % 10 == 0 && start.elapsed() > timeout {
return Ok(None);
}
let password = build_password(&nonce_bytes, counter);
let derived = derive_key(params, &salt_bytes, &password)?;
let matched = match &key_prefix_bytes {
Some(prefix) => buffer_starts_with(&derived, prefix),
None => bytes_to_hex(&derived).starts_with(¶ms.key_prefix),
};
if matched {
return Ok(Some(Solution {
counter,
derived_key: bytes_to_hex(&derived),
time: Some(elapsed_ms(start)),
}));
}
counter = counter.wrapping_add(options.counter_step);
}
}
pub fn verify_solution(options: VerifySolutionOptions<'_>) -> Result<VerifySolutionResult> {
let start = Instant::now();
let challenge = options.challenge;
let solution = options.solution;
let params = &challenge.parameters;
if let Some(expires_at) = params.expires_at {
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
if now_secs > expires_at {
return Ok(VerifySolutionResult {
expired: true,
invalid_signature: None,
invalid_solution: None,
time: elapsed_ms(start),
verified: false,
});
}
}
let Some(challenge_sig) = &challenge.signature else {
return Ok(VerifySolutionResult {
expired: false,
invalid_signature: Some(true),
invalid_solution: None,
time: elapsed_ms(start),
verified: false,
});
};
let json = canonical_json(params)?;
let expected_sig = hmac_sign(
&options.hmac_algorithm,
json.as_bytes(),
&options.hmac_signature_secret,
)?;
if !constant_time_equal_hex(challenge_sig, &bytes_to_hex(&expected_sig)) {
return Ok(VerifySolutionResult {
expired: false,
invalid_signature: Some(true),
invalid_solution: None,
time: elapsed_ms(start),
verified: false,
});
}
if let (Some(key_sig), Some(key_secret)) =
(¶ms.key_signature, &options.hmac_key_signature_secret)
{
let derived_key_bytes = hex_to_bytes(&solution.derived_key)?;
let expected_key_sig =
hmac_sign(&options.hmac_algorithm, &derived_key_bytes, key_secret)?;
let valid = constant_time_equal_hex(key_sig, &bytes_to_hex(&expected_key_sig));
return Ok(VerifySolutionResult {
expired: false,
invalid_signature: Some(false),
invalid_solution: Some(!valid),
time: elapsed_ms(start),
verified: valid,
});
}
let nonce_bytes = hex_to_bytes(¶ms.nonce)?;
let salt_bytes = hex_to_bytes(¶ms.salt)?;
let password = build_password(&nonce_bytes, solution.counter);
let derived = derive_key(params, &salt_bytes, &password)?;
let derived_hex = bytes_to_hex(&derived);
let valid = constant_time_equal_hex(&derived_hex, &solution.derived_key);
Ok(VerifySolutionResult {
expired: false,
invalid_signature: Some(false),
invalid_solution: Some(!valid),
time: elapsed_ms(start),
verified: valid,
})
}