package device import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io" ) type CredentialVault struct{ key []byte } func NewCredentialVault(encodedKey string, allowRandom bool) (*CredentialVault, error) { var key []byte if encodedKey != "" { decoded, err := base64.StdEncoding.DecodeString(encodedKey) if err != nil { return nil, fmt.Errorf("SENSE_CREDENTIAL_KEY must be base64 encoded") } key = decoded } else if allowRandom { key = make([]byte, 32) if _, err := rand.Read(key); err != nil { return nil, err } } if len(key) != 32 { return nil, fmt.Errorf("SENSE_CREDENTIAL_KEY must decode to 32 bytes") } return &CredentialVault{key: key}, nil } func (v *CredentialVault) Encrypt(username, password string) ([]byte, error) { plaintext, err := json.Marshal(map[string]string{"username": username, "password": password}) if err != nil { return nil, err } block, err := aes.NewCipher(v.key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } return gcm.Seal(nonce, nonce, plaintext, []byte("sense-device-credential-v1")), nil } func (v *CredentialVault) Decrypt(ciphertext []byte) (string, string, error) { block, err := aes.NewCipher(v.key) if err != nil { return "", "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", "", err } if len(ciphertext) < gcm.NonceSize() { return "", "", fmt.Errorf("invalid credential ciphertext") } nonce, encrypted := ciphertext[:gcm.NonceSize()], ciphertext[gcm.NonceSize():] plaintext, err := gcm.Open(nil, nonce, encrypted, []byte("sense-device-credential-v1")) if err != nil { return "", "", fmt.Errorf("decrypt device credential: %w", err) } var value struct { Username string `json:"username"` Password string `json:"password"` } if err := json.Unmarshal(plaintext, &value); err != nil { return "", "", err } return value.Username, value.Password, nil }