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, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, 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×tamp=%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"` RTCAppIDAlt string `json:"rtc_app_id"` } `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) } codeN := fmt.Sprint(payload.ResponseMetadata.Error.CodeN) if response.StatusCode < 200 || response.StatusCode >= 300 || payload.ResponseMetadata.Error.Code != "" || (payload.ResponseMetadata.Error.CodeN != nil && codeN != "0") { 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 } appID := strings.TrimSpace(payload.Result.RTCAppID) if appID == "" { appID = strings.TrimSpace(payload.Result.RTCAppIDAlt) } if appID == "" { return RegisterResult{}, errors.New("Volcengine registration response missing RTCAppID") } return RegisterResult{DeviceSecret: secret, RTCAppID: appID}, 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 }