Files
story/internal/hardwareprovision/registrar.go
T
Codex Local 0c2151ace9
ci / verify (push) Canceled after 0s
chore: checkpoint 第一版
2026-09-06 17:04:11 +08:00

167 lines
5.9 KiB
Go

package hardwareprovision
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"strings"
"time"
)
const defaultDynamicRegisterURL = "https://iot-cn-shanghai.iot.volces.com/2021-12-14/DynamicRegister?Action=DynamicRegister&Version=2021-12-14"
type RegisterInput struct {
InstanceID string
ProductKey string
DeviceName string
}
type RegisterResult struct {
DeviceSecret string
RTCAppID string
}
type Registrar interface {
Register(context.Context, RegisterInput) (RegisterResult, error)
}
type HTTPRegistrar struct {
endpoint string
productSecret string
client *http.Client
now func() time.Time
}
func NewHTTPRegistrar(endpoint, productSecret string, timeout time.Duration) (*HTTPRegistrar, error) {
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
endpoint = defaultDynamicRegisterURL
}
if !strings.HasPrefix(endpoint, "https://") && !strings.HasPrefix(endpoint, "http://127.0.0.1:") && !strings.HasPrefix(endpoint, "http://localhost:") {
return nil, errors.New("Volcengine dynamic registration URL must use HTTPS")
}
if len([]byte(strings.TrimSpace(productSecret))) < 16 {
return nil, errors.New("Volcengine hardware product secret must contain at least 16 bytes")
}
if timeout <= 0 {
return nil, errors.New("Volcengine hardware registration timeout must be positive")
}
return &HTTPRegistrar{
endpoint: endpoint, productSecret: strings.TrimSpace(productSecret),
client: &http.Client{Timeout: timeout}, now: time.Now,
}, nil
}
func (r *HTTPRegistrar) Register(ctx context.Context, input RegisterInput) (RegisterResult, error) {
randomNumber, err := rand.Int(rand.Reader, big.NewInt(2_147_483_646))
if err != nil {
return RegisterResult{}, fmt.Errorf("generate dynamic registration nonce: %w", err)
}
randomValue := randomNumber.Int64() + 1
timestamp := r.now().UTC().UnixMilli()
content := fmt.Sprintf("auth_type=1&device_name=%s&random_num=%d&product_key=%s&timestamp=%d", input.DeviceName, randomValue, input.ProductKey, timestamp)
mac := hmac.New(sha256.New, []byte(r.productSecret))
_, _ = mac.Write([]byte(content))
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
body, err := json.Marshal(map[string]any{
"InstanceID": input.InstanceID, "product_key": input.ProductKey, "device_name": input.DeviceName,
"random_num": randomValue, "timestamp": timestamp, "auth_type": 1, "signature": signature,
})
if err != nil {
return RegisterResult{}, fmt.Errorf("encode dynamic registration request: %w", err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, r.endpoint, strings.NewReader(string(body)))
if err != nil {
return RegisterResult{}, fmt.Errorf("create dynamic registration request: %w", err)
}
request.Header.Set("Accept", "application/json")
request.Header.Set("Content-Type", "application/json")
response, err := r.client.Do(request)
if err != nil {
return RegisterResult{}, fmt.Errorf("call Volcengine dynamic registration: %w", err)
}
defer response.Body.Close()
raw, err := io.ReadAll(io.LimitReader(response.Body, 64<<10))
if err != nil {
return RegisterResult{}, fmt.Errorf("read Volcengine dynamic registration response: %w", err)
}
var payload struct {
ResponseMetadata struct {
Error struct {
Code string `json:"Code"`
CodeN any `json:"CodeN"`
Message string `json:"Message"`
} `json:"Error"`
} `json:"ResponseMetadata"`
Result struct {
Payload string `json:"payload"`
RTCAppID string `json:"RTCAppID"`
} `json:"Result"`
Message string `json:"message"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
return RegisterResult{}, fmt.Errorf("decode Volcengine dynamic registration response: HTTP %d", response.StatusCode)
}
if response.StatusCode < 200 || response.StatusCode >= 300 || payload.ResponseMetadata.Error.Code != "" || payload.ResponseMetadata.Error.CodeN != nil {
code := payload.ResponseMetadata.Error.Code
if code == "" {
code = fmt.Sprintf("HTTP_%d", response.StatusCode)
}
message := strings.TrimSpace(payload.ResponseMetadata.Error.Message)
if message == "" {
message = strings.TrimSpace(payload.Message)
}
if message == "" {
message = "Volcengine dynamic registration failed"
}
return RegisterResult{}, fmt.Errorf("%s: %s", code, message)
}
secret, err := decryptRegisteredDeviceSecret(payload.Result.Payload, r.productSecret)
if err != nil {
return RegisterResult{}, err
}
return RegisterResult{DeviceSecret: secret, RTCAppID: strings.TrimSpace(payload.Result.RTCAppID)}, nil
}
func decryptRegisteredDeviceSecret(encoded, productSecret string) (string, error) {
keyMaterial := []byte(productSecret)
if len(keyMaterial) < aes.BlockSize {
return "", errors.New("product secret is too short to decrypt device credential")
}
key := keyMaterial[:aes.BlockSize]
encrypted, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
if err != nil || len(encrypted) == 0 || len(encrypted)%aes.BlockSize != 0 {
return "", errors.New("Volcengine dynamic registration returned an invalid device credential")
}
block, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("create device credential decryptor: %w", err)
}
plain := make([]byte, len(encrypted))
cipher.NewCBCDecrypter(block, key).CryptBlocks(plain, encrypted)
padding := int(plain[len(plain)-1])
if padding < 1 || padding > aes.BlockSize || padding > len(plain) {
return "", errors.New("Volcengine device credential padding is invalid")
}
for _, value := range plain[len(plain)-padding:] {
if int(value) != padding {
return "", errors.New("Volcengine device credential padding is invalid")
}
}
secret := strings.TrimSpace(string(plain[:len(plain)-padding]))
if len(secret) < 16 || len(secret) > 4096 {
return "", errors.New("Volcengine device credential length is invalid")
}
return secret, nil
}