@@ -0,0 +1,166 @@
|
||||
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×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"`
|
||||
} `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
|
||||
}
|
||||
Reference in New Issue
Block a user