-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcrypto.go
More file actions
167 lines (146 loc) · 4.2 KB
/
crypto.go
File metadata and controls
167 lines (146 loc) · 4.2 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
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"log"
"os"
"os/exec"
"runtime"
"strings"
"sync"
)
const encPrefix = "enc:" // prefix for encrypted values in config.json
var (
machineKey []byte
machineKeyOnce sync.Once
)
// getMachineID returns a stable machine identifier.
// macOS: IOPlatformUUID from ioreg
// Windows: MachineGuid from registry
// Linux: /etc/machine-id
func getMachineID() (string, error) {
switch runtime.GOOS {
case "darwin":
out, err := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output()
if err != nil {
return "", fmt.Errorf("ioreg: %w", err)
}
for _, line := range strings.Split(string(out), "\n") {
if strings.Contains(line, "IOPlatformUUID") {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
uuid := strings.TrimSpace(parts[1])
uuid = strings.Trim(uuid, `"`)
return uuid, nil
}
}
}
return "", fmt.Errorf("IOPlatformUUID not found")
case "windows":
cmd := exec.Command("reg", "query",
`HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography`,
"/v", "MachineGuid")
hideWindowCmd(cmd)
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("registry: %w", err)
}
for _, line := range strings.Split(string(out), "\n") {
if strings.Contains(line, "MachineGuid") {
fields := strings.Fields(line)
if len(fields) >= 3 {
return fields[len(fields)-1], nil
}
}
}
return "", fmt.Errorf("MachineGuid not found")
default: // Linux and others
data, err := os.ReadFile("/etc/machine-id")
if err != nil {
return "", fmt.Errorf("machine-id: %w", err)
}
return strings.TrimSpace(string(data)), nil
}
}
// deriveKey creates a 32-byte AES key from the machine ID using SHA-256.
// The salt ensures different apps on the same machine get different keys.
func deriveKey() []byte {
machineKeyOnce.Do(func() {
machineID, err := getMachineID()
if err != nil {
log.Printf("[crypto] WARNING: could not get machine ID: %v — using fallback", err)
machineID = "tsc-bridge-fallback-key"
}
// Salt with app identifier
salted := "tsc-bridge:v3:" + machineID
hash := sha256.Sum256([]byte(salted))
machineKey = hash[:]
})
return machineKey
}
// encryptString encrypts plaintext using AES-256-GCM with the machine key.
// Returns base64-encoded ciphertext prefixed with "enc:".
func encryptString(plaintext string) (string, error) {
if plaintext == "" {
return "", nil
}
key := deriveKey()
block, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("aes cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("gcm: %w", err)
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("nonce: %w", err)
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return encPrefix + base64.StdEncoding.EncodeToString(ciphertext), nil
}
// decryptString decrypts an "enc:"-prefixed base64 string using AES-256-GCM.
// If the value doesn't have the prefix, it's returned as-is (plaintext migration).
func decryptString(encrypted string) (string, error) {
if encrypted == "" {
return "", nil
}
// Not encrypted — return as-is (supports migrating from plaintext configs)
if !strings.HasPrefix(encrypted, encPrefix) {
return encrypted, nil
}
encoded := strings.TrimPrefix(encrypted, encPrefix)
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return "", fmt.Errorf("base64 decode: %w", err)
}
key := deriveKey()
block, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("aes cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("gcm: %w", err)
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", fmt.Errorf("decrypt: %w", err)
}
return string(plaintext), nil
}
// isEncrypted checks if a string value is already encrypted.
func isEncrypted(value string) bool {
return strings.HasPrefix(value, encPrefix)
}