Files
margin-calendar/src-tauri
pj 99bbe1113a Bind a stored token to the Mac it was stored on
The machine identifier mixed into the key was read from /etc/machine-id,
which does not exist on macOS, so it was the empty string there. The salt
sits next to the ciphertext and the key context is a constant in a public
binary, which makes that identifier the only thing standing between a copied
home directory and a readable refresh token. Empty is not a neutral
contribution of no entropy, it is the absence of the binding: until now a
home directory lifted off a Mac decrypted anywhere. The file has claimed
otherwise since it was written.

macOS now reads IOPlatformUUID from ioreg, a stock binary called by absolute
path because an app launched from Finder inherits a minimal environment. The
result is cached, since a process spawn is real money when key() runs on
every load and every store. A failure yields an empty id and a working key
rather than an error, because refusing here would lock a user out of a token
that is perfectly good.

iOS and Android stay empty deliberately. The sandbox is the real boundary
there, and every identifier those platforms offer is reset by a reinstall,
which would strand a token that was never in any danger.

No migration, and that is a decision rather than an oversight. Nothing has
been released, so the only install this can orphan is a development machine,
and doing it now costs one reconnect instead of costing every Mac user one
later.

For the same reason the keychain-fallback.* paths are gone. They migrated
nobody, and they were subtly wrong anyway: they carried the old salt across
under the new name while deriving with the new key context, which silently
produced a token that could not be opened. A migration that looks like it
works and does not is worse than no migration.

key() also no longer treats every failure to read the salt as absence. A
missing file and a file too short to be a salt both take a fresh one, since
neither has anything left to lose, but a salt that exists and will not read
is now an error. Writing over it turned one transient read failure into the
permanent loss of every stored token.
2026-08-12 20:16:29 +05:30
..