crates/veilvoice-crypto/src/lib.rs

what this file is for · veilvoice-crypto · 241 lines · the same file on GitHub

The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.


// SPDX-License-Identifier: GPL-3.0-or-later
//! # veilvoice-crypto
//!
//! Key derivation, post-quantum-hybrid key agreement, authenticated encryption
//! and amnesic secret storage for VeilVoice.
//!
//! ## What this crate is for
//!
//! [`veilvoice_core`](../veilvoice_core/index.html) makes a voice
//! unrecognisable; it does not hide the *words*, and it is not meant to. When a
//! recording needs to stay secret as well, at rest on disk or in transit to
//! someone else, that is this crate's job.
//!
//! - [`kdf`], Argon2id, for turning a password into a key.
//! - [`hybrid`], X25519 + ML-KEM-768, so a recording captured today is not
//!   readable by a quantum adversary tomorrow.
//! - [`aead`], XChaCha20-Poly1305, with random nonces and authenticated
//!   associated data.
//! - [`container`], the `.veil` file format that ties the three together.
//! - [`amnesia`], page-locked, zeroizing, constant-time-comparable secrets.
//! - [`shred`], secure erasure, and an honest account of what that is worth
//!   on flash storage.
//! - [`privatefile`], writing a file that is owner-only from the moment it
//!   exists, rather than world-readable until a second syscall tightens it.
//! - [`lock`], the application lock: an Argon2id verifier with a rate limit,
//!   which protects against casual access and says so rather than pretending to
//!   be tamper-proof.
//!
//! ## Threat model, stated plainly
//!
//! This crate protects data **at rest and in transit** against an attacker who
//! later obtains the file, including one who stores it until quantum hardware
//! exists. It does **not** protect against an attacker who is already running
//! code as you, or who can read this process's memory: page-locking keeps keys
//! out of the swap file, not out of a debugger. Hibernation writes RAM to disk
//! wholesale and defeats locking entirely.
//!
//! ## Example
//!
//! ```
//! use veilvoice_crypto::{container, kdf};
//!
//! # fn main() -> Result<(), veilvoice_crypto::Error> {
//! // Cheap parameters so the doctest is fast; real callers use the default.
//! let params = kdf::KdfParams::weak_for_tests();
//! let sealed = container::seal_with_password(b"pass phrase", b"audio bytes", params)?;
//! assert_eq!(container::open_with_password(b"pass phrase", &sealed)?, b"audio bytes");
//! assert!(container::open_with_password(b"wrong", &sealed).is_err());
//! # Ok(())
//! # }
//! ```
//!
//! VeilVoice contains **no `unsafe` code at all**, including the page-locking
//! in [`amnesia`], which goes through a safe wrapper.
//!
//! # In plain words
//!
//! This is the locking.
//!
//! Two separate things use it. Recordings are sealed on your disk so that somebody
//! who takes the disk cannot listen to them, and the app can be put behind a
//! password so somebody who picks up your unlocked computer cannot open it.
//!
//! Those two passwords are deliberately different. Opening the program should not
//! be the same act as unsealing everything it has ever written.
//!
//! The maths is chosen to be slow to guess and to still be safe if somebody one
//! day builds a quantum computer. Nothing is sent anywhere; the sealing happens on
//! your machine and the key is made from your password each time.
#![forbid(unsafe_code)]
#![warn(missing_docs)]

pub mod aead;
pub mod amnesia;
pub mod container;
pub mod decoy;
pub mod hoard;
pub mod hybrid;
pub mod kdf;
pub mod lock;
pub mod privatefile;
pub mod shred;
pub mod studio;
pub mod tape;
pub mod vault;
pub mod weave;

pub use amnesia::Secret;
pub use lock::{AppLock, LockStore};
pub use shred::{shred_file, Passes, ShredReport};
pub use studio::StudioKey;
pub use tape::Tape;
pub use vault::Vault;


/// Crate version string, surfaced in the About panel.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");



/// Everything that can go wrong in this crate.
///
/// Decryption failures are deliberately coarse: [`Error::Decrypt`] does not say
/// *why* authentication failed, because distinguishing a wrong password from a
/// corrupt tag would hand an attacker an oracle.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
    /// The OS random number generator was unavailable.
    Random,
    /// Argon2 rejected the cost parameters, or the salt was too short.
    KdfParams,
    /// The declared memory cost is legal but above the ceiling this caller set.
    /// See [`kdf::KdfParams::within`].
    KdfCostRefused {
        /// The memory cost the file asked for, in KiB.
        requested: u32,
        /// The ceiling the caller allowed, in KiB.
        ceiling: u32,
    },
    /// Key derivation failed.
    Kdf,
    /// A key was not the expected length.
    KeyLength,
    /// Encryption failed.
    Encrypt,
    /// Decryption or authentication failed.
    Decrypt,
    /// KEM encapsulation failed.
    Encapsulate,
    /// KEM decapsulation failed.
    Decapsulate,
    /// A public key or encapsulation was malformed.
    BadKeyEncoding,
    /// The data does not start with the container magic.
    BadMagic,
    /// The container header is structurally invalid.
    BadHeader,
    /// The container ended sooner than its header promised.
    Truncated,
    /// The container uses a format version this build does not support.
    UnsupportedVersion(u8),
    /// The container uses an unknown locking mode.
    UnsupportedMode(u8),
    /// The container is locked a different way than the call assumed.
    WrongMode,
    /// A file could not be securely erased.
    Shred,
    /// The path named for erasure is a symbolic link. Following it would
    /// overwrite whatever it points at while deleting only the link, so it is
    /// refused rather than obeyed.
    ShredSymlink,
    /// The app-lock password was wrong.
    AppLockRejected,
    /// Too many wrong app-lock attempts. The payload is the number of seconds
    /// still to wait before another attempt will be considered.
    AppLockCooldown(u64),
    /// The app-lock file could not be read, written or removed.
    AppLockStore,
    /// A lock is already set here, so creating one was refused.
    ///
    /// Its own variant because it was reported as [`Self::AppLockStore`], and
    /// that message -- "could not read or write the app-lock file" -- reads as
    /// a broken installation rather than as "you already have one of these".
    /// A user who saw it had no way to tell a refusal from a failure, which is
    /// half of what made F-141 so confusing to hit.
    AppLockExists,
    /// The password was changed, but the second copy of the lock could not be
    /// updated, so it still holds the previous one.
    AppLockSpareStale,
    /// A [`tape::Tape`] was asked to copy itself into a buffer that is not its
    /// own length. Refused rather than part-filled: a recording written into a
    /// buffer sized for a different one is a truncated file nothing would flag.
    TapeLength,
    /// A studio vault key was asked for with one of its two secrets missing.
    /// Refused rather than derived from the one that was present: silently
    /// falling back to a single secret is the failure that construction exists
    /// to prevent.
    StudioNeedsBoth,
    /// A studio vault was asked to change a recording that is not in it.
    /// An error rather than a silent no-operation: somebody renaming a
    /// recording that is not there has the wrong identifier, and being told so
    /// is more use than appearing to succeed.
    NoSuchTake,
}


impl std::fmt::Display for Error {

    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let msg = match self {
            Self::Random => "the operating system random number generator is unavailable",
            Self::KdfParams => "invalid key-derivation parameters",
            Self::KdfCostRefused { requested, ceiling } => {
                return write!(
                    f,
                    "this file asks for {requested} KiB of memory to open, above the \
                     {ceiling} KiB ceiling this caller allows"
                )
            }
            Self::Kdf => "key derivation failed",
            Self::KeyLength => "key has the wrong length",
            Self::Encrypt => "encryption failed",
            Self::Decrypt => "decryption failed: wrong key, or the data was altered",
            Self::Encapsulate => "key encapsulation failed",
            Self::Decapsulate => "key decapsulation failed",
            Self::BadKeyEncoding => "malformed key or encapsulation",
            Self::BadMagic => "not a VeilVoice container",
            Self::BadHeader => "malformed container header",
            Self::Truncated => "container is truncated",
            Self::UnsupportedVersion(v) => return write!(f, "unsupported container version {v}"),
            Self::UnsupportedMode(m) => return write!(f, "unsupported container mode {m}"),
            Self::WrongMode => "container is locked with a different method",
            Self::Shred => "could not securely erase the file",
            Self::ShredSymlink => {
                "that path is a symbolic link, so erasing it would destroy whatever it \
                 points at and delete only the link; name the real file instead"
            }
            Self::AppLockRejected => "wrong app-lock password",
            Self::AppLockCooldown(secs) => {
                return write!(f, "too many attempts, so wait {secs}s before trying again")
            }
            Self::AppLockStore => "could not read or write the app-lock file",
            Self::AppLockExists => {
                "an app lock is already set on this machine. Change its password \
                 from the security tab, or remove it there first"
            }
            Self::AppLockSpareStale => {
                "the password was changed here, but the administrator-owned copy of the lock \
                 still holds the previous one. Run VeilVoice as an administrator once to \
                 finish the change."
            }
            Self::TapeLength => {
                "the buffer offered for the recording is not the length of the recording"
            }
            Self::StudioNeedsBoth => {
                "the studio vault needs both the app lock and the at-rest passphrase, and \
                 only one was given"
            }
            Self::NoSuchTake => "there is no recording with that identifier in this vault",
        };
        f.write_str(msg)
    }

}

impl std::error::Error for Error {}