Skip to content

cloud: store the sync token in the OS keyring on every platform - #37

Merged
ijroth merged 1 commit into
mainfrom
cloud/os-keyring-credentials
Aug 27, 2026
Merged

ijroth merged 1 commit into
mainfrom
cloud/os-keyring-credentials

Conversation

@ijroth

@ijroth ijroth commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

docs/v1-spec.md §20 has always said "OS keychain where available". Only macOS
satisfied it — Windows and Linux dropped the sync token into a plaintext
~/.ark/credentials.toml. This is a spec-conformance fix, not a new feature.

Modelled on the GitHub CLI, which has stored tokens in the system credential
store by default since v2.26.

platform before after
macOS Keychain, token in argv Keychain, token on stdin
Windows plaintext TOML Credential Manager (wincred)
Linux plaintext TOML Secret Service, file fallback
headless ARK_TOKEN unchanged, still highest precedence

Resolution stays ARK_TOKEN → keyring → restricted file, and credentials stay
keyed by sync-service host, so one ark login still covers every repository
on that service.

Four defects

  1. Keyring was macOS-only. Now all three, via zalando/go-keyring.

  2. The token was passed in argv. security add-generic-password … -w <token>
    put the secret in the process table, readable by any user on the machine for
    the duration of the call. go-keyring feeds security -i on stdin instead.
    Verified against the real keychain — see below.

  3. Keyring failure was silently swallowed. err == nil guarded the success
    path with no else, so a locked or denied keychain fell straight through to
    plaintext with no word to anyone. That is the bug the GitHub CLI maintainers
    now regret (cli/cli#10108,
    cli/cli#13317). We now say so on
    stderr before touching the file:

    ark: OS keyring unavailable (macOS keychain): exit status 154
    ark: storing the token in /Users/…/.ark/credentials.toml instead — plaintext, protected by mode 0600
    

    A keyring that answers and simply holds nothing is a miss, not a failure,
    and stays quiet — otherwise the warning that matters gets trained away.

  4. Tests could clobber real credentials on Windows — fixed in chore: support Windows development end to end #34, which
    landed first. See Credit below.

Improving on gh rather than copying it

  • Loud fallback, as above. gh degrades silently; we don't.

  • ark status reports where the token came from, the way gh auth status
    does. ark login already named where it wrote the token; resolution is now
    equally legible.

    sync        https://ark.example.com (0 pending mutations)
    token       macOS keychain
    

Also in here

  • The plaintext copy is removed once the keyring holds the token. The
    keyring outranks the file, so a leftover entry is a secret on disk that
    nothing will ever read again. This is the upgrade path for anyone who logged
    in on Windows or Linux before today. Other remotes in the file are untouched;
    the file itself goes when it empties.
  • ARK_NO_KEYRING=1 skips the keyring — gh's --insecure-storage, as an
    env var so it covers resolution too. For a headless box with no Secret
    Service, or a CI image where a prompt would hang. That path is quiet,
    because it was asked for.
  • internal/cli gains a TestMain that redirects HOME/USERPROFILE and
    opts out of the keyring, because ark status now resolves a credential and no
    test should be able to reach the developer's real one.

Interface changes

  • statusReport gains token_source (env | keyring | file | none,
    omitted when no remote is configured). Added, not renamed.
  • ark login --json gains storage (keyring | file) as the machine-readable
    half of stored_in. stored_in keeps its existing values byte-for-byte —
    still macOS keychain or the file path.
  • docs/v1-spec.md §20 rewritten in this PR: explicit precedence list, the
    no-argv rule, the loud-fallback requirement, and ARK_NO_KEYRING.

No CGO

zalando/go-keyring is pure Go — danieljoos/wincred, godbus/dbus/v5,
golang.org/x/sys. 99designs/keyring would have broken the invariant — it
uses cgo on macOS.

Verification

On CI, both jobs pass: Linux (gofmt, vet, build, go test -race ./...)
and the native Windows job #34 added, which runs gofmt, vet, build and
the full go test ./... on windows-latest. That job exercises the Windows ACL
code on the fallback file for real — creds_file_windows_test.go reads the DACL
back and asserts a single current-user ACE — and runs the whole credential suite
under USERPROFILE. Before #34 landed, CI here was Linux-only and none of that
was reachable.

Locally on macOS (darwin/arm64):

  • go build ./..., go test ./... -count=1, gofmt -l . (empty), go vet ./... — all clean
  • Cross-compiled CGO_ENABLED=0: linux/amd64, linux/arm64, darwin/amd64,
    darwin/arm64, windows/amd64, windows/arm64 — all six build
  • go vet under GOOS=windows (amd64 + arm64), linux, darwin — this is
    what type-checks the Windows-only ACL code and its test from a Mac
  • Against the real macOS Keychain, end to end: ark login stores there with
    no warning and writes no file; ark status reports token_source: keyring;
    the stored item is go-keyring-base64:…, proving the secret went in on stdin
    rather than argv; an entry written the old way (raw, via argv) still
    resolves
    , so existing macOS users are not logged out by this change; and a
    planted plaintext token is migrated into the keychain and cleared.

Not verified against a live credential store: Windows Credential Manager and
the Linux Secret Service.
The tests substitute a fake keyring — deliberately,
so the suite can never touch a developer's real credentials, and so a locked or
denied keyring can be simulated at all — which means CI proves those platforms
compile, vet, and pass every test, but not that wincred and D-Bus actually
store and return a secret. Only the macOS store has been driven live, by me,
against the real Keychain. @tylercdmx has the Windows machine if a live
ark login / ark status check is wanted before this lands; on Linux it needs
a session bus, which CI does not have.

Credit

@tylercdmx identified the Windows ACL gap on the fallback file and the
USERPROFILE test-isolation bug (os.UserHomeDir reads USERPROFILE on
Windows, not HOME, so go test ./... could read and write a developer's live
credentials). Both landed in #34, which this builds directly on top of — this PR
does not duplicate that work. #34 closes #33.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LbNXVAkmXtXnSfCWRMwBc9

Spec §20 has always said "OS keychain where available". Only macOS
satisfied it: Windows and Linux dropped the token into a plaintext
~/.ark/credentials.toml. This is the conformance fix.

Resolution stays ARK_TOKEN -> keyring -> restricted file, and credentials
stay keyed by sync-service host, so one `ark login` still covers every
repository on that service.

Three things change beyond the platform coverage:

- The token no longer appears in argv. The old macOS path ran
  `security add-generic-password -w <token>`, which put the secret in the
  process table for any user on the machine to read for the duration of
  the call. go-keyring feeds `security -i` on stdin instead.

- A keyring that fails is no longer silent. The old code guarded the
  success path with `err == nil` and no else, so a locked or denied
  keychain fell straight through to plaintext with no word to anyone.
  That is the bug the GitHub CLI maintainers now regret (cli/cli#10108,
  cli/cli#13317); we say so on stderr before touching the file. A keyring
  that answers and holds nothing is a miss, not a failure, and stays quiet.

- Storage is legible from both ends. `ark login` already named where it
  wrote the token; `ark status` now names where it resolved one from, as
  `token_source` in --json (a new field; nothing renamed) and a `token`
  line for humans. Neither prints the token.

Two smaller consequences of getting a real keyring:

- Once the keyring holds a host's token, `ark login` removes that host's
  entry from the fallback file, and the file itself once it is empty. The
  keyring outranks it, so the copy was a secret on disk nothing would ever
  read again — this is the upgrade path for anyone who logged in on
  Windows or Linux before today.

- ARK_NO_KEYRING=1 skips the keyring for machines where the file is the
  deliberate choice: a headless box with no Secret Service, a CI image
  where a prompt would hang. That path is quiet, because it was asked for.

zalando/go-keyring is pure Go (wincred, godbus, x/sys), so the no-CGO
invariant holds; all six release targets build with CGO_ENABLED=0.
99designs/keyring would not have — it uses cgo on macOS.

internal/cli grows a TestMain that redirects HOME/USERPROFILE and opts out
of the keyring, because `ark status` now resolves a credential and no test
should be able to reach the developer's real one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01LbNXVAkmXtXnSfCWRMwBc9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: support install, development, CI, and project onboarding end to end

1 participant