use std::sync::Arc;
use cratefield_core::{Database, Statement};
use sea_query::Value as SeaValue;
use sha2::{Digest, Sha256};
use crate::{Access, Audit, AuditEvent, SecretsError, StoreId};
const GENESIS: [u8; 32] = [0; 32];
pub struct ChainAudit {
db: Arc<dyn Database>,
store: StoreId,
}
impl ChainAudit {
#[must_use]
pub fn new(store: StoreId, db: Arc<dyn Database>) -> Self {
Self { db, store }
}
async fn tail(&self) -> Result<(i64, Vec<u8>), SecretsError> {
let rows = self
.db
.query(&Statement::new(
"SELECT seq, hash FROM harness_secret_audit ORDER BY seq DESC LIMIT 1",
))
.await?;
match rows.first() {
Some(row) => {
let seq: i64 = row.get("seq").ok_or_else(|| {
SecretsError::NotAudited("an audit row has no seq".to_owned())
})?;
let hash: Vec<u8> = row.get("hash").ok_or_else(|| {
SecretsError::NotAudited("an audit row has no hash".to_owned())
})?;
Ok((seq, hash))
}
None => Ok((0, GENESIS.to_vec())),
}
}
}
#[async_trait::async_trait]
impl Audit for ChainAudit {
async fn record(&self, event: &AuditEvent<'_>) -> Result<(), SecretsError> {
let (last_seq, prev_hash) = self.tail().await?;
let seq = last_seq.saturating_add(1);
let ts = now();
let name = event.name.unwrap_or("");
let entry = Entry {
seq,
ts: &ts,
actor: event.actor.as_str(),
name,
version: event.version,
action: event.access,
allowed: event.allowed,
request_id: event.request_id,
};
let hash = entry.hash(&prev_hash);
self.db
.execute(&Statement::with_values(
"INSERT INTO harness_secret_audit \
(seq, ts, actor, name, version, action, allowed, request_id, prev_hash, hash) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
vec![
SeaValue::BigInt(Some(seq)),
text(&ts),
text(event.actor.as_str()),
text(name),
match event.version {
Some(version) => SeaValue::BigInt(Some(i64::from(version))),
None => SeaValue::BigInt(None),
},
text(event.access.as_str()),
SeaValue::BigInt(Some(i64::from(event.allowed))),
match event.request_id {
Some(id) => text(id),
None => SeaValue::String(None),
},
bytes(prev_hash),
bytes(hash),
],
))
.await
.map_err(|err| SecretsError::NotAudited(err.to_string()))?;
let _ = &self.store;
Ok(())
}
}
struct Entry<'a> {
seq: i64,
ts: &'a str,
actor: &'a str,
name: &'a str,
version: Option<u32>,
action: Access,
allowed: bool,
request_id: Option<&'a str>,
}
impl Entry<'_> {
fn hash(&self, prev_hash: &[u8]) -> Vec<u8> {
fn push(hasher: &mut Sha256, bytes: &[u8]) {
hasher.update(u32::try_from(bytes.len()).unwrap_or(u32::MAX).to_le_bytes());
hasher.update(bytes);
}
let mut hasher = Sha256::new();
hasher.update(b"FZ-SECRET-AUDIT-v1");
hasher.update(prev_hash);
push(&mut hasher, &self.seq.to_le_bytes());
push(&mut hasher, self.ts.as_bytes());
push(&mut hasher, self.actor.as_bytes());
push(&mut hasher, self.name.as_bytes());
match self.version {
Some(version) => {
push(&mut hasher, b"v");
push(&mut hasher, &version.to_le_bytes());
}
None => push(&mut hasher, b"-"),
}
push(&mut hasher, self.action.as_str().as_bytes());
push(&mut hasher, &[u8::from(self.allowed)]);
push(&mut hasher, self.request_id.unwrap_or("").as_bytes());
hasher.finalize().to_vec()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Anchor {
pub store: String,
pub seq: i64,
pub hash: String,
}
pub async fn verify(store: &StoreId, db: &dyn Database) -> Result<Anchor, SecretsError> {
let rows = db
.query(&Statement::new(
"SELECT seq, ts, actor, name, version, action, allowed, request_id, prev_hash, hash \
FROM harness_secret_audit ORDER BY seq ASC",
))
.await?;
let mut prev_hash = GENESIS.to_vec();
let mut expected_seq: i64 = 1;
let mut last = Anchor {
store: store.to_string(),
seq: 0,
hash: hex(&GENESIS),
};
for row in &rows.rows {
let seq: i64 = row
.get("seq")
.ok_or_else(|| broken(store, expected_seq, "a row has no seq"))?;
if seq != expected_seq {
return Err(broken(
store,
seq,
&format!("expected seq {expected_seq}: a row is missing or out of order"),
));
}
let recorded_prev: Vec<u8> = row
.get("prev_hash")
.ok_or_else(|| broken(store, seq, "a row has no prev_hash"))?;
if recorded_prev != prev_hash {
return Err(broken(
store,
seq,
"its prev_hash is not the previous row's hash",
));
}
let ts: String = row
.get("ts")
.ok_or_else(|| broken(store, seq, "a row has no timestamp"))?;
let actor: String = row
.get("actor")
.ok_or_else(|| broken(store, seq, "a row has no actor"))?;
let name: String = row.get("name").unwrap_or_default();
let action: String = row
.get("action")
.ok_or_else(|| broken(store, seq, "a row has no action"))?;
let action = Access::parse(&action)
.ok_or_else(|| broken(store, seq, &format!("unknown action `{action}`")))?;
let version = row
.get::<i64>("version")
.and_then(|v| u32::try_from(v).ok());
let allowed = row.get::<i64>("allowed").unwrap_or_default() != 0;
let request_id: Option<String> = row.get("request_id");
let recorded_hash: Vec<u8> = row
.get("hash")
.ok_or_else(|| broken(store, seq, "a row has no hash"))?;
let computed = Entry {
seq,
ts: &ts,
actor: &actor,
name: &name,
version,
action,
allowed,
request_id: request_id.as_deref(),
}
.hash(&prev_hash);
if computed != recorded_hash {
return Err(broken(
store,
seq,
"its hash does not match its contents: the row was altered",
));
}
prev_hash = recorded_hash;
last = Anchor {
store: store.to_string(),
seq,
hash: hex(&prev_hash),
};
expected_seq = seq.saturating_add(1);
}
Ok(last)
}
fn broken(store: &StoreId, seq: i64, detail: &str) -> SecretsError {
SecretsError::ChainBroken {
store: store.to_string(),
seq,
detail: detail.to_owned(),
}
}
fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
}
out
}
fn text(value: &str) -> SeaValue {
SeaValue::String(Some(Box::new(value.to_owned())))
}
fn bytes(value: Vec<u8>) -> SeaValue {
SeaValue::Bytes(Some(Box::new(value)))
}
fn now() -> String {
time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default()
}