-
-
Notifications
You must be signed in to change notification settings - Fork 134
Expand file tree
/
Copy pathapi_key_helper.go
More file actions
146 lines (125 loc) · 4.3 KB
/
api_key_helper.go
File metadata and controls
146 lines (125 loc) · 4.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package util
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"time"
)
const (
// HelperTimeout is the maximum time to wait for the API key helper script to execute
HelperTimeout = 10 * time.Second
// DefaultRefreshInterval is the default interval for refreshing API keys
DefaultRefreshInterval = 900 * time.Second // 15 minutes
// helperKeyPrefix is the credstore key namespace for helper command cache entries.
helperKeyPrefix = "helper:"
)
// apiKeyCache stores cached API keys with their metadata.
// HelperCmd is intentionally omitted: the credstore key already encodes the
// command via SHA-256, so storing the raw command string here would
// unnecessarily persist potentially sensitive command-line arguments.
type apiKeyCache struct {
APIKey string `json:"apiKey"`
LastFetchTime time.Time `json:"lastFetchTime"`
}
// helperCacheKey returns the credstore key for a given helper command.
func helperCacheKey(helperCmd string) string {
hash := sha256.Sum256([]byte(helperCmd))
return helperKeyPrefix + hex.EncodeToString(hash[:])
}
// readCache reads the cached API key from credstore.
func readCache(helperCmd string) (*apiKeyCache, error) {
key := helperCacheKey(helperCmd)
val, err := GetCredential(key)
if err != nil {
return nil, err
}
if val == "" {
return nil, nil //nolint:nilnil // nil cache indicates cache miss, not an error
}
var cache apiKeyCache
if err := json.Unmarshal([]byte(val), &cache); err != nil {
return nil, err
}
return &cache, nil
}
// writeCache writes the API key cache to credstore.
func writeCache(helperCmd, apiKey string) error {
key := helperCacheKey(helperCmd)
cache := apiKeyCache{
APIKey: apiKey,
LastFetchTime: time.Now(),
}
data, err := json.Marshal(cache)
if err != nil {
return err
}
return SetCredential(key, string(data))
}
// needsRefresh checks if the cached key needs to be refreshed
func needsRefresh(cache *apiKeyCache, refreshInterval time.Duration) bool {
if cache == nil {
return true
}
// Always refresh if interval is 0
if refreshInterval == 0 {
return true
}
// Check if cache is expired
return time.Since(cache.LastFetchTime) >= refreshInterval
}
// GetAPIKeyFromHelper executes a shell command to dynamically generate an API key.
// Platform-specific implementations are in api_key_helper_unix.go and api_key_helper_windows.go.
//
// The command is executed with a timeout controlled by the provided context.
// It returns the trimmed output from stdout, or an error if the command fails.
//
// On timeout:
// - Unix/Linux/macOS: kills the entire process group (shell and all descendants)
// - Windows: terminates the Job Object (cmd.exe and all descendants)
//
// Security note: The returned API key is sensitive and should not be logged.
// GetAPIKeyFromHelperWithCache executes a shell command to dynamically generate an API key,
// with credstore-backed caching support. The API key is cached for the specified refresh interval.
// If refreshInterval is 0, the cache is disabled and the command is executed every time.
//
// The cache is stored in the OS keyring (macOS Keychain / Linux Secret Service /
// Windows Credential Manager) with a file-based fallback.
//
// Parameters:
// - ctx: Context for controlling execution and timeouts
// - helperCmd: The shell command to execute
// - refreshInterval: How long to cache the API key (0 to disable caching)
//
// Returns the API key from cache if still valid, otherwise executes the helper command.
//
// Security note: The returned API key is sensitive and should not be logged.
func GetAPIKeyFromHelperWithCache(
ctx context.Context,
helperCmd string,
refreshInterval time.Duration,
) (string, error) {
if helperCmd == "" {
return "", errors.New("api_key_helper command is empty")
}
// Try to read from cache
cache, err := readCache(helperCmd)
if err != nil {
// If cache read fails, log but continue to fetch fresh key
// Don't fail the entire operation just because cache is broken
cache = nil
}
// Check if we need to refresh
if !needsRefresh(cache, refreshInterval) {
return cache.APIKey, nil
}
// Fetch new API key
apiKey, err := GetAPIKeyFromHelper(ctx, helperCmd)
if err != nil {
return "", err
}
// Write to cache (ignore errors to not block the operation)
_ = writeCache(helperCmd, apiKey)
return apiKey, nil
}