Security and cryptography
The primitives, the threat model, and what the app lock is and is not.
This section is also part of the front page, where it sits in context with the rest.
Why the transform cannot be undone
Three independent mechanisms, each individually lossy. Reversing the output means defeating all three.
| Mechanism | What it destroys |
|---|---|
| Phase discard | Every frame's measured phase is thrown away and a synthetic one generated. Phase encodes the exact waveform and the speaker's micro-timing. It is never stored, and infinitely many waveforms share any given magnitude spectrogram. |
| Many-to-one normalisation | Pitch register, vocal-tract length and long-term spectral tilt are each collapsed onto a single canonical value. A whole population of speakers maps to the same output, so there is nothing to invert. |
| CSPRNG modulation | The residual transform changes every frame from a ChaCha20 stream whose seed comes from the OS CSPRNG, lives only in page-locked RAM, and is zeroized on drop. There is no fixed transform to undo. |
| Rolling seed | Every two seconds by default the stream draws a fresh seed from its own output and restarts. ChaCha20 does not run backwards, so each roll permanently seals off the audio before it: a long recording is a chain of short streams, not one. Configurable, and inaudible: parameters glide across a roll and phase offsets ease over half a second. |
At-rest encryption
| Layer | Primitive | Why |
|---|---|---|
| Password → key | Argon2id (RFC 9106) | Memory-hard, so GPU and ASIC cracking gains little. Cost parameters travel with the file so old files still open. |
| Public-key | X25519 + ML-KEM-768 hybrid | An attacker must break both. Guards against harvest-now-decrypt-later: a recording stored today may be attacked decades from now. |
| Payload | XChaCha20-Poly1305 | 192-bit random nonces remove the counter-management failure mode entirely. |
| Header | Authenticated as associated data | An attacker cannot downgrade the stored KDF cost to make cracking cheap, because tampering makes decryption fail. |
| Keys in memory | Page-locked, zeroized, constant-time | Keys stay out of the swap file and are wiped on drop. Comparison leaks no timing. |
Stated plainly: page-locking keeps keys off disk, not away from an attacker who can already read this process's memory, and hibernation writes RAM to disk wholesale and defeats it. A passphrase still sitting in a text field has not reached that protection yet, which is why it is wiped the moment it is used.
Recordings are sealed in memory and written once. An encrypted recording never exists on disk in the clear, because a plaintext file that is written and then deleted cannot be reliably taken back on flash storage.
The app lock, and exactly what it is worth
VeilVoice can sit behind a password of its own, separate from the one that encrypts recordings, so that opening the app is not the same act as unsealing everything it has written.
| What it is | What that means |
|---|---|
| An Argon2id verifier, not a key | A password hash is stored and compared in constant time. It encrypts nothing, because there is nothing local it could usefully encrypt. |
| Rate limited, and the limit persists | Three attempts are free; then the wait doubles from 5 s to a 15-minute cap. The count is written to disk after every attempt, so killing the app does not hand an attacker a fresh budget. |
| Domain separated | Type the same passphrase in both places and you still do not end up with two copies of one value. |
Not tamper-proof, and it cannot be. A local application has nowhere to hide a secret from the machine it runs on. Anyone who can write to your files can delete the lock; anyone holding the disk can edit the attempt counter, move the clock, or attack the stored hash offline. This protects against casual access, meaning the person who sits down at your unlocked session. If the disk is the threat, the answers are full-volume encryption and the at-rest encryption above, not this.
Libre, and what that buys you
- GPL-3.0-or-later. You may use, study, modify and redistribute it; derivatives stay free under the same terms.
- No
unsafeanywhere. Every crate carries#![forbid(unsafe_code)], including the page-locking path. Whole classes of memory-corruption bugs are impossible by construction. - Offline by construction. No telemetry and no accounts, and CI fails the build if an HTTP client so much as enters the dependency graph. One thing reaches the network and only when you press it: the desktop app's check for updates button, which runs then and at no other time, sends nothing about you, and downloads nothing. It borrows your system's own transfer tool, exactly as the release verifier does.
- Reproducible. Pinned toolchain, committed lockfile, path-remapped builds. Rebuild a release and confirm it matches, byte for byte.
- Artwork generated from source. Every icon and the banner come out of a readable script, not a committed binary blob.
- This website too. No CDN, no web fonts, no analytics, no cookies. The only third-party request is the optional repository panel, and it is a button you press.
Audited by tilas01, the author, who wrote and reviewed it. Be clear about what that is worth: a maintainer audit catches what the author can see, and no external firm or independent researcher has reviewed this code. The cryptography uses standard, well-reviewed primitives rather than anything invented here, and the de-identification argument is verifiable by reading two source files. Until an independent review exists, the source is the strongest verification available to you.