Skip to main content

authy/audit/
mod.rs

1use hmac::{Hmac, Mac};
2use serde::{Deserialize, Serialize};
3use sha2::Sha256;
4use std::fs::{self, OpenOptions};
5use std::io::{BufRead, BufReader, Write};
6use std::path::Path;
7
8use crate::error::{AuthyError, Result};
9use crate::types::*;
10
11type HmacSha256 = Hmac<Sha256>;
12
13/// A single audit log entry.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct AuditEntry {
16    pub timestamp: DateTime<Utc>,
17    pub operation: String,
18    pub secret: Option<String>,
19    pub actor: String,
20    pub outcome: String,
21    pub detail: Option<String>,
22    pub chain_hmac: String,
23}
24
25/// Append an audit entry to the log file.
26pub fn log_event(
27    audit_path: &Path,
28    operation: &str,
29    secret: Option<&str>,
30    actor: &str,
31    outcome: &str,
32    detail: Option<&str>,
33    hmac_key: &[u8],
34) -> Result<()> {
35    let prev_hmac = read_last_hmac(audit_path);
36
37    let entry = AuditEntry {
38        timestamp: Utc::now(),
39        operation: operation.to_string(),
40        secret: secret.map(|s| s.to_string()),
41        actor: actor.to_string(),
42        outcome: outcome.to_string(),
43        detail: detail.map(|s| s.to_string()),
44        chain_hmac: String::new(), // Will be filled below
45    };
46
47    // Compute HMAC chain: HMAC(prev_hmac || serialized_entry_without_chain)
48    let chain_data = format!(
49        "{}|{}|{}|{:?}|{}|{}|{:?}",
50        prev_hmac,
51        entry.timestamp.to_rfc3339(),
52        entry.operation,
53        entry.secret,
54        entry.actor,
55        entry.outcome,
56        entry.detail,
57    );
58
59    let chain_hmac = compute_chain_hmac(&chain_data, hmac_key);
60
61    let final_entry = AuditEntry {
62        chain_hmac,
63        ..entry
64    };
65
66    let json_line =
67        serde_json::to_string(&final_entry).map_err(|e| AuthyError::Serialization(e.to_string()))?;
68
69    if let Some(dir) = audit_path.parent() {
70        fs::create_dir_all(dir)?;
71    }
72
73    let mut file = OpenOptions::new()
74        .create(true)
75        .append(true)
76        .open(audit_path)?;
77    writeln!(file, "{}", json_line)?;
78
79    Ok(())
80}
81
82/// Read all audit entries from the log file.
83pub fn read_entries(audit_path: &Path) -> Result<Vec<AuditEntry>> {
84    if !audit_path.exists() {
85        return Ok(Vec::new());
86    }
87
88    let file = fs::File::open(audit_path)?;
89    let reader = BufReader::new(file);
90    let mut entries = Vec::new();
91
92    for line in reader.lines() {
93        let line = line?;
94        if line.trim().is_empty() {
95            continue;
96        }
97        let entry: AuditEntry =
98            serde_json::from_str(&line).map_err(|e| AuthyError::Serialization(e.to_string()))?;
99        entries.push(entry);
100    }
101
102    Ok(entries)
103}
104
105/// Verify the HMAC chain integrity of the audit log.
106pub fn verify_chain(audit_path: &Path, hmac_key: &[u8]) -> Result<(usize, bool)> {
107    let entries = read_entries(audit_path)?;
108    let mut prev_hmac = String::new();
109
110    for (i, entry) in entries.iter().enumerate() {
111        let chain_data = format!(
112            "{}|{}|{}|{:?}|{}|{}|{:?}",
113            prev_hmac,
114            entry.timestamp.to_rfc3339(),
115            entry.operation,
116            entry.secret,
117            entry.actor,
118            entry.outcome,
119            entry.detail,
120        );
121
122        let expected_hmac = compute_chain_hmac(&chain_data, hmac_key);
123        if expected_hmac != entry.chain_hmac {
124            return Err(AuthyError::AuditChainBroken(i));
125        }
126        prev_hmac = entry.chain_hmac.clone();
127    }
128
129    Ok((entries.len(), true))
130}
131
132fn read_last_hmac(audit_path: &Path) -> String {
133    if !audit_path.exists() {
134        return String::new();
135    }
136
137    // Read the file and get the last non-empty line
138    if let Ok(content) = fs::read_to_string(audit_path) {
139        for line in content.lines().rev() {
140            if !line.trim().is_empty() {
141                if let Ok(entry) = serde_json::from_str::<AuditEntry>(line) {
142                    return entry.chain_hmac;
143                }
144            }
145        }
146    }
147
148    String::new()
149}
150
151fn compute_chain_hmac(data: &str, hmac_key: &[u8]) -> String {
152    let mut mac = HmacSha256::new_from_slice(hmac_key).expect("HMAC can take key of any size");
153    mac.update(data.as_bytes());
154    hex::encode(mac.finalize().into_bytes())
155}
156
157/// Derive the audit HMAC key from the master key material.
158pub fn derive_audit_key(master_material: &[u8]) -> Vec<u8> {
159    crate::vault::crypto::derive_key(master_material, b"audit-hmac", 32)
160}
161
162/// Get the master material from a VaultKey (for HKDF derivation).
163pub fn key_material(key: &crate::vault::VaultKey) -> Vec<u8> {
164    match key {
165        crate::vault::VaultKey::Passphrase(p) => p.as_bytes().to_vec(),
166        crate::vault::VaultKey::Keyfile { identity, .. } => identity.as_bytes().to_vec(),
167    }
168}