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
27 changes: 26 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "writui"
version = "0.17.0"
version = "0.18.0"
edition = "2024"
description = "A terminal writing app. No nonsense. Markdown. Local. Encrypted."
license = "MIT"
Expand All @@ -26,6 +26,14 @@ unicode-width = "0.2.2"
ureq = { version = "3.4.2", default-features = false, features = ["rustls", "json"] }
zeroize = "1.9.0"

# Touch ID, through Apple's frameworks.
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.3"
objc2-core-foundation = { version = "0.3.2", default-features = false, features = ["std", "CFBase", "CFData", "CFDictionary", "CFError", "CFNumber", "CFString"] }
objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSString", "NSError"] }
objc2-local-authentication = { version = "0.3.2", default-features = false, features = ["std", "LAContext"] }
objc2-security = { version = "0.3.2", default-features = false, features = ["std", "SecAccessControl", "SecBase", "SecItem", "SecKey"] }

[dev-dependencies]
tempfile = "3.27.0"

Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ The first time, writui asks you to choose a password (twice) and creates your
vault: the encrypted file that holds all your posts. After that, it asks for
the password each time it starts.

On a Mac with Touch ID, you can unlock with your fingerprint instead: tick
"Unlock with Touch ID" when you create your vault, or turn on "Touch ID" in
Settings (`s` on the list) later. writui then asks for your fingerprint
when it starts; after locking, press Enter or click "Touch ID". The
password still works too. It applies to this Mac only, and adding a new
fingerprint to the Mac turns it off until you next unlock with the
password. Your vault's key, sealed so that only this Mac's Secure Enclave
can open it after a fingerprint, is kept in `~/.config/writui/touch-id/`.

- **Posts** are listed with the most recently changed first. Open one with
Enter or a click, start a new one with `n` (or Enter: "New" is
highlighted when the list opens). The list's shortcuts are single keys,
Expand All @@ -130,7 +139,7 @@ the password each time it starts.
either way round: click "Sort" on the right of the search and pick one,
or press Tab (Shift+Tab) to go through them.
- **Lock** with Ctrl+O, wherever you are: writui asks for the password
again, then takes you back to where you were.
(or your fingerprint) again, then takes you back to where you were.
- **Zen writing:** the editor shows only your words. Hold Ctrl to see the
post's word count and when you started it. (This needs a terminal that can
report Ctrl on its own, like Alacritty, Ghostty, kitty, WezTerm or a recent
Expand Down
40 changes: 40 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,3 +686,43 @@ The creator also rejected a promotion plan that waited two weeks for a
directory listing before attempting another placement. Prepare suitable
opportunities concurrently and progress approved recruitment continuously;
human response time must not block research, preparation, or improvements.

## Touch ID: a Secure Enclave key, not the keychain

The creator asked to unlock with a fingerprint. The obvious route, the
vault's key in the macOS keychain behind Touch ID, doesn't suit writui:
keychain items with biometric protection need the app to be signed with
an Apple developer identity and entitlements, and plain keychain items are
tied to the binary's code signature. writui's releases are only ad-hoc
signed, so every upgrade would make macOS ask for the Mac's password before
the new binary could read the item. (Letting any app read it would avoid
that, but then any program running as the user could read the key without
a fingerprint.)

Instead, turning Touch ID on makes a P-256 key in the Secure Enclave that's
"not permanent" (so not in the keychain, needing no entitlements), with
access control `privateKeyUsage | biometryCurrentSet`, and encrypts the
vault's raw 32-byte SQLCipher key to it (ECIES, X9.63 SHA-256, AES-GCM).
The Secure Enclave gives back an opaque reference to its key (attribute
`toid`, Apple's `kSecAttrTokenOID`), which brings the same key back when
passed to `SecKeyCreateRandomKey`; this is what CryptoKit's
`SecureEnclave…PrivateKey(dataRepresentation:)` does. The reference and the
encrypted key go in `<config dir>/touch-id/<first 16 bytes of SHA-256 of
the vault's canonical path, hex>`, mode 0600. They're useless off this Mac,
and decrypting needs a fingerprint every time, enforced by the Secure
Enclave rather than by writui. Tested working from an unsigned binary, and
across binaries (so across upgrades). Bindings: the `objc2-security` /
`objc2-local-authentication` crates, macOS only; elsewhere Touch ID is
simply unavailable.

`biometryCurrentSet` (rather than `biometryAny` or `userPresence`) means
adding a fingerprint voids the key, so someone who knows the Mac's password
can't enrol their own finger to read the vault. The prompt has no "Use
Password…" fallback: the vault's password goes into writui. When the key
stops working, or doesn't open the vault (it was replaced), the next
password unlock quietly makes a new one.

`Vault` now keeps its key in memory while open (SQLCipher holds a copy
anyway), so Touch ID can be turned on from Settings without retyping the
password. Opening with the key skips Argon2, so Touch ID unlocks are
instant. Changing the password, when it lands, must redo the Touch ID file.
4 changes: 3 additions & 1 deletion docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Ctrl/Cmd+arrow jumps in `0.15.0`, and in `0.16.0` the logo as a big banner
on the password screens, boxed password fields, Esc to clear or quit, and
the whole repo run through `cargo fmt` (checked in CI). `0.17.0` narrows
the logo, adds an Unlock button, trims the setup screen, and makes Esc on
the list quit without asking. Next:
the list quit without asking. `0.18.0` unlocks with Touch ID on Macs
(asked for out of order). Next:

1. **Try online** (writui.com/try), below: the creator wants it next, and
it must work on a phone. Probably two or three PRs, in the order listed.
Expand Down Expand Up @@ -193,6 +194,7 @@ The creator's batch of ideas, after M3.
## Later

- [ ] Homebrew tap (maybe)
- [x] Unlock with Touch ID on Macs (`0.18.0`)
- [ ] Auto-lock after inactivity
- [ ] Change password
- [ ] Diffs between versions
Expand Down
32 changes: 31 additions & 1 deletion docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,37 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md).

So "Create your vault" no longer has its own paragraph about the
password being unrecoverable: the Overview's first two points say it.
- Change password — planned.

> can we make it so that the password can be stored in the mac keychain
> or whatever so that i can unlock with my fingerprint?

- **Touch ID** (Macs with it): Settings has a "Touch ID" switch, shown only
where the Mac can use it. Once it's on, starting writui asks for a
fingerprint straight away; after locking (Ctrl+O), Enter (with no
password typed) or the "Touch ID" button asks for one. Cancelling the
prompt says "Touch ID cancelled." and leaves the password to type, which
always works too.

> okay, just one more thing, i want a [ ] Unlock with Touch ID checkbox
> on the screen where you are setting up the password. And remove the
> "Enter tries again, or type your password" comment. Also, keep the
> "Unlock" button on the password page, looks like you just replaced it
> with "

"Create your vault" has a "[ ] Unlock with Touch ID" box under the
password boxes, where the Mac can use it, unticked at first; a click or
Ctrl+T ticks it.

> you know, i don't want an x in th echeckbox, i want a tick

Ticked, it reads "[✓] Unlock with Touch ID". The unlock screen keeps its "Unlock" button, with a
separate "Touch ID" button right of it where Touch ID is on. It's per
Mac and per vault, kept outside the vault, and survives upgrades. It's
never offered over SSH. If it stops working (e.g. a fingerprint was
added, which deliberately voids it), writui says so, and the next
password unlock sets it up again. Where it's available but off, the
unlock screen's Overview gets one more line pointing to Settings.
- Change password — planned (it must set Touch ID up again).

## The screens

Expand Down
3 changes: 2 additions & 1 deletion site/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
- **Pick up where you left off.** Your writing saves automatically. Reopen
a post and your cursor is right where you left it.
- **Keep your writing private.** Your posts stay on your computer in a
password-encrypted vault. Lock it when you step away.
password-encrypted vault. Lock it when you step away; unlock with
Touch ID.
- **Try another draft. Keep the first.** Save a named version before you
make changes. Read or restore it whenever you want.
- **Use the controls you know.** Click, scroll and select, or use keyboard
Expand Down
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,10 @@ fn expand_tilde(path: PathBuf) -> PathBuf {
_ => path,
}
}

/// Where Touch ID keeps what it needs to unlock each vault: next to the
/// config file, as it's needed before unlocking and belongs to this Mac.
pub fn touch_id_dir() -> Result<PathBuf> {
let config = config_path()?;
Ok(config.parent().unwrap_or(Path::new(".")).join("touch-id"))
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod config;
mod files;
mod touch_id;
mod tui;
mod upgrade;
mod vault;
Expand Down
Loading
Loading