-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaes.go
More file actions
274 lines (221 loc) · 5.99 KB
/
aes.go
File metadata and controls
274 lines (221 loc) · 5.99 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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
package vault
import (
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"gopkg.in/yaml.v3"
"github.com/flowexec/vault/crypto"
)
const (
aesCurrentVaultVersion = 1
aesVaultFileExt = "enc"
)
// AESState represents the state of the local AES256 vault.
type AESState struct {
Metadata `yaml:"metadata"`
Version int `json:"version"`
ID string `yaml:"id"`
Secrets map[string]string `yaml:"secrets"`
}
// AES256Vault manages operations on an instance of a local vault backed by AES256 symmetric encryption.
type AES256Vault struct {
mu sync.RWMutex
id string
fullPath string
state *AESState
resolver *KeyResolver
dek string
}
// GenerateEncryptionKey generates a new AES encryption key
func GenerateEncryptionKey() (string, error) {
return crypto.GenerateKey()
}
// DeriveEncryptionKey derives an AES encryption key from a passphrase
func DeriveEncryptionKey(passphrase, sal string) (string, string, error) {
key, salt, err := crypto.DeriveKey([]byte(passphrase), []byte(sal))
if err != nil {
return "", "", fmt.Errorf("failed to derive encryption key: %w", err)
}
return key, salt, nil
}
// ValidateEncryptionKey checks if a key is valid by attempting to encrypt/decrypt test data
func ValidateEncryptionKey(key string) error {
testData := "test-validation-data"
encrypted, err := crypto.EncryptValue(key, testData)
if err != nil {
return fmt.Errorf("key validation failed during encryption: %w", err)
}
decrypted, err := crypto.DecryptValue(key, encrypted)
if err != nil {
return fmt.Errorf("key validation failed during decryption: %w", err)
}
if decrypted != testData {
return fmt.Errorf("key validation failed: decrypted data does not match")
}
return nil
}
func NewAES256Vault(cfg *Config) (*AES256Vault, error) {
if cfg.Aes == nil {
return nil, fmt.Errorf("AES configuration is required")
}
path := filepath.Join(
filepath.Clean(cfg.Aes.StoragePath),
filepath.Clean(fmt.Sprintf("%s-%s.%s", vaultFileBase, cfg.ID, aesVaultFileExt)),
)
vault := &AES256Vault{
id: cfg.ID,
fullPath: path,
resolver: NewKeyResolver(cfg.Aes.KeySource),
}
if err := vault.load(); err != nil {
return nil, fmt.Errorf("failed to load vault: %w", err)
}
if vault.state == nil {
if err := vault.init(); err != nil {
return nil, fmt.Errorf("failed to initialize vault: %w", err)
}
}
return vault, nil
}
func (v *AES256Vault) init() error {
keys, err := v.resolver.ResolveKeys()
if err != nil {
return fmt.Errorf("no encryption key available for new vault: %w", err)
}
v.dek = keys[0]
now := time.Now()
v.state = &AESState{
Version: aesCurrentVaultVersion,
ID: v.id,
Metadata: Metadata{
Created: now,
LastModified: now,
},
Secrets: make(map[string]string),
}
return v.save()
}
// load retrieves the AESState from the vault file, decrypts it, and unmarshals it into an AESState struct.
func (v *AES256Vault) load() error {
data, err := os.ReadFile(filepath.Clean(v.fullPath))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("%w: failed to read vault file %s: %w", ErrVaultNotFound, v.fullPath, err)
}
if len(data) == 0 {
return nil
}
// try to decrypt the vault file using available keys
dataStr, key, err := v.resolver.TryDecrypt(string(data))
if err != nil {
return err
}
v.dek = key
var state AESState
if err := yaml.Unmarshal([]byte(dataStr), &state); err != nil {
return fmt.Errorf("failed to unmarshal vault state: %w", err)
}
v.state = &state
return nil
}
// save encrypts and writes the vault contents to disk
func (v *AES256Vault) save() error {
if v.state == nil {
return nil
}
if v.dek == "" {
return fmt.Errorf("no encryption key available for saving")
}
v.state.LastModified = time.Now()
data, err := yaml.Marshal(v.state)
if err != nil {
return fmt.Errorf("failed to marshal vault state: %w", err)
}
encryptedDataStr, err := crypto.EncryptValue(v.dek, string(data))
if err != nil {
return fmt.Errorf("failed to encrypt vault state: %w", err)
}
// write to the file atomically
if err := os.MkdirAll(filepath.Dir(v.fullPath), 0750); err != nil {
return fmt.Errorf("failed to create vault directory: %w", err)
}
tempFile := v.fullPath + ".tmp"
if err := os.WriteFile(tempFile, []byte(encryptedDataStr), 0600); err != nil {
return fmt.Errorf("failed to write temp vault file: %w", err)
}
if err := os.Rename(tempFile, v.fullPath); err != nil {
_ = os.Remove(tempFile)
return fmt.Errorf("failed to move vault file: %w", err)
}
return nil
}
func (v *AES256Vault) ID() string {
return v.id
}
func (v *AES256Vault) Metadata() Metadata {
v.mu.RLock()
defer v.mu.RUnlock()
if v.state == nil {
return Metadata{}
}
return v.state.Metadata
}
func (v *AES256Vault) GetSecret(key string) (Secret, error) {
v.mu.RLock()
defer v.mu.RUnlock()
value, exists := v.state.Secrets[key]
if !exists {
return nil, ErrSecretNotFound
}
return NewSecretValue([]byte(value)), nil
}
func (v *AES256Vault) SetSecret(key string, secret Secret) error {
v.mu.Lock()
defer v.mu.Unlock()
if err := ValidateSecretKey(key); err != nil {
return err
}
if v.state.Secrets == nil {
v.state.Secrets = make(map[string]string)
}
v.state.Secrets[key] = secret.PlainTextString()
return v.save()
}
func (v *AES256Vault) DeleteSecret(key string) error {
v.mu.Lock()
defer v.mu.Unlock()
_, exists := v.state.Secrets[key]
if !exists {
return ErrSecretNotFound
}
delete(v.state.Secrets, key)
return v.save()
}
func (v *AES256Vault) ListSecrets() ([]string, error) {
v.mu.RLock()
defer v.mu.RUnlock()
keys := make([]string, 0, len(v.state.Secrets))
for k := range v.state.Secrets {
keys = append(keys, k)
}
return keys, nil
}
func (v *AES256Vault) HasSecret(key string) (bool, error) {
v.mu.RLock()
defer v.mu.RUnlock()
_, exists := v.state.Secrets[key]
return exists, nil
}
func (v *AES256Vault) Close() error {
// clear the secret state from memory
v.mu.Lock()
defer v.mu.Unlock()
v.dek = ""
v.state = nil
return nil
}