secure-types 0.5.1

Secure data types that protect sensitive data in memory via locking and zeroization.
Documentation

Secure Types

The goal of this crate is to provide a simple way to properly handle sensitive data in memory (e.g. passwords, private keys, etc).

Currently there are 3 types:

  • SecureString: For working with strings.
  • SecureVec: For working with Vec<T>.
  • SecureArray: For working with &[T; LENGTH].

Features

  • Zeroization on Drop: Memory is wiped when dropped.
  • Memory locking (OS only): Pages are mprotected PROT_NONE except during an unlock* scope. That stops accidental reads, including from this process; it is not a defence against ptrace or a debugger. On the malloc path the allocation is also mlocked (Windows: VirtualLock) and, where the OS allows it, excluded from core dumps. Linux uses memfd_secret when the kernel supports it. See How memory is locked.
  • Scoped access: No Index/Derefsecret[0] does not compile. Contents are only reachable through the unlock* closures (the expose-ptr testing feature aside). Memory is unprotected only for that closure.
  • Send, not Sync: Values can move to another thread. Sharing one instance needs an explicit lock (Arc<Mutex<_>>); concurrent unlock races on page protection.
  • no_std: Zeroization only. Disable the default features — see Feature Flags.
  • Serde: Optional serialization for SecureString, SecureVec<T>, and SecureArray<T, LENGTH>. A u8 container serializes as a byte buffer; other element types as a sequence (SeqElement).
  • Binary codec (feature codec): A serde Serializer/Deserializer that encodes into locked memory and decodes out of it. No extra dependency beyond serde.

How memory is locked

  • Windows: VirtualProtect and VirtualLock.
  • Linux: memfd_secret when the kernel supports it. Otherwise mlock and madvise (MADV_DONTDUMP) on the malloc path. A memfd_secret allocation is not mlocked and is not marked MADV_DONTDUMP by memsec. Every path still uses mprotect(PROT_NONE) between unlocks.
  • Other Unix (macOS, FreeBSD, …): mlock plus mprotect(PROT_NONE). FreeBSD/DragonFly also use madvise(MADV_NOCORE). No memfd_secret.

mlock is best-effort: memsec ignores its return value, so hitting RLIMIT_MEMLOCK still constructs. mprotect failure is Error::LockFailed. A failed re-lock after an unlock* scope panics in every profile.

Usage

SecureString

use secure_types::SecureString;

 // Create a SecureString
let mut secret = SecureString::from("my_super_secret");

// The memory is locked here

// Safely append more data.
secret.push_str("_password");

// The memory is locked here.

// Use a scope to safely access the content as a &str.
secret.unlock_str(|exposed_str| {
     assert_eq!(exposed_str, "my_super_secret_password");
 });

 // When `secret` is dropped, its data zeroized.

SecureVec

use secure_types::SecureVec;

// Create a new, empty secure vector.
let mut secret_key: SecureVec<u8> = SecureVec::new().unwrap();

// Push some sensitive data into it.
secret_key.push(0);
secret_key.push(1);
secret_key.push(2);

// The memory is locked here.

// Use a scope to safely access the contents as a slice.
secret_key.unlock_slice(|unlocked_slice| {
     assert_eq!(unlocked_slice, &[0, 1, 2]);
 });

SecureArray

use secure_types::SecureArray;

let exposed_array: &mut [u8; 3] = &mut [1, 2, 3];
let mut secure_array = SecureArray::from_slice_mut(exposed_array).unwrap();


secure_array.unlock_mut(|unlocked_slice| {
    assert_eq!(unlocked_slice, &[1, 2, 3]);
});

Binary codec

The codec feature is a small binary serde format. Encode goes into locked memory; decode reads out of it. #[derive(Serialize, Deserialize)] and #[serde(...)] work as usual. No extra dependency beyond serde.

# #[cfg(feature = "codec")] {
use secure_types::{decode, encode};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct VaultData {
    label: String,

    #[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()],
};

// The encoded document is the only copy, and it lives in locked memory that is
// zeroized on drop.
let encoded = encode(&vault)?;
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>>(())

encode returns a SecureBytes. decode unlocks only for the parse and re-locks afterwards, including on error. Types are raw binary — a SecureArray<u8, 32> is 32 bytes — and strings are not escaped, so there is no scratch copy of an unescaped string.

Format evolution. FORMAT_VERSION is the first byte. An unknown version is refused. Adding a field with #[serde(default)] does not need a bump: fields are named and length-prefixed, so unknown fields are skipped and missing ones take their default. Changing a field's type does need a bump.

Not supported. No type tags, so deserialize_any is unimplemented. #[serde(flatten)], #[serde(untagged)], and Value-shaped fields fail with DecodeError::Unsupported. Untagged enums can still serialize; they cannot be read back.

See also the examples.

Feature Flags

  • use_os (default): Enables all OS-level security features. Supported on Linux, Windows, and other Unix (macOS, FreeBSD, …); the memfd_secret backing (and core-dump exclusion via MADV_DONTDUMP) is Linux-only.
  • no_os: No-op, kept for backwards compatibility. no_std is selected by disabling the default features (--no-default-features), which leaves only the zeroize-on-drop guarantee.
  • serde: Enables serialization/deserialization.
  • codec: Adds encode / encode_with_capacity / decode / decode_slice, a binary format written into locked memory and read out of it. Implies serde, works in no_std + alloc, and adds no dependency.
  • expose-ptr: For testing purposes. Exposes the locked memory region pointer.

Security notes

  • Serialization writes plaintext. serde_json::to_string/to_vec leave the document in an ordinary String/Vec that nothing wipes. Zeroize that buffer yourself, write through SecureBytesWriter, or use the binary codec.
  • Deserialization reads a buffer you own. serde_json::from_str/from_slice take a plain &str/&[u8]. Parse from inside locked memory (locked.unlock_slice(|json| serde_json::from_slice::<Vault>(json))) so the input is unlocked only for the parse. Escaped JSON strings still land in serde_json's own scratch buffer, which this crate cannot wipe. The codec decoder has no such scratch.

Running tests

Public-API tests live in tests/ (one integration crate per module). Internals, memory-protection checks, and crash tests that spawn a child stay in src/ — a fault on locked memory kills the process, so they run in a child. Shared fixtures are in tests/common/.

cargo test                                          # default features
cargo test --all-features
cargo test --features serde,expose-ptr
cargo test --no-default-features --features codec    # no_std + alloc, codec only

License

Licensed under the MIT license.

Credits