amnesia.rs

crates/veilvoice-crypto/src/amnesia.rs

veilvoice-crypto · 428 lines · read the source here · or on GitHub

Amnesic secret storage: page-locked, zeroized, and never printed.

No unsafe, even here

Locking pages out of the swap file is a raw syscall, VirtualLock on Windows and mlock on Unix, and it is the one place a project like this usually has to reach for unsafe. It does not here: the region crate exposes a safe, cross-platform wrapper. VeilVoice therefore contains no unsafe code at all, and every crate keeps #![forbid(unsafe_code)].

What locking does and does not buy

Locking keeps key material out of the page file, so a secret cannot be recovered later by reading swap off the disk. It does not protect against an attacker who can already read this process's memory, and it does not survive hibernation, which writes RAM to disk wholesale. Locking can also fail outright, because unprivileged Linux users get a small RLIMIT_MEMLOCK budget, so it is best-effort hardening and never a precondition. Secret::is_locked reports what actually happened, so the UI can tell the user the truth rather than imply a guarantee that was not obtained.

Zeroization, by contrast, always happens.

Why each secret owns whole pages

Locking has page granularity, not byte granularity. If two secrets share a 4 KiB page, both lock it, and the first one dropped unlocks the page out from under the second, which is still live and now swappable.

Each Secret therefore over-allocates and locks a page-aligned, page-sized span lying entirely within its own allocation. No other allocation can occupy those bytes, so none can be inside those pages: lock and unlock are exact, and locking a secret never drags unrelated data into physical memory alongside it.

Why the lock is not held by an RAII guard

region::lock hands back a guard that unlocks on drop, but its destructor panics if unlocking fails, and unlocking can fail for reasons that are nobody's fault: Windows does not reference-count VirtualLock and may drop pages from a process working set on its own, after which VirtualUnlock reports ERROR_NOT_LOCKED. A type whose entire job is holding key material must not abort the process while being dropped. The lock is released explicitly instead, and a failure to unlock is ignored: it leaves pages pinned, which is harmless, rather than unwinding out of a destructor.

In plain words

A place to hold a passphrase or a key while it is being used, which tries hard to forget it afterwards.

It asks the operating system not to write that memory out to disk, wipes it as soon as it is finished with, and refuses to print itself. That last one matters more than it sounds: secrets most often escape not by being stolen but by appearing in an error message or a log that somebody later sends on.

Comparisons take the same amount of time whether or not they match, so nothing is given away by how long an answer took.

WHAT THIS FILE CONTAINS

428 lines defining 15 functions (9 public), 1 type and 0 constants. Everything below is read out of the source, so it cannot disagree with the code.

The types it owns.

  • struct Secret line 71 · A byte buffer holding key material.

What happens when it runs. These are the ways in: public, and nothing else in this file calls them, so they are what an outside caller reaches first.

  • Secret::new line 131 · Wrap bytes, taking ownership and wiping the caller's copy.
    reaches zeroed, lock_pages
  • Secret::random line 173 · Fill len bytes from the operating-system CSPRNG.
    reaches zeroed, lock_pages
  • Secret::is_locked line 184 · Whether the pages were successfully locked out of swap.
  • Secret::len line 189 · Length in bytes.
  • Secret::is_empty line 194 · Whether the secret is empty.
  • Secret::expose_mut line 205 · Borrow mutably, for filling in place.
  • Secret::wipe line 213 · Wipe the contents now, before the value goes out of scope.

WHAT CALLS WHAT

lock_pages line 103 unlock_pages line 117 Secret::new line 131 Secret::zeroed line 139 Secret::random line 173 Secret::is_locked line 184 Secret::len line 189 Secret::is_empty line 194 Secret::expose line 200 Secret::expose_mut line 205 Secret::wipe line 213 Secret::drop line 219 Secret::clone line 230 Secret::eq line 240 Secret::fmt line 249 entry: a way in: public, and nothing in this file calls it api: public, and also used inside this file helper: private to this file dashed: a call that goes back up, or across a wrapped rank The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.

The same graph as Mermaid source
%%{init: {"theme":"base","themeVariables":{"background":"#1a1b26","primaryColor":"#1f2335","primaryTextColor":"#c0caf5","primaryBorderColor":"#7aa2f7","secondaryColor":"#16161e","tertiaryColor":"#16161e","lineColor":"#737aa2","textColor":"#c0caf5","mainBkg":"#1f2335","nodeBorder":"#7aa2f7","clusterBkg":"#16161e","clusterBorder":"#2f3549","fontFamily":"ui-monospace, SFMono-Regular, Consolas, monospace","fontSize":"14px"}}}%%
flowchart TD
    n_lock_pages["lock_pages<br/>line 103"]
    n_unlock_pages["unlock_pages<br/>line 117"]
    n_new(["Secret::new<br/>line 131"])
    n_zeroed["Secret::zeroed<br/>line 139"]
    n_random(["Secret::random<br/>line 173"])
    n_is_locked(["Secret::is_locked<br/>line 184"])
    n_len(["Secret::len<br/>line 189"])
    n_is_empty(["Secret::is_empty<br/>line 194"])
    n_expose["Secret::expose<br/>line 200"]
    n_expose_mut(["Secret::expose_mut<br/>line 205"])
    n_wipe(["Secret::wipe<br/>line 213"])
    n_drop["Secret::drop<br/>line 219"]
    n_clone["Secret::clone<br/>line 230"]
    n_eq["Secret::eq<br/>line 240"]
    n_fmt["Secret::fmt<br/>line 249"]
    n_clone --> n_expose
    n_clone --> n_zeroed
    n_drop --> n_unlock_pages
    n_eq --> n_expose
    n_new --> n_zeroed
    n_random --> n_zeroed
    n_zeroed --> n_lock_pages
    click n_lock_pages href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L103" "open the source"
    click n_unlock_pages href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L117" "open the source"
    click n_new href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L131" "open the source"
    click n_zeroed href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L139" "open the source"
    click n_random href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L173" "open the source"
    click n_is_locked href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L184" "open the source"
    click n_len href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L189" "open the source"
    click n_is_empty href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L194" "open the source"
    click n_expose href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L200" "open the source"
    click n_expose_mut href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L205" "open the source"
    click n_wipe href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L213" "open the source"
    click n_drop href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L219" "open the source"
    click n_clone href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L230" "open the source"
    click n_eq href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L240" "open the source"
    click n_fmt href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/amnesia.rs#L249" "open the source"
    classDef entry fill:#1f2335,stroke:#7aa2f7,color:#c0caf5
    class n_new,n_random,n_is_locked,n_len,n_is_empty,n_expose_mut,n_wipe entry
    classDef api fill:#1f2335,stroke:#7dcfff,color:#c0caf5
    class n_zeroed,n_expose api
    classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
    class n_lock_pages,n_unlock_pages,n_drop,n_clone,n_eq,n_fmt helper

This site loads no third-party script, so it cannot run Mermaid; the diagram above is the same nodes and edges drawn by the generator instead. GitHub renders the source below directly.

ITEMS

ItemLineDocumentation
Secret pub struct71A byte buffer holding key material.
lock_pages fn103Lock span bytes at at out of swap, answering whether it happened.
unlock_pages fn117Release a lock taken by lock_pages.
Secret::new pub fn131Wrap bytes, taking ownership and wiping the caller's copy.
Secret::zeroed pub fn139Allocate len zero bytes, ready to be filled in place.
Secret::random pub fn173Fill len bytes from the operating-system CSPRNG.
Secret::is_locked pub fn184Whether the pages were successfully locked out of swap.
Secret::len pub fn189Length in bytes.
Secret::is_empty pub fn194Whether the secret is empty.
Secret::expose pub fn200Borrow the raw bytes.
Secret::expose_mut pub fn205Borrow mutably, for filling in place.
Secret::wipe pub fn213Wipe the contents now, before the value goes out of scope.
Secret::drop fn219
Secret::clone fn230
Secret::eq fn240
Secret::fmt fn249