secure-types 0.5.1

Secure data types that protect sensitive data in memory via locking and zeroization.
Documentation
//! A self-owned binary format for secrets, implemented as a
//! [`serde::Serializer`] and a [`serde::Deserializer`].
//!
//! # Why not JSON
//!
//! `serde_json` cannot be made to leave no traces, and the parts that leak are
//! out of our reach:
//!
//! - its `Deserializer` holds a private `scratch: Vec<u8>` that it reuses for
//!   every escaped string and never zeroizes;
//! - `from_reader` copies *every* string into that scratch, escaped or not;
//! - `Value` / `RawValue` deserialize the whole document into plain `String`s;
//! - serde's own error formatting renders `Unexpected::Str(value)` as
//!   `string "…the plaintext…"`, which is exactly what ends up in a log.
//!
//! None of those are serde's trait layer — they belong to `serde_json`. A
//! format we own has no scratch buffer, no escaping pass, no `Value`, and
//! builds every error from a length, an index or a `&'static str`. The one
//! message an impl can influence is discarded rather than rendered in both
//! directions — serde's own `unknown_variant` / `unknown_field` helpers format
//! the offending name into it, and a hand-written `Serialize` impl can format
//! payload into it. Errors are then safe to log, and a test pins that.
//!
//! # Why serde traits instead of a new trait pair
//!
//! Because it costs nothing and gives away nothing. Writing a bespoke
//! `SecureSerialize` trait would mean shipping a `#[derive]` macro, which means
//! `syn`, `quote`, `proc-macro2` and a second `proc-macro = true` crate. Using
//! `serde::Serializer` / `serde::Deserializer` needs **no new dependency at
//! all** (serde is already optional here) and keeps the exact attribute surface:
//!
//! ```
//! use secure_types::{decode, encode};
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Serialize, Deserialize, Debug, PartialEq)]
//! struct VaultData {
//!    label: String,
//!
//!    /// AEAD key for the wallet state
//!    #[serde(default)]
//!    wallet_state_key: Option<u32>,
//!
//!    #[serde(default, skip_serializing)]
//!    contacts: Vec<String>,
//! }
//!
//! let vault = VaultData {
//!    label: "main".to_owned(),
//!    wallet_state_key: Some(7),
//!    contacts: vec!["not persisted".to_owned()],
//! };
//!
//! let encoded = encode(&vault)?;
//!
//! // The plaintext only ever existed in locked memory.
//! let decoded = decode::<VaultData>(&encoded)?;
//!
//! assert_eq!(decoded.wallet_state_key, Some(7));
//! assert!(decoded.contacts.is_empty()); // `skip_serializing` -> default
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! # What is not supported
//!
//! The format is not self-describing, so [`serde::Deserializer::deserialize_any`]
//! cannot be implemented. Anything that depends on it fails with
//! [`DecodeError::Unsupported`] rather than guessing:
//!
//! - `#[serde(flatten)]`
//! - `#[serde(untagged)]`
//! - `serde_json::Value`-shaped fields
//!
//! Note the asymmetry for untagged enums: an untagged variant *serializes*
//! successfully — writing one needs no type tag — and only fails on the way
//! back in. A successful [`encode`] is therefore not on its own a promise that
//! the value is decodable.
//!
//! [`serde::Deserializer::deserialize_ignored_any`] works in exactly one place:
//! the body of a struct field, which is the only position whose extent is known.
//! That is what makes "add a field with `#[serde(default)]`" a compatible
//! change, and it is why struct fields carry a length.
//!
//! # The wire format
//!
//! Integers are little-endian; lengths and counts are unsigned LEB128 varints.
//! [`FORMAT_VERSION`] is the first byte of every document.
//!
//! ```text
//! unit, unit_struct   := (no bytes)
//! bool                := 0x00 | 0x01
//! u8..u64 / i8..i64   := little-endian, fixed width
//! u128 / i128         := little-endian, 16 bytes
//! f32 / f64           := IEEE-754 bits, little-endian
//! char                := u32 LE Unicode scalar value
//! str                 := varint(len) || utf8 bytes
//! bytes               := varint(len) || bytes
//! none                := 0x00
//! some(v)             := 0x01 || v
//! seq / tuple /
//!   tuple_struct      := varint(count) || value * count
//! map                 := varint(count) || (key || value) * count
//! struct / struct_var := varint(count) || (str(field_name) ||
//!                                          u32_le(value_len) || value) * count
//! newtype_struct      := value
//! enum                := str(variant_name) || variant_payload
//! ```
//!
//! Struct fields and enum variants are tagged by *name* rather than by position
//! or index, so reordering or skipping one cannot silently reinterpret old data,
//! and the `u32` length around each field body is what lets a reader skip a
//! field it does not know.

mod decoder;
mod encoder;
mod format;

use crate::SecureBytes;

use serde::Serialize;
use serde::de::DeserializeOwned;

pub use format::{DecodeError, EncodeError, FORMAT_VERSION};

/// Initial buffer size used by [`encode`].
///
/// Matches the JSON helpers' `DEFAULT_JSON_CAPACITY`: large enough that a
/// typical payload does not grow at all, small enough not to lock pages for
/// nothing.
const DEFAULT_CAPACITY: usize = 1024;

/// Encodes `value` into a fresh locked buffer.
///
/// The returned [`SecureBytes`] holds the only copy of the encoded value: it is
/// locked while unused and zeroized on drop, and growth wipes the previous
/// allocation. Use [`encode_with_capacity`] when the payload size is known, so
/// the buffer does not have to be reallocated and re-locked.
///
/// # Errors
///
/// Returns [`EncodeError::Secure`] if the locked buffer cannot be allocated or
/// grown, [`EncodeError::LengthOverflow`] if a value does not fit its length
/// field, and [`EncodeError::ElementCountMismatch`] if a hand-written
/// `Serialize` impl writes a different number of elements than it declared.
///
/// # Example
///
/// ```
/// use secure_types::{decode, encode, SecureString};
///
/// let secret = SecureString::from("hunter2");
///
/// let encoded = encode(&secret)?;
/// let decoded = decode::<SecureString>(&encoded)?;
///
/// decoded.unlock_str(|value| assert_eq!(value, "hunter2"));
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn encode<T>(value: &T) -> Result<SecureBytes, EncodeError>
where
   T: ?Sized + Serialize,
{
   encode_with_capacity(value, DEFAULT_CAPACITY)
}

/// Same as [`encode`], with an explicit initial buffer size.
///
/// Sizing the buffer to the expected payload avoids growing (and re-locking) it,
/// which keeps both the `mprotect` traffic and the `RLIMIT_MEMLOCK` pressure
/// predictable.
///
/// # Errors
///
/// Same as [`encode`].
pub fn encode_with_capacity<T>(value: &T, capacity: usize) -> Result<SecureBytes, EncodeError>
where
   T: ?Sized + Serialize,
{
   let mut buffer = SecureBytes::new_with_capacity(capacity).map_err(EncodeError::Secure)?;

   encoder::encode_into(&mut buffer, value)?;

   Ok(buffer)
}

/// Decodes `value` out of a locked buffer.
///
/// The buffer is unlocked only for the duration of the decode, and put back
/// under protection afterwards — including if the decode fails.
///
/// `T: DeserializeOwned` is load-bearing rather than decoration: it statically
/// forbids any type that would borrow out of the buffer, which is what keeps a
/// reference from surviving into the re-locked window.
///
/// # Errors
///
/// Returns [`DecodeError`] if the input is truncated or malformed, carries an
/// unsupported [`FORMAT_VERSION`], has trailing bytes, or does not match `T`.
///
/// # Example
///
/// ```
/// use secure_types::{decode, encode, SecureVec};
///
/// let key = SecureVec::from_slice(&[1u8, 2, 3])?;
///
/// let encoded = encode(&key)?;
/// let decoded = decode::<SecureVec<u8>>(&encoded)?;
///
/// decoded.unlock_slice(|value| assert_eq!(value, &[1, 2, 3]));
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn decode<T>(bytes: &SecureBytes) -> Result<T, DecodeError>
where
   T: DeserializeOwned,
{
   bytes.unlock_slice(|unlocked| decode_slice::<T>(unlocked))
}

/// Same as [`decode`], for a slice the caller already holds unlocked.
///
/// `bytes` must be the complete document: trailing bytes are an error, because a
/// document that decodes with bytes left over means the reader and the writer
/// disagree about the layout.
///
/// # Errors
///
/// Same as [`decode`].
pub fn decode_slice<T>(bytes: &[u8]) -> Result<T, DecodeError>
where
   T: DeserializeOwned,
{
   decoder::decode_from(bytes)
}