bluesky viewer in the terminal
1package config
2
3import (
4 "crypto/aes"
5 "crypto/cipher"
6 "crypto/rand"
7 "crypto/sha256"
8 "encoding/base64"
9 "errors"
10 "io"
11 "os"
12)
13
14// EncryptToken encrypts a plaintext token using AES-256-GCM with a key derived from SHA256 as a [base64]-encoded ciphertext with prepended nonce.
15// The encryption key is derived from either SKYCLI_SECRET env var or machine-specific identifier.
16func EncryptToken(plaintext string) (string, error) {
17 if plaintext == "" {
18 return "", nil
19 }
20
21 key, err := getDerivedKey()
22 if err != nil {
23 return "", err
24 }
25
26 block, err := aes.NewCipher(key)
27 if err != nil {
28 return "", &CryptoError{Op: "NewCipher", Err: err}
29 }
30
31 gcm, err := cipher.NewGCM(block)
32 if err != nil {
33 return "", &CryptoError{Op: "NewGCM", Err: err}
34 }
35
36 nonce := make([]byte, gcm.NonceSize())
37 if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
38 return "", &CryptoError{Op: "GenerateNonce", Err: err}
39 }
40
41 ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
42 return base64.StdEncoding.EncodeToString(ciphertext), nil
43}
44
45// DecryptToken decrypts a base64-encoded token encrypted with [EncryptToken].
46// Returns the original plaintext token or an error if decryption fails.
47func DecryptToken(encrypted string) (string, error) {
48 if encrypted == "" {
49 return "", nil
50 }
51
52 key, err := getDerivedKey()
53 if err != nil {
54 return "", err
55 }
56
57 ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
58 if err != nil {
59 return "", &CryptoError{Op: "DecodeBase64", Err: err}
60 }
61
62 block, err := aes.NewCipher(key)
63 if err != nil {
64 return "", &CryptoError{Op: "NewCipher", Err: err}
65 }
66
67 gcm, err := cipher.NewGCM(block)
68 if err != nil {
69 return "", &CryptoError{Op: "NewGCM", Err: err}
70 }
71
72 nonceSize := gcm.NonceSize()
73 if len(ciphertext) < nonceSize {
74 return "", &CryptoError{Op: "DecryptToken", Err: errors.New("ciphertext too short")}
75 }
76
77 nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
78 plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
79 if err != nil {
80 return "", &CryptoError{Op: "Decrypt", Err: err}
81 }
82
83 return string(plaintext), nil
84}
85
86// getDerivedKey derives a 32-byte AES key from either SKYCLI_SECRET env var or a combination of hostname and username.
87// Uses SHA256 for key derivation.
88func getDerivedKey() ([]byte, error) {
89 secret := os.Getenv("SKYCLI_SECRET")
90 if secret == "" {
91 hostname, err := os.Hostname()
92 if err != nil {
93 return nil, &CryptoError{Op: "GetHostname", Err: err}
94 }
95 username := os.Getenv("USER")
96 if username == "" {
97 username = os.Getenv("USERNAME") // Windows
98 }
99 secret = hostname + ":" + username
100 }
101
102 hash := sha256.Sum256([]byte(secret))
103 return hash[:], nil
104}
105
106// CryptoError represents an error that occurred during cryptographic operations
107type CryptoError struct {
108 Op string
109 Err error
110}
111
112func (e *CryptoError) Error() string {
113 return "crypto." + e.Op + ": " + e.Err.Error()
114}
115
116func (e *CryptoError) Unwrap() error {
117 return e.Err
118}
119
120var _ error = &CryptoError{}