-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkeyring.go
More file actions
257 lines (224 loc) · 7.6 KB
/
Copy pathkeyring.go
File metadata and controls
257 lines (224 loc) · 7.6 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
package keyring
import (
"errors"
"reflect"
"time"
"github.com/launchrctl/launchr"
)
const defaultFileYaml = "keyring.yaml"
// Keyring errors.
var (
ErrNotFound = errors.New("item not found") // ErrNotFound if an item was not found
ErrEmptyFields = errors.New("item can't be empty") // ErrEmptyFields if fields are empty
ErrEmptyPass = errors.New("passphrase can't be empty") // ErrEmptyPass if a passphrase is empty
ErrKeyringMalformed = errors.New("the keyring is malformed") // ErrKeyringMalformed when keyring can't be read.
ErrIncorrectPass = errors.New("the given passphrase is incorrect") // ErrIncorrectPass if a passphrase is incorrect
)
// SecretItem is an interface that represents an item saved in a storage.
// It is used in the DataStore interface for adding and manipulating items.
type SecretItem interface {
isEmpty() bool
}
// CredentialsItem stores credentials.
// Supports both basic auth (username/password) and OAuth (access_token/refresh_token).
type CredentialsItem struct {
URL string `yaml:"url"`
Username string `yaml:"username"`
// AuthType distinguishes between "basic" and "oauth" credentials.
// Empty string is treated as "basic" for backward compatibility.
AuthType string `yaml:"auth_type,omitempty"`
// Basic auth fields
Password string `yaml:"password,omitempty"`
// OAuth fields
AccessToken string `yaml:"access_token,omitempty"`
RefreshToken string `yaml:"refresh_token,omitempty"`
ExpiresAt int64 `yaml:"expires_at,omitempty"`
Issuer string `yaml:"issuer,omitempty"`
TokenEndpoint string `yaml:"token_endpoint,omitempty"`
}
func (i CredentialsItem) isEmpty() bool {
if i.URL == "" || i.Username == "" {
return true
}
// For OAuth, need access token; for basic, need password
if i.AuthType == AuthTypeOAuth {
return i.AccessToken == ""
}
return i.Password == ""
}
// GetSecret returns the secret value for authentication.
// For OAuth credentials, returns the access token.
// For basic credentials, returns the password.
func (i CredentialsItem) GetSecret() string {
if i.AuthType == AuthTypeOAuth {
return i.AccessToken
}
return i.Password
}
// IsOAuth returns true if this is an OAuth credential.
func (i CredentialsItem) IsOAuth() bool {
return i.AuthType == AuthTypeOAuth
}
// IsExpired returns true if OAuth token is expired (with 5 minute buffer).
// Always returns false for basic credentials.
func (i CredentialsItem) IsExpired() bool {
if i.AuthType != AuthTypeOAuth || i.ExpiresAt == 0 {
return false
}
return time.Now().Unix() >= i.ExpiresAt-300
}
// KeyValueItem stores key-value pair.
type KeyValueItem struct {
Key string `yaml:"key"`
Value any `yaml:"value"`
}
func (i KeyValueItem) isEmpty() bool {
if i.Key == "" {
return true
}
if i.Value == nil {
return true
}
// Use reflection to check if the value is its zero value
v := reflect.ValueOf(i.Value)
switch v.Kind() {
case reflect.String: // also handles type alias for string.
return v.String() == ""
case reflect.Slice, reflect.Map, reflect.Array:
return v.Len() == 0
case reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
return v.IsNil()
default:
// For other types, check if it's the zero value
return v.IsZero()
}
}
// DataStore provides password storage functionality.
type DataStore interface {
// Load loads the keyring data from storage.
// This triggers decryption and passphrase prompt if the keyring is encrypted.
// It is idempotent - subsequent calls return immediately if already loaded.
Load() error
// GetUrls retrieves a list of stored URLs.
GetUrls() ([]string, error)
// GetKeys retrieves a list of stored keys.
GetKeys() ([]string, error)
// GetForURL returns a credentials item by a URL.
// Error is returned if either the keyring could not be unlocked
// Error ErrNotFound if the credentials were not found.
GetForURL(url string) (CredentialsItem, error)
// GetForKey returns a key-value item by a key.
// Error is returned if either the keyring could not be unlocked
// Error ErrNotFound if the key was not found.
GetForKey(key string) (KeyValueItem, error)
// AddItem adds a new credential item.
// Error is returned if the vault couldn't be unlocked.
// Error ErrEmptyFields is returned if item is empty.
AddItem(SecretItem) error
// RemoveByURL deletes an item by url.
// Error is returned if the vault couldn't be unlocked.
// Error ErrNotFound if the credentials were not found.
RemoveByURL(url string) error
// RemoveByKey deletes an item by key.
// Error is returned if the vault couldn't be unlocked.
// Error ErrNotFound if the credentials were not found.
RemoveByKey(key string) error
// CleanStorage cleanups storage (credentials or key-value).
// Error is returned if the vault couldn't be unlocked.
CleanStorage(item SecretItem) error
// Exists checks if keyring exists in persistent storage.
Exists() bool
// Save saves the keyring to the persistent storage.
Save() error
// Destroy removes the keyring from the persistent storage.
Destroy() error
}
// dataStore is a type alias to embed it as a private property.
type dataStore = DataStore
// Keyring is a [launchr.Service] providing password store functionality.
type Keyring = *keyringService
type keyringService struct {
dataStore
mask *launchr.SensitiveMask
}
// NewService creates a new Keyring service.
func NewService(store DataStore, mask *launchr.SensitiveMask) Keyring {
return &keyringService{
dataStore: store,
mask: mask,
}
}
// NewFileStore creates a DataStore using a file.
func NewFileStore(f CredentialsFile) DataStore {
if f == nil {
f = nullFile{}
}
return &dataStoreYaml{file: f}
}
// ServiceInfo implements [launchr.Service] interface.
func (k *keyringService) ServiceInfo() launchr.ServiceInfo {
return launchr.ServiceInfo{}
}
func (k *keyringService) ServiceCreate(svc *launchr.ServiceManager) launchr.Service {
var cfg launchr.Config
var mask *launchr.SensitiveMask
svc.Get(&cfg)
svc.Get(&mask)
// Read keyring from a global config directory.
// TODO: parse header to know if it's encrypted or not.
// TODO: do not encrypt if the passphrase is not provided.
store := NewFileStore(
NewAgeFile(
cfg.Path(defaultFileYaml+".age"),
AskPassFirstAvailable{
AskPassConst(passphrase.get),
AskPassWithTerminal{},
},
),
)
return NewService(store, mask)
}
// GetForURL implements DataStore interface. Uses service default store.
func (k *keyringService) GetForURL(url string) (CredentialsItem, error) {
item, err := k.dataStore.GetForURL(url)
if err == nil {
k.maskItem(item)
}
return item, err
}
// GetForKey implements DataStore interface. Uses service default store.
func (k *keyringService) GetForKey(key string) (KeyValueItem, error) {
item, err := k.dataStore.GetForKey(key)
if err == nil {
k.maskItem(item)
}
return item, err
}
// AddItem implements DataStore interface. Uses service default store.
func (k *keyringService) AddItem(item SecretItem) error {
k.maskItem(item)
return k.dataStore.AddItem(item)
}
// Unlock proactively unlocks the keyring.
// This triggers the passphrase prompt if the keyring is encrypted.
// Returns error if the keyring could not be unlocked.
func (k *keyringService) Unlock() error {
return k.dataStore.Load()
}
// MaskItem masks the item values
func (k *keyringService) maskItem(item SecretItem) {
if k.mask == nil {
// Mask may be nil in unit tests for simplicity.
// Mask is checked in e2e tests.
return
}
switch dataItem := item.(type) {
case CredentialsItem:
k.mask.AddString(dataItem.Password)
case KeyValueItem:
if v, ok := dataItem.Value.(string); ok {
k.mask.AddString(v)
}
default:
}
}