mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
7a6a4dbdf5
* fix: preserve credential and config read failures Report corrupt or inaccessible stored credentials as typed errors instead of treating them as missing. Preserve config load failures across auth diagnostics and keep trusted request metadata available when host signal collection is unavailable. Fixes #1925 * fix(auth): attach re-authorization recovery to corrupt stored token errors A stored token that fails to decode or fails semantic validation is now reported as internal/storage but carried no recovery hint, so `api --as user`, `auth check`, `auth status`, and `doctor` all ended in a dead end. A new login overwrites the damaged entry, so attach the canonical user-authorization recovery on the two read-side corruption branches. The write-side validator stays hint-free: a rejected write leaves nothing on disk to re-authorize. `auth status` keeps the storage failure in `note` and appends the recovery instead of replacing it. Tests pin the hint on GetStoredToken, GetValidAccessToken, identitydiag (including reduced-distribution projection when auth login is concealed), auth check, and auth status, and pin that SetStoredToken replaces a corrupt entry. ---------
97 lines
3.5 KiB
Go
97 lines
3.5 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"
|
|
"github.com/larksuite/cli/internal/recovery"
|
|
)
|
|
|
|
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.
|
|
// Entries are distinguished by account key format:
|
|
// - AppSecret: "appsecret:<appId>"
|
|
// - Stored TAT: "tat:v1:<sha256(appId)>"
|
|
// - UAT: "<appId>:<userOpenId>"
|
|
LarkCliService = "lark-cli"
|
|
)
|
|
|
|
// wrapError classifies keychain failures at the storage boundary and carries 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)
|
|
func() {
|
|
defer func() { recover() }()
|
|
LogAuthError("keychain", op, fmt.Errorf("keychain %s error: %w", op, err))
|
|
}()
|
|
|
|
return recovery.Attach(
|
|
errs.NewInternalError(errs.SubtypeStorage, "%s", msg).WithCause(err),
|
|
RecoveryHint(err),
|
|
)
|
|
}
|
|
|
|
// RecoveryHint separates generic guidance from command-specific recovery.
|
|
func RecoveryHint(err error) recovery.Hint {
|
|
base := "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."
|
|
parts := []recovery.Part{}
|
|
if errors.Is(err, errNotInitialized) {
|
|
base = "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."
|
|
parts = append(parts, recovery.Text(base), recovery.Command(
|
|
recovery.TargetConfigInit,
|
|
" Otherwise, please reconfigure the CLI by running lark-cli config init.",
|
|
))
|
|
} else {
|
|
parts = append(parts, recovery.Text(base))
|
|
}
|
|
if extra := extraHint(err); extra != "" {
|
|
parts = append(parts, recovery.Command(recovery.TargetConfigKeychainDowngrade, extra))
|
|
}
|
|
return recovery.Join("", parts...)
|
|
}
|
|
|
|
// KeychainAccess abstracts keychain Get/Set/Remove for dependency injection.
|
|
// Used by AppSecret and stored-TAT operations.
|
|
// 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))
|
|
}
|