fix: Phase 7 — key zeroization, OsRng, checked arithmetic, TOTP rate limits

- SecretsManager: raw key stored in Zeroizing<[u8; 32]>, auto-zeroed on drop
- SecretsManager: replaced thread_rng with OsRng (CSPRNG) for nonces
- Remember-me secret: derived from machine-id via SHA-256 (deterministic, no
  plaintext key storage)
- Bitcoin ecash balance: uses checked_add with u64::MAX saturation on overflow
- TOTP setup/confirm: added to EndpointRateLimiter (3 and 5 per 5min)
- AppId validation and Tor service name validation already existed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 01:00:57 +00:00
parent 12ae3af981
commit d1eb01799f
5 changed files with 37 additions and 16 deletions

View File

@@ -38,6 +38,7 @@ pub struct ExpiringSecret {
pub struct SecretsManager {
secrets_dir: PathBuf,
cipher: Aes256Gcm,
raw_key: zeroize::Zeroizing<[u8; 32]>,
}
impl SecretsManager {
@@ -49,11 +50,14 @@ impl SecretsManager {
"Encryption key must be exactly 32 bytes (256 bits), got {}",
encryption_key.len()
);
let cipher = Aes256Gcm::new_from_slice(&encryption_key)
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&encryption_key);
let cipher = Aes256Gcm::new_from_slice(&key_array)
.map_err(|e| anyhow::anyhow!("Failed to create cipher: {}", e))?;
Ok(Self {
secrets_dir,
cipher,
raw_key: zeroize::Zeroizing::new(key_array),
})
}
@@ -61,7 +65,7 @@ impl SecretsManager {
/// Returns: MAGIC (10 bytes) + nonce (12 bytes) + ciphertext (variable)
fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
let mut nonce_bytes = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = self
@@ -220,7 +224,7 @@ impl SecretsManager {
) -> Result<String> {
// Generate a new random secret (32 bytes, hex-encoded = 64 chars)
let mut new_secret_bytes = [0u8; 32];
rand::thread_rng().fill_bytes(&mut new_secret_bytes);
rand::rngs::OsRng.fill_bytes(&mut new_secret_bytes);
let new_value = hex::encode(new_secret_bytes);
let secret_path = self