#![forbid(unsafe_code)]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use forensicnomicon::report::{Category, Evidence, Finding, Observation, Severity, Source};
use veracrypt::{Flavor, VolumeInfo};
pub const ANALYZER: &str = "veracrypt-forensic";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnomalyKind {
LegacyTrueCrypt,
HiddenVolumeDeclared {
size: u64,
},
CipherInventory {
prf: &'static str,
cipher: String,
offset: u64,
},
}
impl AnomalyKind {
#[must_use]
pub fn severity(&self) -> Severity {
match self {
AnomalyKind::LegacyTrueCrypt => Severity::Low,
AnomalyKind::HiddenVolumeDeclared { .. } => Severity::Medium,
AnomalyKind::CipherInventory { .. } => Severity::Info,
}
}
#[must_use]
pub fn code(&self) -> &'static str {
match self {
AnomalyKind::LegacyTrueCrypt => "VC-LEGACY-TRUECRYPT",
AnomalyKind::HiddenVolumeDeclared { .. } => "VC-HIDDEN-VOLUME-DECLARED",
AnomalyKind::CipherInventory { .. } => "VC-CIPHER-INVENTORY",
}
}
#[must_use]
pub fn category(&self) -> Category {
match self {
AnomalyKind::LegacyTrueCrypt | AnomalyKind::CipherInventory { .. } => {
Category::Provenance
}
AnomalyKind::HiddenVolumeDeclared { .. } => Category::Concealment,
}
}
#[must_use]
pub fn note(&self) -> String {
match self {
AnomalyKind::LegacyTrueCrypt => {
"volume is a legacy TrueCrypt (not VeraCrypt) container".to_string()
}
AnomalyKind::HiddenVolumeDeclared { size } => format!(
"outer header declares a hidden volume of {size} bytes (deniable-encryption indicator)"
),
AnomalyKind::CipherInventory {
prf,
cipher,
offset,
} => format!("PRF {prf}, cipher {cipher}, data area at byte {offset}"),
}
}
fn evidence(&self) -> Vec<Evidence> {
match self {
AnomalyKind::LegacyTrueCrypt => Vec::new(),
AnomalyKind::HiddenVolumeDeclared { size } => {
vec![evidence("hidden_size", size.to_string())]
}
AnomalyKind::CipherInventory {
prf,
cipher,
offset,
} => vec![
evidence("prf", (*prf).to_string()),
evidence("cipher", cipher.clone()),
evidence("encrypted_area_start", offset.to_string()),
],
}
}
}
fn evidence(field: &str, value: String) -> Evidence {
Evidence {
field: field.to_string(),
value,
location: None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Anomaly {
pub severity: Severity,
pub code: &'static str,
pub kind: AnomalyKind,
pub note: String,
}
impl Anomaly {
#[must_use]
pub fn new(kind: AnomalyKind) -> Self {
Anomaly {
severity: kind.severity(),
code: kind.code(),
note: kind.note(),
kind,
}
}
}
impl Observation for Anomaly {
fn severity(&self) -> Option<Severity> {
Some(self.severity)
}
fn code(&self) -> &'static str {
self.code
}
fn note(&self) -> String {
self.note.clone()
}
fn category(&self) -> Category {
self.kind.category()
}
fn evidence(&self) -> Vec<Evidence> {
self.kind.evidence()
}
}
#[must_use]
pub fn audit(info: &VolumeInfo, hidden_size: u64) -> Vec<Anomaly> {
let mut out = Vec::new();
if info.flavor == Flavor::TrueCrypt {
out.push(Anomaly::new(AnomalyKind::LegacyTrueCrypt));
}
if hidden_size != 0 {
out.push(Anomaly::new(AnomalyKind::HiddenVolumeDeclared {
size: hidden_size,
}));
}
out.push(Anomaly::new(AnomalyKind::CipherInventory {
prf: info.prf.name(),
cipher: info.cipher_display(),
offset: info.encrypted_area_start,
}));
out
}
#[must_use]
pub fn audit_findings(
info: &VolumeInfo,
hidden_size: u64,
scope: impl Into<String>,
) -> Vec<Finding> {
let source = Source {
analyzer: ANALYZER.to_string(),
scope: scope.into(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
};
audit(info, hidden_size)
.into_iter()
.map(|a| a.to_finding(source.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use veracrypt::{Cipher, Prf};
fn info(flavor: Flavor) -> VolumeInfo {
VolumeInfo {
flavor,
prf: Prf::Sha512,
ciphers: vec![Cipher::Serpent],
version: 5,
encrypted_area_start: 131_072,
encrypted_area_size: 36_864,
}
}
#[test]
fn veracrypt_clean_only_inventory() {
let a = audit(&info(Flavor::VeraCrypt), 0);
assert_eq!(a.len(), 1);
assert_eq!(a[0].code, "VC-CIPHER-INVENTORY");
assert_eq!(a[0].severity, Severity::Info);
}
#[test]
fn flags_truecrypt_and_hidden() {
let a = audit(&info(Flavor::TrueCrypt), 4096);
let codes: Vec<_> = a.iter().map(|x| x.code).collect();
assert!(codes.contains(&"VC-LEGACY-TRUECRYPT"));
assert!(codes.contains(&"VC-HIDDEN-VOLUME-DECLARED"));
}
#[test]
fn findings_carry_source_category_and_evidence() {
let findings = audit_findings(&info(Flavor::TrueCrypt), 4096, "container.vc");
assert_eq!(findings.len(), 3);
for f in &findings {
assert_eq!(f.source.analyzer, "veracrypt-forensic");
assert_eq!(f.source.scope, "container.vc");
assert!(f.source.version.is_some());
}
let hidden = findings
.iter()
.find(|f| f.code == "VC-HIDDEN-VOLUME-DECLARED")
.unwrap();
assert_eq!(hidden.category, Category::Concealment);
assert_eq!(hidden.severity, Some(Severity::Medium));
assert_eq!(hidden.evidence.len(), 1);
let inv = findings
.iter()
.find(|f| f.code == "VC-CIPHER-INVENTORY")
.unwrap();
assert_eq!(inv.category, Category::Provenance);
assert_eq!(inv.evidence.len(), 3);
let legacy = findings
.iter()
.find(|f| f.code == "VC-LEGACY-TRUECRYPT")
.unwrap();
assert!(legacy.evidence.is_empty());
}
}