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
11 changes: 7 additions & 4 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ focuses on:

## Platform credential-store adapters

The macOS implementation invokes the system `security` utility without a shell
and sends new vault keys through standard input rather than the process argument
vector. Linux similarly sends values to `secret-tool` over standard input.
Windows calls the native Credential Manager API directly.
The macOS implementation invokes the system `security` utility without a shell.
Because that utility treats `-w` without a value as an interactive prompt rather
than reading standard input, the adapter supplies the vault key directly to
`-w`. The value can therefore be visible briefly to processes running as the
same operating-system user, which is inside this project's trust boundary.
Linux sends values to `secret-tool` over standard input. Windows calls the
native Credential Manager API directly.
10 changes: 8 additions & 2 deletions internal/secretstore/store_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ func (darwinStore) Set(key, value string) error {
if err := validateKey(key); err != nil {
return err
}
command := exec.Command("security", "add-generic-password", "-U", "-a", target(key), "-s", serviceName, "-w")
command.Stdin = strings.NewReader(value + "\n")
command := newDarwinSetCommand(key, value)
if output, err := command.CombinedOutput(); err != nil {
return fmt.Errorf("write macOS Keychain entry: %s: %w", strings.TrimSpace(string(output)), err)
}
return nil
}

func newDarwinSetCommand(key, value string) *exec.Cmd {
// The security CLI treats a trailing -w as a request to prompt twice on a
// terminal; it does not consume a password from stdin. Supply the value as
// the argument to -w so unattended initialization stores the generated key.
return exec.Command("security", "add-generic-password", "-U", "-a", target(key), "-s", serviceName, "-w", value)
}

func (darwinStore) Get(key string) (string, error) {
if err := validateKey(key); err != nil {
return "", err
Expand Down
32 changes: 32 additions & 0 deletions internal/secretstore/store_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//go:build darwin

package secretstore

import (
"os"
"path/filepath"
"testing"
)

func TestDarwinSetSuppliesPasswordToWFlag(t *testing.T) {
directory := t.TempDir()
security := filepath.Join(directory, "security")
stub := `#!/bin/sh
test "$#" -eq 8 || exit 64
test "$1" = "add-generic-password" || exit 65
test "$2" = "-U" || exit 66
test "$3" = "-a" || exit 67
test "$4" = "codex-switch/master-key/test" || exit 68
test "$5" = "-s" || exit 69
test "$6" = "codex-switch" || exit 70
test "$7" = "-w" || exit 71
test "$8" = "generated-vault-key" || exit 72
`
if err := os.WriteFile(security, []byte(stub), 0o700); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", directory)
if err := (darwinStore{}).Set("master-key/test", "generated-vault-key"); err != nil {
t.Fatal(err)
}
}