Skip to main content

forensic_vfs/
encryption.rs

1//! [`EncryptionLayer`] — full-disk-encryption translation, a distinct layer between
2//! volume and filesystem.
3//!
4//! BitLocker/LUKS/FileVault sit between a volume and its filesystem; the resolver
5//! (in the engine) probes for them by on-disk header magic. Credentials are
6//! supplied at resolve time through an injected [`CredentialSource`], never stored
7//! in a [`crate::locator::Locator`], so a serialized address never leaks keys.
8
9use crate::error::VfsResult;
10use crate::source::DynSource;
11
12/// The FDE scheme.
13#[non_exhaustive]
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum EncryptionScheme {
16    Bitlocker,
17    Luks1,
18    Luks2,
19    FileVault,
20    ApfsEncrypted,
21    /// VeraCrypt / TrueCrypt full-volume encryption (XTS, optional cipher cascade).
22    VeraCrypt,
23}
24
25/// One credential offered to a encryption layer.
26#[non_exhaustive]
27#[derive(Debug, Clone)]
28pub enum Credential {
29    Password(String),
30    /// BitLocker numeric recovery key or LUKS passphrase file contents.
31    RecoveryKey(String),
32    /// Raw key material (FVEK / volume master key).
33    KeyBytes(Vec<u8>),
34    /// A keyfile path the provider will read.
35    KeyFile(std::path::PathBuf),
36}
37
38/// Supplies credentials at resolve time. Injected into the resolve call, kept out
39/// of the serialized address, so a `Locator` is safe to persist and re-open
40/// (the caller re-supplies credentials on re-open).
41pub trait CredentialSource: Send + Sync {
42    /// Offer credentials for a target (a volume GUID / label / scheme name). An
43    /// empty slice means "none available" — the layer then errors
44    /// [`crate::error::VfsError::NeedCredentials`] rather than guessing.
45    fn credentials_for(&self, scheme: EncryptionScheme, target: &str) -> Vec<Credential>;
46}
47
48/// A [`CredentialSource`] that offers nothing — the secure-by-default credential
49/// context for a resolve that supplies no keys. It is the default threaded through
50/// the resolver's no-credential entry point, so an encrypted volume is never
51/// silently skipped nor guessed: a signature-detected scheme (BitLocker/LUKS/
52/// FileVault) surfaces [`crate::error::VfsError::NeedCredentials`] loudly, and a
53/// credential-attempt scheme (VeraCrypt) simply fails to decrypt and falls through
54/// (see ADR 0010).
55#[derive(Debug, Default, Clone, Copy)]
56pub struct NoCredentials;
57
58impl CredentialSource for NoCredentials {
59    fn credentials_for(&self, _scheme: EncryptionScheme, _target: &str) -> Vec<Credential> {
60        Vec::new()
61    }
62}
63
64/// An encryption translation over one [`crate::ImageSource`]: consumes credentials +
65/// ciphertext sectors, presents a decrypted [`DynSource`].
66pub trait EncryptionLayer: Send + Sync {
67    fn scheme(&self) -> EncryptionScheme;
68    /// Present the decrypted volume. Errs `NeedCredentials` if keys are absent,
69    /// `Decode` (loud, with the header bytes) on a bad key / unsupported cipher.
70    fn open(&self, creds: &dyn CredentialSource) -> VfsResult<DynSource>;
71
72    /// Findings raised while parsing the FDE header. Behind the `findings`
73    /// feature so a bare reader does not inherit forensicnomicon.
74    #[cfg(feature = "findings")]
75    fn findings(&self) -> VfsResult<Vec<forensicnomicon::report::Finding>> {
76        Ok(Vec::new())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::{Credential, CredentialSource, EncryptionScheme, NoCredentials};
83
84    #[test]
85    fn no_credentials_offers_nothing() {
86        // The secure-by-default context: no keys for any scheme/target, so a
87        // signature scheme surfaces NeedCredentials loud and a credential-attempt
88        // scheme simply fails to decrypt (see ADR 0010).
89        let creds = NoCredentials;
90        let none: Vec<Credential> = creds.credentials_for(EncryptionScheme::Bitlocker, "vol-guid");
91        assert!(none.is_empty());
92        assert!(creds
93            .credentials_for(EncryptionScheme::VeraCrypt, "")
94            .is_empty());
95    }
96}