bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
82 lines
3.1 KiB
Go
82 lines
3.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package keychain provides cross-platform secure storage for secrets.
|
|
// macOS uses the system Keychain; Linux uses AES-256-GCM encrypted files; Windows uses DPAPI + registry.
|
|
package keychain
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
)
|
|
|
|
var (
|
|
// ErrNotFound is returned when the requested credential is not found.
|
|
ErrNotFound = errors.New("keychain: item not found")
|
|
|
|
// errNotInitialized is an internal error indicating the master key is missing or invalid.
|
|
errNotInitialized = errors.New("keychain not initialized")
|
|
)
|
|
|
|
const (
|
|
// LarkCliService is the unified keychain service name for all secrets
|
|
// (both AppSecret and UAT). Entries are distinguished by account key format:
|
|
// - AppSecret: "appsecret:<appId>"
|
|
// - UAT: "<appId>:<userOpenId>"
|
|
LarkCliService = "lark-cli"
|
|
)
|
|
|
|
// wrapError wraps underlying keychain failures into a typed *errs.APIError
|
|
// (exit code 1) carrying a hint for troubleshooting keychain access issues.
|
|
// nil and ErrNotFound pass through unchanged.
|
|
func wrapError(op string, err error) error {
|
|
if err == nil || errors.Is(err, ErrNotFound) {
|
|
return err
|
|
}
|
|
|
|
msg := fmt.Sprintf("keychain %s failed: %v", op, err)
|
|
hint := "Check if the OS keychain/credential manager is locked or accessible. If running inside a sandbox or CI environment, please ensure the process has the necessary permissions to access the keychain, you can try running this outside the sandbox."
|
|
|
|
if errors.Is(err, errNotInitialized) {
|
|
hint = "The keychain master key may have been cleaned up or deleted. If running inside a sandbox or CI environment, please ensure the process has the necessary permissions to access the keychain, you can try running this outside the sandbox. Otherwise, please reconfigure the CLI by running lark-cli config init."
|
|
}
|
|
hint += extraHint(err)
|
|
|
|
func() {
|
|
defer func() { recover() }()
|
|
LogAuthError("keychain", op, fmt.Errorf("keychain %s error: %w", op, err))
|
|
}()
|
|
|
|
return errs.NewAPIError(errs.SubtypeUnknown, "%s", msg).
|
|
WithHint("%s", hint).
|
|
WithCause(err)
|
|
}
|
|
|
|
// KeychainAccess abstracts keychain Get/Set/Remove for dependency injection.
|
|
// Used by AppSecret operations (ForStorage, ResolveSecretInput, RemoveSecretStore).
|
|
// UAT operations in token_store.go use the package-level Get/Set/Remove directly.
|
|
type KeychainAccess interface {
|
|
Get(service, account string) (string, error)
|
|
Set(service, account, value string) error
|
|
Remove(service, account string) error
|
|
}
|
|
|
|
// Get retrieves a value from the keychain.
|
|
// Returns empty string if the entry does not exist.
|
|
func Get(service, account string) (string, error) {
|
|
val, err := platformGet(service, account)
|
|
return val, wrapError("Get", err)
|
|
}
|
|
|
|
// Set stores a value in the keychain, overwriting any existing entry.
|
|
func Set(service, account, data string) error {
|
|
return wrapError("Set", platformSet(service, account, data))
|
|
}
|
|
|
|
// Remove deletes an entry from the keychain. No error if not found.
|
|
func Remove(service, account string) error {
|
|
return wrapError("Remove", platformRemove(service, account))
|
|
}
|