security.rs

crates/veilvoice-gui/src/security.rs

veilvoice-gui · 2403 lines · read the source here · or on GitHub

The application lock, and the at-rest encryption of what VeilVoice writes.

Two passwords, and why

There are two, deliberately:

  • the app lock, which decides whether VeilVoice will open at all, and
  • the recording passphrase, which encrypts the files it produces.

Collapsing them into one would mean that unlocking the app also unseals every recording it has ever written, which is the opposite of what a lock is for. veilvoice_crypto::lock additionally domain-separates its verifier, so even a user who types the same string in both places does not end up with two copies of one value.

What the lock is worth

Not much against an attacker with the disk, and the UI says so in veilvoice_crypto::lock::SCOPE, shown on the unlock screen itself rather than buried in an about page. It stops the person who picks up your unlocked laptop. It does not stop someone who takes the drive.

A limitation of typing a password into a window

A text field owns a String, so a passphrase exists as ordinary heap bytes while it is being typed. That window cannot be removed, because something has to receive the keystrokes, but it can be kept short, and it is:

  • the typing buffer is wiped the moment the passphrase is confirmed;
  • the confirmed passphrase is held only as a veilvoice_crypto::Secret, page-locked and zeroized on drop, for the rest of the session;
  • locking the app, or changing the passphrase, wipes both.

It used to be kept as a plain String for the whole session, which was a much larger window for no benefit.

None of this defends against someone who can read this process's memory. If they can, they have already won, and docs/WHITEPAPER.md §7 says so rather than implying otherwise. What it does is stop a passphrase lingering in a heap allocation long after it was needed, where a core dump or a swapped page could pick it up.

In plain words

The lock on the window, and the encryption of the files VeilVoice writes.

There are two passphrases and they do different jobs. One opens the application. The other encrypts a recording, and it is asked for separately because they protect different things and losing one should not mean losing the other.

The panel says what the lock is worth and what it is not: it stops somebody who picks up your unlocked computer, and it does not stop somebody who has the disk. Encrypting the recording is what protects the recording.

WHAT THIS FILE CONTAINS

2403 lines defining 45 functions (26 public), 4 types and 3 constants. Everything below is read out of the source, so it cannot disagree with the code.

The types it owns.

  • enum Sealing line 81 · How the recording that comes out of a job is protected.
  • enum Op line 106 · What a background lock operation was trying to do.
  • struct Security line 129 · Everything about locking the app and sealing its output.
  • enum Plan line 1357 · What a finished job should do with its bytes.

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.

  • Security::load line 279 · Read the lock file for this machine and start locked if one is set.
    reaches default
  • Security::take_unlock_passphrase line 322 · Take the passphrase that just opened the lock, once.
  • Security::take_unlock_store_key line 331 · Collect the obfuscated store's key from the unlock that just happened.
  • Security::prefer_app_lock_sealing line 340 · Start in Sealing::AppLock, because the user asked for that last time.
  • Security::seals_with_app_lock line 348 · Whether the app-lock sealing mode is currently chosen, so the window can have the choice remembered.
  • Security::tampered line 356 · Whether the lock reported having been interfered with.
  • Security::is_locked line 361 · Whether the unlock screen should be shown instead of the app.
  • Security::set_lock_from_setup line 371 · Whether a lock is configured at all.
    reaches spawn, run_op, reopen
  • Security::has_recording_passphrase line 378 · Whether a recording passphrase is held for this session.
  • Security::set_recording_passphrase line 387 · Take a recording passphrase from the first-run setup.
    reaches into_secret
  • Security::lock_after_idle line 415 · Lock because nobody has touched the window for a while.
    reaches lock_inner, wipe_secrets
  • Security::blocked_reason line 476 · Why a job cannot start yet, for the button's tooltip.
    reaches ready_to_write
  • Security::plan line 494 · How the next job should protect its output.
  • Security::is_busy line 647 · Whether a lock operation is running, so the window keeps repainting and the spinner actually spins.
    reaches busy
  • Security::unlock_screen line 652 · The full-window unlock screen.
    reaches busy, poll, spawn, unlock_row, into_secret, wipe_form, run_op, reopen
  • Security::tab line 854 · The security tab: manage the lock, and see what it is worth.
    reaches busy, button_column, has_lock, interference_banner, lock_now, password_row, poll, spawn, lock_inner, into_secret, wipe_form, run_op
  • Security::load_mandate line 1031 · Read the baseline from disk and apply it to the checkbox.
  • Security::mandate_requires_app_lock line 1046 · Whether the baseline insists on the app lock.
  • Security::mandate_requires_encryption line 1051 · Whether the baseline insists on encryption at rest.
  • Security::mandate_history line 1056 · The change log, for the panel that shows it.
  • Security::recording_controls line 1079 · The at-rest controls that sit inside the file tab.
    reaches into_secret, mandate_history_panel, password_row, record, mandate_history_rows
  • Security::disable_dialogue line 1299 · The dialogue shown when the user turns at-rest encryption off.
    reaches record
  • Plan::write line 1382 · Seal wav if the plan says to, and write it.

WHAT CALLS WHAT

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. 22 of 45 functions are drawn; the diagram is bounded at 22 so it stays readable.

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_into_secret["into_secret<br/>line 72"]
    n_default["Security::default<br/>line 236"]
    n_drop["Security::drop<br/>line 272"]
    n_load(["Security::load<br/>line 279"])
    n_set_lock_from_setup(["Security::set_lock_from_setup<br/>line 371"])
    n_set_recording_passphrase(["Security::<br/>set_recording_passphrase<br/>line 387"])
    n_has_lock["Security::has_lock<br/>line 396"]
    n_lock_now["Security::lock_now<br/>line 405"]
    n_lock_after_idle(["Security::lock_after_idle<br/>line 415"])
    n_lock_inner["Security::lock_inner<br/>line 424"]
    n_wipe_secrets["Security::wipe_secrets<br/>line 433"]
    n_ready_to_write["Security::ready_to_write<br/>line 464"]
    n_blocked_reason(["Security::blocked_reason<br/>line 476"])
    n_spawn["Security::spawn<br/>line 526"]
    n_poll["Security::poll<br/>line 541"]
    n_wipe_form["Security::wipe_form<br/>line 633"]
    n_busy["Security::busy<br/>line 641"]
    n_is_busy(["Security::is_busy<br/>line 647"])
    n_unlock_screen(["Security::unlock_screen<br/>line 652"])
    n_tab(["Security::tab<br/>line 854"])
    n_recording_controls(["Security::recording_controls<br/>line 1079"])
    n_disable_dialogue(["Security::disable_dialogue<br/>line 1299"])
    n_blocked_reason --> n_ready_to_write
    n_drop --> n_wipe_secrets
    n_is_busy --> n_busy
    n_load --> n_default
    n_lock_after_idle --> n_lock_inner
    n_lock_inner --> n_wipe_secrets
    n_lock_now --> n_lock_inner
    n_poll --> n_into_secret
    n_poll --> n_wipe_form
    n_recording_controls --> n_into_secret
    n_set_lock_from_setup --> n_spawn
    n_set_recording_passphrase --> n_into_secret
    n_tab --> n_busy
    n_tab --> n_has_lock
    n_tab --> n_lock_now
    n_tab --> n_poll
    n_tab --> n_spawn
    n_unlock_screen --> n_busy
    n_unlock_screen --> n_poll
    n_unlock_screen --> n_spawn
    click n_into_secret href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L72" "open the source"
    click n_default href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L236" "open the source"
    click n_drop href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L272" "open the source"
    click n_load href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L279" "open the source"
    click n_set_lock_from_setup href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L371" "open the source"
    click n_set_recording_passphrase href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L387" "open the source"
    click n_has_lock href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L396" "open the source"
    click n_lock_now href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L405" "open the source"
    click n_lock_after_idle href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L415" "open the source"
    click n_lock_inner href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L424" "open the source"
    click n_wipe_secrets href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L433" "open the source"
    click n_ready_to_write href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L464" "open the source"
    click n_blocked_reason href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L476" "open the source"
    click n_spawn href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L526" "open the source"
    click n_poll href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L541" "open the source"
    click n_wipe_form href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L633" "open the source"
    click n_busy href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L641" "open the source"
    click n_is_busy href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L647" "open the source"
    click n_unlock_screen href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L652" "open the source"
    click n_tab href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L854" "open the source"
    click n_recording_controls href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L1079" "open the source"
    click n_disable_dialogue href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-gui/src/security.rs#L1299" "open the source"
    classDef entry fill:#1f2335,stroke:#7aa2f7,color:#c0caf5
    class n_load,n_set_lock_from_setup,n_set_recording_passphrase,n_lock_after_idle,n_blocked_reason,n_is_busy,n_unlock_screen,n_tab,n_recording_controls,n_disable_dialogue entry
    classDef api fill:#1f2335,stroke:#7dcfff,color:#c0caf5
    class n_has_lock,n_lock_now,n_ready_to_write api
    classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
    class n_into_secret,n_default,n_drop,n_lock_inner,n_wipe_secrets,n_spawn,n_poll,n_wipe_form,n_busy 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
into_secret fn72Move a typed passphrase out of its String and into page-locked storage, wiping the buffer it came from.
Sealing pub enum81How the recording that comes out of a job is protected.
Op enum106What a background lock operation was trying to do.
OpResult type117A finished lock operation: the store as it now stands, and how it went.
Security pub struct129Everything about locking the app and sealing its output.
Security::default fn236The safe state, and deliberately free of I/O so tests and VeilVoiceApp::default() never touch the real lock file.
Security::drop fn272
Security::load pub fn279Read the lock file for this machine and start locked if one is set.
Security::take_unlock_passphrase pub fn322Take the passphrase that just opened the lock, once.
Security::take_unlock_store_key pub fn331Collect the obfuscated store's key from the unlock that just happened.
Security::prefer_app_lock_sealing pub fn340Start in Sealing::AppLock, because the user asked for that last time.
Security::seals_with_app_lock pub fn348Whether the app-lock sealing mode is currently chosen, so the window can have the choice remembered.
Security::tampered pub fn356Whether the lock reported having been interfered with.
Security::is_locked pub fn361Whether the unlock screen should be shown instead of the app.
Security::set_lock_from_setup pub fn371Whether a lock is configured at all.
Security::has_recording_passphrase pub fn378Whether a recording passphrase is held for this session.
Security::set_recording_passphrase pub fn387Take a recording passphrase from the first-run setup.
Security::has_lock pub fn396Whether an app lock is configured on this machine.
Security::lock_now pub fn405Lock the app now, wiping the session passphrase with it.
Security::lock_after_idle pub fn415Lock because nobody has touched the window for a while.
Security::lock_inner fn424Lock, remembering whether the person did it or the idle timer did.
Security::wipe_secrets fn433Wipe every plaintext secret this struct is holding.
Security::ready_to_write pub fn464Whether a job may start: either encryption is off, or there is something to encrypt with.
Security::blocked_reason pub fn476Why a job cannot start yet, for the button's tooltip.
Security::plan pub fn494How the next job should protect its output.
Security::spawn fn526Run a lock operation on a thread, so the window keeps drawing.
Security::poll fn541Collect a finished lock operation.
Security::wipe_form fn633Clear what was typed, so a passphrase does not sit in a field after use.
Security::busy fn641Whether an operation is in flight, so the panel can refuse a second one.
Security::is_busy pub fn647Whether a lock operation is running, so the window keeps repainting and the spinner actually spins.
Security::unlock_screen pub fn652The full-window unlock screen.
Security::interference_banner fn798The standing report that the lock file was interfered with.
Security::tab pub fn854The security tab: manage the lock, and see what it is worth.
Security::load_mandate pub fn1031Read the baseline from disk and apply it to the checkbox.
Security::mandate_requires_app_lock pub fn1046Whether the baseline insists on the app lock.
Security::mandate_requires_encryption pub fn1051Whether the baseline insists on encryption at rest.
Security::mandate_history pub fn1056The change log, for the panel that shows it.
Security::record fn1066Record a change to the baseline, and write it down.
Security::recording_controls pub fn1079The at-rest controls that sit inside the file tab.
Security::mandate_history_panel fn1269The log of every time a requirement was turned off or back on.
Security::mandate_history_rows fn1288One coloured line per change, newest concern last: green for a requirement put back, yellow for one turned off.
Security::disable_dialogue pub fn1299The dialogue shown when the user turns at-rest encryption off.
DISABLE_WARNING pub const1341What the user is told before recordings stop being encrypted.
Plan pub enum1357What a finished job should do with its bytes.
Plan::write pub fn1382Seal wav if the plan says to, and write it.
Plan::fmt fn1429
run_op fn1440Run one lock operation, off the UI thread.
reopen fn1509Re-open the lock store from disk, or None when there is nothing to open.
PASSWORD_LABEL_WIDTH const1516The width every passphrase label is given, so every field starts level.
PASSWORD_FIELD_WIDTH const1521How wide every passphrase field is drawn, on this tab and on the lock screen.
button_column fn1555One labelled passphrase field, with the field in the same place every time.
password_row fn1570One passphrase field with its label, at the shared width.
unlock_row fn1615The password row on the lock screen: the label, the field and the button.