package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io" "os" "path/filepath" "strings" ) // cookieKeyPath returns the filesystem path where the local encryption key is stored. // The key is intentionally NOT stored in recorder_settings.json. func cookieKeyPath() (string, error) { // Prefer ./data (you already use it for models_store.db) p, err := resolvePathRelativeToApp("data/cookies.key") if err == nil && strings.TrimSpace(p) != "" { _ = os.MkdirAll(filepath.Dir(p), 0o755) return p, nil } // Fallback: next to the executable p2, err2 := resolvePathRelativeToApp("cookies.key") if err2 != nil { return "", err2 } _ = os.MkdirAll(filepath.Dir(p2), 0o755) return p2, nil } func loadOrCreateCookieKey() ([]byte, error) { p, err := cookieKeyPath() if err != nil { return nil, err } if b, err := os.ReadFile(p); err == nil { // accept exactly 32 bytes, or trim if longer if len(b) >= 32 { return b[:32], nil } } key := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, key); err != nil { return nil, err } // 0600 is best-effort; on Windows this is still fine. if err := os.WriteFile(p, key, 0o600); err != nil { return nil, err } return key, nil } func normalizeCookieMap(in map[string]string) map[string]string { out := map[string]string{} for k, v := range in { kk := strings.ToLower(strings.TrimSpace(k)) vv := strings.TrimSpace(v) if kk == "" || vv == "" { continue } out[kk] = vv } return out } func encryptCookieMap(cookies map[string]string) (string, error) { clean := normalizeCookieMap(cookies) plain, err := json.Marshal(clean) if err != nil { return "", err } return encryptBytesToBase64(plain) } func decryptCookieMap(blob string) (map[string]string, error) { blob = strings.TrimSpace(blob) if blob == "" { return map[string]string{}, nil } plain, err := decryptBase64ToBytes(blob) if err != nil { return nil, err } var out map[string]string if err := json.Unmarshal(plain, &out); err != nil { return nil, err } return normalizeCookieMap(out), nil } func encryptBytesToBase64(plain []byte) (string, error) { key, err := loadOrCreateCookieKey() if err != nil { return "", err } block, err := aes.NewCipher(key) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := gcm.Seal(nil, nonce, plain, nil) buf := append(nonce, ciphertext...) return base64.StdEncoding.EncodeToString(buf), nil } func decryptBase64ToBytes(blob string) ([]byte, error) { key, err := loadOrCreateCookieKey() if err != nil { return nil, err } block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } buf, err := base64.StdEncoding.DecodeString(blob) if err != nil { return nil, err } ns := gcm.NonceSize() if len(buf) < ns { return nil, fmt.Errorf("encrypted cookies: invalid length") } nonce := buf[:ns] ciphertext := buf[ns:] plain, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return nil, err } return plain, nil }