2026-09-06 17:04:11 +08:00
|
|
|
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),
|
2026-10-08 10:06:44 +08:00
|
|
|
client: &http.Client{Timeout: timeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, now: time.Now,
|
2026-09-06 17:04:11 +08:00
|
|
|
}, 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 {
|
2026-10-08 10:06:44 +08:00
|
|
|
Payload string `json:"payload"`
|
|
|
|
|
RTCAppID string `json:"RTCAppID"`
|
|
|
|
|
RTCAppIDAlt string `json:"rtc_app_id"`
|
2026-09-06 17:04:11 +08:00
|
|
|
} `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)
|
|
|
|
|
}
|
2026-10-08 10:06:44 +08:00
|
|
|
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") {
|
2026-09-06 17:04:11 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-10-08 10:06:44 +08:00
|
|
|
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
|
2026-09-06 17:04:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|