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 StudioKeyline 83 · A key that exists only while both locks are open.struct Entryline 144 · One recording in the vault.struct Studioline 180 · A directory of recordings, sealed under a StudioKey.struct Shapeline 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::deriveline 94 · Derive the vault key from both secrets.StudioKey::exposeline 124 · Borrow the key bytes, for sealing and opening the vault.StudioKey::is_lockedline 134 · Whether the operating system agreed to keep this key out of swap.Studio::openline 192 · Open the vault in dir, creating the directory if it is not there.Studio::dirline 199 · Where the vault lives.Studio::storeline 233 · Seal wav into the vault under name, returning its entry.
reacheslist,new_id,seal,write_index,parse_index,unseal,secret_key,render_index,safe_idStudio::loadline 262 · Open one recording into locked memory.
reachessafe_id,unseal_secret,secret_keyStudio::renameline 281 · Change what a recording is called.
reacheslist,safe_id,write_index,parse_index,unseal,render_index,seal,secret_keyStudio::removeline 302 · Remove one recording and its index entry.
reacheslist,safe_id,write_index,parse_index,unseal,render_index,seal,secret_keyShape::ofline 473 · Measure a real vault, to build decoys that match it.
reachesrender_indexShape::bytes_on_diskline 502 · What one vault of this shape occupies, in bytes, as files on a disk.
reachesindex_len,bare_index,digitsfind_or_makeline 602 · Open the one vault under parent that key unlocks, making it on a first run.
reachesmigrate_flat,new_id,vault_dirsmake_decoy_inline 685 · Make one decoy under parent, named the way a real vault is named.
reachesmake_decoy,new_id,pad_index,render_index
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 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
| Item | Line | Documentation |
|---|---|---|
KEY_LEN pub const | 67 | Bytes in a studio vault key. |
SALT const | 73 | Names this construction and its version in the HKDF salt. |
INFO const | 76 | The HKDF info label. |
StudioKey pub struct | 83 | A key that exists only while both locks are open. |
StudioKey::derive pub fn | 94 | Derive the vault key from both secrets. |
StudioKey::expose pub fn | 124 | Borrow the key bytes, for sealing and opening the vault. |
StudioKey::is_locked pub fn | 134 | Whether the operating system agreed to keep this key out of swap. |
Entry pub struct | 144 | One recording in the vault. |
Entry::fmt fn | 159 | |
Studio pub struct | 180 | A directory of recordings, sealed under a StudioKey. |
INDEX const | 188 | The index file's name. |
Studio::open pub fn | 192 | Open the vault in dir, creating the directory if it is not there. |
Studio::dir pub fn | 199 | Where the vault lives. |
Studio::secret_key fn | 208 | The vault key as a Secret, for the AEAD. |
Studio::list pub fn | 219 | Every recording in the vault, oldest first. |
Studio::store pub fn | 233 | Seal wav into the vault under name, returning its entry. |
Studio::load pub fn | 262 | Open one recording into locked memory. |
Studio::rename pub fn | 281 | Change what a recording is called. |
Studio::remove pub fn | 302 | Remove one recording and its index entry. |
Studio::write_index fn | 319 | Seal the index and replace the file on disk with it. |
Studio::seal fn | 328 | Seal with a fresh nonce, binding aad so a file cannot be moved to another identity inside the same vault and still open. |
Studio::unseal fn | 341 | Open what Studio::seal produced, with the same associated data. |
Studio::unseal_secret fn | 359 | The same, decrypting straight into locked memory. |
ID_LEN const | 375 | How long an identifier is, in characters. |
new_id fn | 386 | A random, opaque identifier: ID_LEN lower-case letters and digits that say nothing about what they name. |
safe_id fn | 401 | Whether id is one this vault could have produced. |
render_index fn | 411 | The index, as lines. |
parse_index fn | 426 | Read the index back: one entry per line, four tab-separated fields. |
Shape pub struct | 456 | What a decoy vault looks like from outside, so it looks like the real one. |
Shape::of pub fn | 473 | Measure a real vault, to build decoys that match it. |
Shape::bytes_on_disk pub fn | 502 | What one vault of this shape occupies, in bytes, as files on a disk. |
Shape::bare_index fn | 516 | The shortest index a decoy of this shape can be written with: every entry present and every name empty. |
Shape::index_len fn | 529 | The index length a decoy of this shape will actually be written with. |
digits fn | 539 | How many decimal digits n is written with. |
vault_dirs pub fn | 559 | Every directory under parent that is shaped like a vault. |
find_or_make pub fn | 602 | Open the one vault under parent that key unlocks, making it on a first run. |
migrate_flat fn | 645 | Move a vault written straight into parent down into a directory of its own. |
make_decoy_in pub fn | 685 | Make one decoy under parent, named the way a real vault is named. |
make_decoy pub fn | 717 | Fill dir with a vault that never held anything. |
pad_index fn | 770 | Grow the names until the index is exactly the length a real one was. |