studio.rs

crates/veilvoice-crypto/src/studio.rs

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

The studio vault: a key that exists only when both locks have been opened.

The one thing this adds

Everything else in this crate is protected by one secret. The app lock guards the window; a sealed recording is opened by its own passphrase. Each is a single point: whoever has that one secret has the thing it guards.

A StudioKey is derived from both, and from neither alone. A laptop stolen with VeilVoice already unlocked opens nothing here, because the at-rest passphrase was never entered. An at-rest passphrase learned by any means opens nothing here either, because it is not the app lock. Both, at the same time, on the same machine, or the vault stays shut.

How the two are combined

Not concatenated, and not one encrypting the other. Both secrets go into HKDF-SHA256 as input keying material, under a salt that names this vault and its version, and the output is the vault key:

ikm  = app_lock_key || at_rest_key
salt = "veilvoice/studio-vault/v1"
key  = HKDF-SHA256(ikm, salt, info)

HKDF-Extract mixes the whole of the input, so an attacker holding one half and guessing the other faces the full cost of the half they are guessing. Concatenating the two ciphertexts instead, or encrypting once with each key in turn, would let each layer be attacked separately, which is the mistake this shape exists to avoid.

The length of each half is bound into the info string. Without that, ("ab", "c") and ("a", "bc") would produce the same input keying material and therefore the same vault key, which is a collision an attacker chooses rather than finds.

What it is worth, and what it is not

It raises the cost of a stolen machine and of a leaked passphrase, and it turns one compromise into two. Both of those are real.

It does not defeat somebody who is watching this process while both secrets are entered: at that moment the derived key exists in memory, and this crate has never claimed to beat an attacker who is already inside the process. It is page-locked and zeroized like every other secret here, which narrows the window and does not close it. The vault is a second lock on the door, not a guard in the room.

In plain words

The recordings the studio makes are locked with a key made out of two of your passwords at once. Somebody who learns one of them still cannot open them, and neither can somebody who walks off with the computer while the app is open.

What it cannot do is protect you from something already running inside VeilVoice at the moment you type both.

WHAT THIS FILE CONTAINS

1508 lines defining 31 functions (16 public), 4 types and 5 constants. Everything below is read out of the source, so it cannot disagree with the code.

The types it owns.

  • struct StudioKey line 83 · A key that exists only while both locks are open.
  • struct Entry line 144 · One recording in the vault.
  • struct Studio line 180 · A directory of recordings, sealed under a StudioKey.
  • struct Shape line 456 · What a decoy vault looks like from outside, so it looks like the real one.

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.

  • StudioKey::derive line 94 · Derive the vault key from both secrets.
  • StudioKey::expose line 124 · Borrow the key bytes, for sealing and opening the vault.
  • StudioKey::is_locked line 134 · Whether the operating system agreed to keep this key out of swap.
  • Studio::open line 192 · Open the vault in dir, creating the directory if it is not there.
  • Studio::dir line 199 · Where the vault lives.
  • Studio::store line 233 · Seal wav into the vault under name, returning its entry.
    reaches list, new_id, seal, write_index, parse_index, unseal, secret_key, render_index, safe_id
  • Studio::load line 262 · Open one recording into locked memory.
    reaches safe_id, unseal_secret, secret_key
  • Studio::rename line 281 · Change what a recording is called.
    reaches list, safe_id, write_index, parse_index, unseal, render_index, seal, secret_key
  • Studio::remove line 302 · Remove one recording and its index entry.
    reaches list, safe_id, write_index, parse_index, unseal, render_index, seal, secret_key
  • Shape::of line 473 · Measure a real vault, to build decoys that match it.
    reaches render_index
  • Shape::bytes_on_disk line 502 · What one vault of this shape occupies, in bytes, as files on a disk.
    reaches index_len, bare_index, digits
  • find_or_make line 602 · Open the one vault under parent that key unlocks, making it on a first run.
    reaches migrate_flat, new_id, vault_dirs
  • make_decoy_in line 685 · Make one decoy under parent, named the way a real vault is named.
    reaches make_decoy, new_id, pad_index, render_index

WHAT CALLS WHAT

Studio::secret_key line 208 Studio::list line 219 Studio::store line 233 Studio::load line 262 Studio::rename line 281 Studio::remove line 302 Studio::write_index line 319 Studio::seal line 328 Studio::unseal line 341 Studio::unseal_secret line 359 new_id line 386 safe_id line 401 render_index line 411 parse_index line 426 Shape::of line 473 Shape::bytes_on_disk line 502 Shape::bare_index line 516 Shape::index_len line 529 vault_dirs line 559 find_or_make line 602 make_decoy_in line 685 make_decoy line 717 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. 22 of 31 functions are drawn; the diagram is bounded at 22 so it stays readable.

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 31 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_secret_key["Studio::secret_key<br/>line 208"]
    n_list["Studio::list<br/>line 219"]
    n_store(["Studio::store<br/>line 233"])
    n_load(["Studio::load<br/>line 262"])
    n_rename(["Studio::rename<br/>line 281"])
    n_remove(["Studio::remove<br/>line 302"])
    n_write_index["Studio::write_index<br/>line 319"]
    n_seal["Studio::seal<br/>line 328"]
    n_unseal["Studio::unseal<br/>line 341"]
    n_unseal_secret["Studio::unseal_secret<br/>line 359"]
    n_new_id["new_id<br/>line 386"]
    n_safe_id["safe_id<br/>line 401"]
    n_render_index["render_index<br/>line 411"]
    n_parse_index["parse_index<br/>line 426"]
    n_of(["Shape::of<br/>line 473"])
    n_bytes_on_disk(["Shape::bytes_on_disk<br/>line 502"])
    n_bare_index["Shape::bare_index<br/>line 516"]
    n_index_len["Shape::index_len<br/>line 529"]
    n_vault_dirs["vault_dirs<br/>line 559"]
    n_find_or_make(["find_or_make<br/>line 602"])
    n_make_decoy_in(["make_decoy_in<br/>line 685"])
    n_make_decoy["make_decoy<br/>line 717"]
    n_bytes_on_disk --> n_index_len
    n_find_or_make --> n_new_id
    n_find_or_make --> n_vault_dirs
    n_index_len --> n_bare_index
    n_list --> n_parse_index
    n_list --> n_unseal
    n_load --> n_safe_id
    n_load --> n_unseal_secret
    n_make_decoy --> n_new_id
    n_make_decoy_in --> n_make_decoy
    n_make_decoy_in --> n_new_id
    n_of --> n_render_index
    n_parse_index --> n_safe_id
    n_remove --> n_list
    n_remove --> n_safe_id
    n_remove --> n_write_index
    n_rename --> n_list
    n_rename --> n_safe_id
    n_rename --> n_write_index
    n_seal --> n_secret_key
    n_store --> n_list
    n_store --> n_new_id
    n_store --> n_seal
    n_store --> n_write_index
    n_unseal --> n_secret_key
    n_unseal_secret --> n_secret_key
    n_write_index --> n_render_index
    n_write_index --> n_seal
    click n_secret_key href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L208" "open the source"
    click n_list href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L219" "open the source"
    click n_store href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L233" "open the source"
    click n_load href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L262" "open the source"
    click n_rename href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L281" "open the source"
    click n_remove href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L302" "open the source"
    click n_write_index href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L319" "open the source"
    click n_seal href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L328" "open the source"
    click n_unseal href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L341" "open the source"
    click n_unseal_secret href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L359" "open the source"
    click n_new_id href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L386" "open the source"
    click n_safe_id href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L401" "open the source"
    click n_render_index href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L411" "open the source"
    click n_parse_index href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L426" "open the source"
    click n_of href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L473" "open the source"
    click n_bytes_on_disk href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L502" "open the source"
    click n_bare_index href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L516" "open the source"
    click n_index_len href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L529" "open the source"
    click n_vault_dirs href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L559" "open the source"
    click n_find_or_make href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L602" "open the source"
    click n_make_decoy_in href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L685" "open the source"
    click n_make_decoy href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/studio.rs#L717" "open the source"
    classDef entry fill:#1f2335,stroke:#7aa2f7,color:#c0caf5
    class n_store,n_load,n_rename,n_remove,n_of,n_bytes_on_disk,n_find_or_make,n_make_decoy_in entry
    classDef api fill:#1f2335,stroke:#7dcfff,color:#c0caf5
    class n_list,n_vault_dirs,n_make_decoy api
    classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
    class n_secret_key,n_write_index,n_seal,n_unseal,n_unseal_secret,n_new_id,n_safe_id,n_render_index,n_parse_index,n_bare_index,n_index_len 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
KEY_LEN pub const67Bytes in a studio vault key.
SALT const73Names this construction and its version in the HKDF salt.
INFO const76The HKDF info label.
StudioKey pub struct83A key that exists only while both locks are open.
StudioKey::derive pub fn94Derive the vault key from both secrets.
StudioKey::expose pub fn124Borrow the key bytes, for sealing and opening the vault.
StudioKey::is_locked pub fn134Whether the operating system agreed to keep this key out of swap.
Entry pub struct144One recording in the vault.
Entry::fmt fn159
Studio pub struct180A directory of recordings, sealed under a StudioKey.
INDEX const188The index file's name.
Studio::open pub fn192Open the vault in dir, creating the directory if it is not there.
Studio::dir pub fn199Where the vault lives.
Studio::secret_key fn208The vault key as a Secret, for the AEAD.
Studio::list pub fn219Every recording in the vault, oldest first.
Studio::store pub fn233Seal wav into the vault under name, returning its entry.
Studio::load pub fn262Open one recording into locked memory.
Studio::rename pub fn281Change what a recording is called.
Studio::remove pub fn302Remove one recording and its index entry.
Studio::write_index fn319Seal the index and replace the file on disk with it.
Studio::seal fn328Seal with a fresh nonce, binding aad so a file cannot be moved to another identity inside the same vault and still open.
Studio::unseal fn341Open what Studio::seal produced, with the same associated data.
Studio::unseal_secret fn359The same, decrypting straight into locked memory.
ID_LEN const375How long an identifier is, in characters.
new_id fn386A random, opaque identifier: ID_LEN lower-case letters and digits that say nothing about what they name.
safe_id fn401Whether id is one this vault could have produced.
render_index fn411The index, as lines.
parse_index fn426Read the index back: one entry per line, four tab-separated fields.
Shape pub struct456What a decoy vault looks like from outside, so it looks like the real one.
Shape::of pub fn473Measure a real vault, to build decoys that match it.
Shape::bytes_on_disk pub fn502What one vault of this shape occupies, in bytes, as files on a disk.
Shape::bare_index fn516The shortest index a decoy of this shape can be written with: every entry present and every name empty.
Shape::index_len fn529The index length a decoy of this shape will actually be written with.
digits fn539How many decimal digits n is written with.
vault_dirs pub fn559Every directory under parent that is shaped like a vault.
find_or_make pub fn602Open the one vault under parent that key unlocks, making it on a first run.
migrate_flat fn645Move a vault written straight into parent down into a directory of its own.
make_decoy_in pub fn685Make one decoy under parent, named the way a real vault is named.
make_decoy pub fn717Fill dir with a vault that never held anything.
pad_index fn770Grow the names until the index is exactly the length a real one was.