Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ A personal desktop app (Mac and Windows) for keeping secrets encrypted and store
The single encrypted file that holds all of the owner's **Notes**; the same file opens on Mac and Windows.
_Avoid_: database, store, safe, wallet

**Vault folder**:
The folder in the owner's user account on this computer that holds the **Vault** file, **Safety Copies**, settings and the **Quick Unlock** file.
_Avoid_: data folder, container

**Backup**:
A complete copy of the **Vault** file the owner saved outside the app (e.g. on a USB stick) with "Back up now"; it is itself a **Vault** and opens with the same **Master Password** or **Recovery Key**.
_Avoid_: export, snapshot, sync

**Safety Copy**:
An earlier version of this computer's **Vault** that the app keeps automatically (the 10 most recent saves, the first save of each day for 30 days, and whenever opening a **Backup** replaced it), so a damaged or mistakenly replaced **Vault** can be restored.
An earlier version of this computer's **Vault** that the app keeps automatically (the 10 most recent saves, the first save of each day for 30 days, and for at least 30 days whenever opening a **Backup** replaced it), so a damaged or mistakenly replaced **Vault** can be restored.
_Avoid_: old vault, previous version, auto-backup, snapshot

**Emergency Reader**:
Expand All @@ -33,7 +37,7 @@ A long random code generated once at setup that can also open the **Vault** and
_Avoid_: backup code, secret key, reset code

**Key Rotation**:
Replacing the **Vault**'s internal keys and its **Recovery Key** (so the owner writes a new **Recovery Kit**), used when the old **Master Password** or **Recovery Kit** may be known to someone else.
Replacing the **Vault**'s internal keys and its **Recovery Key** (so the owner writes a new **Recovery Kit**), used when the old **Master Password** or **Recovery Kit** may be known to someone else, or the owner lost the paper with the **Recovery Key**.
_Avoid_: reset, re-key, re-encrypt

**Locked** / **Unlocked**:
Expand All @@ -49,7 +53,7 @@ Opening a **Locked** app with Touch ID on a supported Mac after explicit enrollm
_Avoid_: biometric login, fingerprint login

**Recovery Kit**:
The sheet (printed or handwritten) the owner keeps somewhere physical, carrying the **Recovery Key**.
The sheet (printed or handwritten) the owner keeps somewhere physical, carrying the **Recovery Key**. On screen: your paper with your Recovery Key.
_Avoid_: emergency kit, backup sheet

### Working with AI agents
Expand Down Expand Up @@ -147,6 +151,7 @@ _Avoid_: pinned, bookmarked, starred item
- A **Note** in **Trash** is still inside the **Vault** (still encrypted); "Delete forever" removes it from this copy only; older copies still contain it.
- Removing a **Wallet Kind** needs the **Master Password**, like every other change to one: moving it to **Trash**, "Delete forever", and emptying a **Trash** that holds one.
- Changing the **Master Password** changes it only in the copy of the **Vault** where it was changed; older copies still open with the old **Master Password** and old **Recovery Key**.
- Restoring an older copy makes its old **Master Password** and **Recovery Key** current again.
- Changing the **Master Password** keeps the same **Recovery Key**; only **Key Rotation** replaces it.

## Example dialogue
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ An offline, encrypted Vault for Seed Phrases, Private Keys, Logins and API Keys,

## Features

- **Offline Vault.** The Vault has no network code, accounts, sync, updater or telemetry; it's one encrypted file on your computer. An optional [Agent Companion](docs/agent-companion.md), off unless you start it, runs in its own sandbox, can't open the Vault, and sends text an approved agent supplies to OpenAI.
- **Hidden by default.** Secrets stay as dots until you choose Show. Seed Phrases and Private Keys also need your Master Password.
- **Careful on screen shares.** Asks the OS to keep the window out of captures (best effort) and covers it when a screen-sharing app is running.
- **Offline Vault.** The Vault has no network code, accounts, sync, updater or telemetry. Your Notes are saved encrypted in one private folder on your computer, next to their encrypted Safety Copies and a small file of non-secret settings ([where your keys are stored](docs/guide.md#where-your-keys-are-stored)). An optional [Agent Companion](docs/agent-companion.md), off unless you start it, runs in its own sandbox, can't open the Vault, and sends text an approved agent supplies to OpenAI. It keeps its Agent Key in its own plain-text file, not in the Vault.
- **Hidden by default.** Hidden Fields stay as dots until you choose Show. Seed Phrases and Private Keys also need your Master Password.
- **Careful on screen shares.** Asks the OS to keep the window out of captures (best effort) and covers it when a known screen-sharing or recording app is running; meetings inside a web browser aren't detected.
- **Recoverable.** A Recovery Key you write down at setup, and an [Emergency Reader](docs/guide.md#if-the-app-wont-open) that opens your Vault without the app.
- **Portable.** Copy the Vault file to a USB stick to open it on another computer.
- **Portable.** Use Back up now to save an encrypted copy, for example on a USB stick, then [open it on another computer](docs/guide.md#moving-to-another-computer).

> [!WARNING]
> v1 personal project, not independently audited. Keep a paper or metal backup of any seed phrase that holds real funds.
> v1 personal project, not independently audited. The Windows build has not been run on a real Windows PC yet ([#11](https://github.com/nniiovoo/encrypted-note/issues/11)), so it may not work. Don't keep real secrets in it until that's done. Keep a paper or metal backup of any seed phrase that holds real funds.
<!-- Take out the Windows sentences when the #11 results are recorded in docs/security-model.md. -->

## Build

Expand Down Expand Up @@ -62,10 +63,10 @@ Agents should test with fictional data in a separate development Vault using `EN
## Docs

- [Security model](docs/security-model.md): what it protects against, how it works, what has been tested
- [User guide](docs/guide.md): keyboard shortcuts, and reading your Vault if the app won't open
- [User guide](docs/guide.md): where your keys are stored, keyboard shortcuts, and reading your Vault if the app won't open
- [Agent Companion](docs/agent-companion.md): the optional helper for one approved agent, setup and limits
- [File format](docs/FORMAT.md) · [Design decisions](docs/adr/) · [Glossary](CONTEXT.md)

## License

[MIT](LICENSE). The passphrase generator uses the [EFF Long Wordlist](https://www.eff.org/dice) ([CC BY 3.0 US](https://creativecommons.org/licenses/by/3.0/us/)). Seed Phrase checks use the BIP39 English word list via the [`bip39`](https://crates.io/crates/bip39) crate.
[MIT](LICENSE). The suggested Master Password uses the [EFF Long Wordlist](https://www.eff.org/dice) ([CC BY 3.0 US](https://creativecommons.org/licenses/by/3.0/us/)). Seed Phrase checks use the BIP39 English word list via the [`bip39`](https://crates.io/crates/bip39) crate.
14 changes: 10 additions & 4 deletions crates/policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ pub const MIN_GUESSES_LOG10: f64 = 16.0;
const EFF_LIST: &str = include_str!("../data/eff_large_wordlist.txt");
const FAMOUS_PHRASES: &str = include_str!("../data/famous_phrases.txt");

const HINT_LENGTH: &str = "Use at least 15 characters. A few random words work well.";
const HINT_LENGTH: &str =
"Use at least 15 characters. 5 or 6 random words work well, or press Suggest a password.";
const HINT_WELL_KNOWN: &str = "This is a well-known phrase that attackers try early. \
Choose something of your own, or use a suggested passphrase.";
Choose your own words, or press Suggest a password.";
const HINT_GUESSABLE: &str =
"This could still be guessed. Add another uncommon word or two, or use a suggested passphrase.";
"This could still be guessed. Add another unusual word or two, or press Suggest a password.";

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Assessment {
Expand All @@ -59,6 +60,11 @@ pub struct Assessment {
pub feedback: Vec<String>,
}

/// Characters on the NFC form the Vault uses: what the 15-character rule in [`assess`] counts.
pub fn char_count(password: &str) -> usize {
password.nfc().count()
}

pub fn assess(password: &str) -> Assessment {
let nfc: Zeroizing<String> = Zeroizing::new(password.nfc().collect());
let long_enough = nfc.chars().count() >= MIN_CHARS;
Expand Down Expand Up @@ -107,7 +113,7 @@ fn push_unique(feedback: &mut Vec<String>, hint: &str) {
/// The weaker of two readings of the password, as typed and without its separators: zxcvbn's
/// analysis of that reading, and log10 of the guesses it comes to (see [`guesses_log10`]).
///
/// zxcvbn can take seconds on a long password full of symbols and this runs as the owner types,
/// zxcvbn can take seconds on a long password full of symbols and this runs at typing pauses,
/// so the second reading is made only when it can change the answer: the password passes as
/// typed, and taking the separators out leaves something different to judge. Neither reading is
/// cut shorter than zxcvbn's own 100 characters: a cut can land inside a repeat and raise the
Expand Down
3 changes: 2 additions & 1 deletion crates/store/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
//! Vault store: everything about the files on disk. Knows nothing about crypto; it moves
//! opaque bytes and calls a `verify` callback the app supplies (which decrypts in memory).
//!
//! Layout inside the data folder (created 0700 on Unix; files 0600):
//! Layout inside the Vault folder (created 0700 on Unix; files 0600):
//! ```text
//! <root>/ e.g. ~/Library/Application Support/<bundle-id>/Vault.noindex
//! vault.enote the Vault
//! .lock single-instance lock file (OS advisory lock held while open)
//! settings.json non-secret preferences (opaque bytes to this crate)
//! safety/auto-<unix>-<n>.enote automatic Safety Copies (previous version on each save)
//! safety/replaced-<unix>-<n>.enote Safety Copies made when a Backup replaced the Vault
//! quick-unlock.sealed Touch ID registration, written by the app shell (not this crate)
//! .tmp-<random> in-flight writes (removed at open if left over)
//! ```
//!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Changing the Master Password re-wraps keys; Key Rotation is a separate step

Each way into the **Vault** (the **Master Password**, the **Recovery Key**) has its own slot in the header. The slot holds the Vault Key and the Wallet Key (ADR-0004), encrypted under a key derived from that credential with Argon2id. Changing the **Master Password** re-encrypts only the password slot, so it's instant and the **Recovery Kit** stays valid. Replacing the Vault Key would also mean re-encrypting the recovery slot, and that requires the **Recovery Key**, which the app deliberately never stores. So we don't rotate on every password change. We offer **Key Rotation** instead: new Vault Key, new Wallet Key and new **Recovery Key**, re-encrypting every **Note** and showing a new **Recovery Kit**. The change-password screen asks why the owner is changing it. "Someone may know my old password" also runs **Key Rotation**.
Each way into the **Vault** (the **Master Password**, the **Recovery Key**) has its own slot in the header. The slot holds the Vault Key and the Wallet Key (ADR-0004), encrypted under a key derived from that credential with Argon2id. Changing the **Master Password** re-encrypts only the password slot, so it's instant and the **Recovery Kit** stays valid. Replacing the Vault Key would also mean re-encrypting the recovery slot, and that requires the **Recovery Key**, which the app deliberately never stores. So we don't rotate on every password change. We offer **Key Rotation** instead: new Vault Key, new Wallet Key and new **Recovery Key**, re-encrypting every **Note** and showing a new **Recovery Kit**. The change-password screen asks why the owner is changing it. "Someone may know it, or I lost my paper" also runs **Key Rotation**. It needs only the current **Master Password**, so an owner who lost the paper can still get a new **Recovery Key**.

## Consequences

- Without **Key Rotation**, someone holding an old copy plus the old **Master Password** could decrypt a newer copy of the same **Vault** if they obtained it. The UI says so plainly.
- After **Key Rotation** the old **Recovery Kit** no longer opens this copy (or later copies), but it still opens older copies.
- Restoring an older copy makes its old credentials current again.

## Considered Options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ The clipboard choice remains 30 or 60 seconds under its existing policy, and a *

Loosening Auto-lock now collects the Master Password in Rust-owned native controls. Rust freezes the complete effective candidate and checks the current native operation/session before applying it; no password or reusable approval is returned to the WebView. This source implementation of [issue #13](https://github.com/nniiovoo/encrypted-note/issues/13) still needs physical native accessibility/capture acceptance. New Hidden Field input and ordinary Show remain in the renderer under ADR-0003's stated limits.

## Show in the Editor: owner decision, 30 October 2026

The owner approved a Show button in the Editor, so a typed **Login** password, or an **API Key**'s key or secret, can be checked before saving. A mistake there otherwise stays hidden until after the save.

It is off by default, for those fields only: never a **Wallet Kind**, a **Passphrase**, an empty box or a box marked for removal. The box stays a password input. The value is drawn on a separate read-only line under it that can't be selected and isn't announced. It hides on window blur and 30 seconds after the press, and typing doesn't extend that. It reads only the typed text already in the Editor, never asks the core to show a stored value, and isn't kept in the unsaved draft.

This adds time on screen, not a new path for the value: typed values are already in WebView memory under ADR-0003. It is best-effort like any text on screen, and this describes the source until it is tested by hand.

## Considered Options

- **Keep one Note's decrypted Wallet Kind fields for a short "visit" so several actions need one Master Password**: rejected. It is the state ADR-0004 exists to prevent; show-and-copy and the bounded extension cover most of the need.
Expand Down
22 changes: 22 additions & 0 deletions docs/capture-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,28 @@ Use a fake-data Vault for these checks. Copy a fake Hidden Field through the app

Capture Hiding and Sharing Guard are separate observations. A captured shield is not proof of Capture Hiding. Browser meetings are not detected by the Sharing Guard (ADR-0002). File each failure as a separate issue and link it in its row.

### Native panels and focus

Use a fake-data Vault and fake values only. The steps come from the Windows checks in [native-credential-design.md](native-credential-design.md).

| Check | Expected observation | Result / sanitized evidence / issue |
|---|---|---|
| Create and Unlock panels | Each panel opens and stays open | pending |
| Alt-Tab with a panel open | The panel closes, with no value left on screen | pending |
| Alt-Tab with a Hidden Field shown | The value hides and the reveal ends | pending |
| Alt-Tab with lock on app switch turned on | The Vault locks | pending |
| Enter on a focused Cancel | The panel cancels, except on the Recovery Kit step, where the first press only asks | pending |
| Click Cancel once on the Recovery Kit step, then type | The keys go into the box | pending |
| Click Suggest a password, then press Enter | It asks if you wrote it down, and the words don't change | pending |
| A refusal after clicking Continue | The next keys go into a box | pending |
| Type right after a panel opens, and after dragging the window by its title bar | One mask dot per key; otherwise file a separate security issue (ADR-0003) | pending |
| Each key with the core lock held on purpose (needs a temporary debug build) | One more mask dot in a Master Password, Recovery Key, New or Confirm box | pending |
| Recovery Kit retype box with Pinyin or another input method | The input method stays off | pending |
| Recovery Kit retype box during a Retry long enough to say Please wait… | The typed text masks, then unmasks | pending |
| Show in File Explorer | The Vault folder opens | pending |
| A fake value containing `&` | It shows as typed in the panel | pending |
| Panels at 100%, 125%, 150% and 200% scaling | No cut-off text. Record the System font's height at each; a height above 20 px means the panels need scaling work | pending |

### Mac ↔ Windows Backup round trip

Use current builds on both computers, one exFAT USB stick prepared by the owner, and only fake Vaults. No formatting, deletion, or replacement of real data is part of this test. Include at least one fake Note with an optional schema-2 field (for example the purpose of a Login), so portability covers the current file format. Opening a Backup replaces this computer's Vault; confirm the destination is the disposable fake Vault before accepting.
Expand Down
Loading
Loading