11package cache
22
33import (
4+ "crypto/aes"
5+ "crypto/cipher"
6+ "crypto/rand"
7+ "encoding/base64"
48 "errors"
59 "fmt"
610 "os"
711 "path/filepath"
812 "regexp"
13+ "strconv"
14+ "time"
15+
16+ "github.com/stackitcloud/stackit-cli/internal/pkg/auth"
917)
1018
1119var (
12- cacheFolderPath string
20+ cacheDirOverwrite string // for testing only
21+ cacheFolderPath string
22+ cacheEncryptionKey []byte
1323
1424 identifierRegex = regexp .MustCompile ("^[a-zA-Z0-9-]+$" )
1525 ErrorInvalidCacheIdentifier = fmt .Errorf ("invalid cache identifier" )
1626)
1727
28+ const (
29+ cacheKeyMaxAge = 90 * 24 * time .Hour
30+ )
31+
1832func Init () error {
19- cacheDir , err := os .UserCacheDir ()
20- if err != nil {
21- return fmt .Errorf ("get user cache dir: %w" , err )
33+ var cacheDir string
34+ if cacheDirOverwrite == "" {
35+ var err error
36+ cacheDir , err = os .UserCacheDir ()
37+ if err != nil {
38+ return fmt .Errorf ("get user cache dir: %w" , err )
39+ }
40+ } else {
41+ cacheDir = cacheDirOverwrite
2242 }
43+
2344 cacheFolderPath = filepath .Join (cacheDir , "stackit" )
45+
46+ // Encryption keys should only be used a limited number of times for aes-gcm.
47+ // Thus, refresh the key periodically. This will invalidate all cached entries.
48+ key , _ := auth .GetAuthField (auth .CACHE_ENCRYPTION_KEY )
49+ age , _ := auth .GetAuthField (auth .CACHE_ENCRYPTION_KEY_AGE )
50+ cacheEncryptionKey = nil
51+ var keyAge time.Time
52+ if age != "" {
53+ ageSeconds , err := strconv .ParseInt (age , 10 , 64 )
54+ if err == nil {
55+ keyAge = time .Unix (ageSeconds , 0 )
56+ }
57+ }
58+ if key != "" && keyAge .Add (cacheKeyMaxAge ).After (time .Now ()) {
59+ cacheEncryptionKey , _ = base64 .StdEncoding .DecodeString (key )
60+ // invalid key length
61+ if len (cacheEncryptionKey ) != 32 {
62+ cacheEncryptionKey = nil
63+ }
64+ }
65+ if len (cacheEncryptionKey ) == 0 {
66+ cacheEncryptionKey = make ([]byte , 32 )
67+ _ , err := rand .Read (cacheEncryptionKey )
68+ if err != nil {
69+ return fmt .Errorf ("cache encryption key: %w" , err )
70+ }
71+ key := base64 .StdEncoding .EncodeToString (cacheEncryptionKey )
72+ err = auth .SetAuthField (auth .CACHE_ENCRYPTION_KEY , key )
73+ if err != nil {
74+ return fmt .Errorf ("save cache encryption key: %w" , err )
75+ }
76+ err = auth .SetAuthField (auth .CACHE_ENCRYPTION_KEY_AGE , fmt .Sprint (time .Now ().Unix ()))
77+ if err != nil {
78+ return fmt .Errorf ("save cache encryption key age: %w" , err )
79+ }
80+ // cleanup old cache entries as they won't be readable anymore
81+ if err := cleanupCache (); err != nil {
82+ return err
83+ }
84+ }
2485 return nil
2586}
2687
@@ -32,7 +93,21 @@ func GetObject(identifier string) ([]byte, error) {
3293 return nil , ErrorInvalidCacheIdentifier
3394 }
3495
35- return os .ReadFile (filepath .Join (cacheFolderPath , identifier ))
96+ data , err := os .ReadFile (filepath .Join (cacheFolderPath , identifier ))
97+ if err != nil {
98+ return nil , err
99+ }
100+
101+ block , err := aes .NewCipher (cacheEncryptionKey )
102+ if err != nil {
103+ return nil , err
104+ }
105+ aead , err := cipher .NewGCMWithRandomNonce (block )
106+ if err != nil {
107+ return nil , err
108+ }
109+
110+ return aead .Open (nil , nil , data , nil )
36111}
37112
38113func PutObject (identifier string , data []byte ) error {
@@ -48,7 +123,17 @@ func PutObject(identifier string, data []byte) error {
48123 return err
49124 }
50125
51- return os .WriteFile (filepath .Join (cacheFolderPath , identifier ), data , 0o600 )
126+ block , err := aes .NewCipher (cacheEncryptionKey )
127+ if err != nil {
128+ return err
129+ }
130+ aead , err := cipher .NewGCMWithRandomNonce (block )
131+ if err != nil {
132+ return err
133+ }
134+ encrypted := aead .Seal (nil , nil , data , nil )
135+
136+ return os .WriteFile (filepath .Join (cacheFolderPath , identifier ), encrypted , 0o600 )
52137}
53138
54139func DeleteObject (identifier string ) error {
@@ -71,3 +156,26 @@ func validateCacheFolderPath() error {
71156 }
72157 return nil
73158}
159+
160+ func cleanupCache () error {
161+ if err := validateCacheFolderPath (); err != nil {
162+ return err
163+ }
164+
165+ entries , err := os .ReadDir (cacheFolderPath )
166+ if err != nil {
167+ if errors .Is (err , os .ErrNotExist ) {
168+ return nil
169+ }
170+ return err
171+ }
172+
173+ for _ , entry := range entries {
174+ name := entry .Name ()
175+ err := DeleteObject (name )
176+ if err != nil && ! errors .Is (err , ErrorInvalidCacheIdentifier ) {
177+ return err
178+ }
179+ }
180+ return nil
181+ }
0 commit comments