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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions