privatefile.rs

crates/veilvoice-crypto/src/privatefile.rs

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

Writing a file that only its owner can read.

Returns std::io::Result rather than this crate's Error(crate::Error), which is Copy and therefore cannot carry the underlying reason. A caller reporting "could not write the key" is far more useful when it can say why.

Why this is not std::fs::write plus a chmod

std::fs::write creates the file with the process umask, which on almost every Unix system means 0644 -- world readable. Tightening it afterwards with set_permissions leaves a window, however short, in which any other local user can open the file and read all of it. For a file that exists because its contents are sensitive, that window has no reason to exist: OpenOptions::mode applies the permission at the moment of creation, before any byte is written.

The audit found this pattern in three places -- the app-lock verifier, the encrypted private key written by veilvoice keygen, and the plaintext a recording is decrypted into. The verifier one was the worst, because it is rewritten after every failed unlock attempt, so the window reopened on each try. This module is the single answer to all of them.

What this does not do

It is a Unix permission, not a security boundary against root, against someone holding the disk, or against a backup client running as you. It narrows one specific, avoidable exposure: another unprivileged user on the same machine.

On Windows there is no mode. A file created under the user profile inherits an ACL that already excludes other unprivileged users, and there is no portable tightening to apply beyond that -- so on Windows this is an ordinary write, and says so rather than implying a protection it did not obtain.

In plain words

Writes a file that only you can read.

The important part is the order. The permissions are set as the file is created, not afterwards, because a file that exists for even a moment with the wrong permissions is a file somebody else's program may have read in that moment.

When it cannot manage that, it says exactly why rather than just failing, since "could not write the key" is not something anybody can act on.

WHAT THIS FILE CONTAINS

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

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.

  • write_owner_only line 56 · Create path containing bytes, readable only by the current user.
    reaches write_inner
  • write_owner_only_new line 67 · As write_owner_only, but fail if anything is already at path.
    reaches write_inner
  • replace_owner_only line 86 · Replace path with bytes in one step, or leave what was there.
    reaches write_inner
  • tighten line 130 · Make an existing file readable only by its owner.

WHAT CALLS WHAT

write_owner_only line 56 write_owner_only_new line 67 replace_owner_only line 86 tighten line 130 write_inner line 148 entry: a way in: public, and nothing in this file calls it 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_write_owner_only(["write_owner_only<br/>line 56"])
    n_write_owner_only_new(["write_owner_only_new<br/>line 67"])
    n_replace_owner_only(["replace_owner_only<br/>line 86"])
    n_tighten(["tighten<br/>line 130"])
    n_write_inner["write_inner<br/>line 148"]
    n_replace_owner_only --> n_write_inner
    n_write_owner_only --> n_write_inner
    n_write_owner_only_new --> n_write_inner
    click n_write_owner_only href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/privatefile.rs#L56" "open the source"
    click n_write_owner_only_new href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/privatefile.rs#L67" "open the source"
    click n_replace_owner_only href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/privatefile.rs#L86" "open the source"
    click n_tighten href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/privatefile.rs#L130" "open the source"
    click n_write_inner href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/src/privatefile.rs#L148" "open the source"
    classDef entry fill:#1f2335,stroke:#7aa2f7,color:#c0caf5
    class n_write_owner_only,n_write_owner_only_new,n_replace_owner_only,n_tighten entry
    classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
    class n_write_inner 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
write_owner_only pub fn56Create path containing bytes, readable only by the current user.
write_owner_only_new pub fn67As write_owner_only, but fail if anything is already at path.
replace_owner_only pub fn86Replace path with bytes in one step, or leave what was there.
tighten pub fn130Make an existing file readable only by its owner.
write_inner fn148Create or replace a file that only its owner can read.