Skip to content

feat(keychain): replace go-keyring with keybase/go-keychain + thin wrapper for access group support #90

@neilmartin83

Description

@neilmartin83

Summary

Replace github.com/zalando/go-keyring with github.com/keybase/go-keychain in internal/keychain/ and add a thin CGo wrapper to expose kSecUseDataProtectionKeychain. This is a reliability improvement on its own, and lays the groundwork for cross-app credential sharing with other Jamf tooling.

Motivation

Reliability — subprocess vs. Security.framework

zalando/go-keyring shells out to /usr/bin/security for every keychain operation and parses stdout. This is fragile: output format can vary across macOS versions, and it base64-encodes all stored values (prefixed go-keyring-base64:) to work around shell-escaping constraints. keybase/go-keychain calls Security.framework directly via CGo, which is more robust, faster, and stores values as plain UTF-8 bytes.

Cross-app credential sharing

Other Jamf tooling (e.g. apiutil) stores credentials using kSecAttrAccessGroup and kSecUseDataProtectionKeychain. Items created with an access group are invisible to apps that aren't in the same group — not even a permission prompt. For jamf-cli to read or share credentials with such tools, it needs:

  1. Direct Security.framework bindings that accept kSecAttrAccessGroupkeybase/go-keychain provides this
  2. kSecUseDataProtectionKeychain: true — required whenever an access group is set; not currently exposed by keybase/go-keychain, so needs a thin CGo wrapper

Agreeing on a shared access group + matching provisioning profile is a separate coordination step, but the code changes here are the prerequisite.

Proposed changes

1. Swap the dependency

go get github.com/keybase/go-keychain
go mod tidy  # removes zalando/go-keyring

2. Rewrite internal/keychain/keychain.go

Update systemStore to use keybase/go-keychain instead of go-keyring. The public Store interface (Get, Set, Delete) and ParseRef / KeychainRef helpers are unchanged — callers see no difference.

3. Thin wrapper for kSecUseDataProtectionKeychain

keybase/go-keychain does not expose this constant. Add a small Darwin-only CGo file in internal/keychain/ that sets it when building items destined for an access group:

// keychain_darwin.go
// #cgo LDFLAGS: -framework CoreFoundation -framework Security
// #include <Security/Security.h>
import "C"

// setDataProtection adds kSecUseDataProtectionKeychain to a query dict.
// Required whenever kSecAttrAccessGroup is set.

For the initial implementation this wrapper can be a no-op/unexported — it just needs to exist and be correct before the access group work lands.

4. Migration: handle existing go-keyring-base64: encoded values

zalando/go-keyring stores all passwords with a go-keyring-base64: prefix (base64-encoded, to survive shell escaping). keybase/go-keychain reads raw bytes, so it will return the encoded string verbatim.

Migration must be transparent to users. On Get():

  1. Read the raw value
  2. If it starts with go-keyring-base64:, base64-decode the suffix to recover the real secret
  3. Immediately re-Set() the decoded value so the item is in the new format
  4. Return the decoded value

This one-time per-item migration fires silently on first use after upgrade. Users with existing config profiles referencing keychain: items will continue working without re-running config add-profile.

The legacy prefix go-keyring-encoded: (hex, older format) should also be handled the same way.

Platform impact

Platform Before After
macOS subprocess /usr/bin/security CGo → Security.framework
Linux Secret Service D-Bus (pure Go) Secret Service D-Bus (pure Go) — unchanged
Windows Credential Manager (out of scope — not a target platform)

CGo is gated to Darwin in keybase/go-keychain, so Linux builds remain CGo-free. macOS GitHub Actions runners (macos-latest) have Xcode/clang, so CGo builds work without any CI changes.

Cross-compilation (GOOS=darwin from Linux) will break for the keychain package unless a cross-compiler is configured — worth checking the release workflow.

Acceptance criteria

  • zalando/go-keyring removed from go.mod
  • keybase/go-keychain added; internal/keychain/ rewritten to use it
  • kSecUseDataProtectionKeychain wrapper exists (Darwin-only, even if not yet wired to a public API)
  • Get() transparently migrates go-keyring-base64: / go-keyring-encoded: prefixed values
  • Existing keychain: config profile references continue to work (no re-setup required)
  • Tests pass on macOS and Linux
  • ParseRef / KeychainRef / DefaultService unchanged (no config format changes)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions