Files

120 lines
4.3 KiB
Go
Raw Permalink Normal View History

2026-09-06 17:04:11 +08:00
package hardwareprovision
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
"data-terminal-cloud/internal/deviceauth"
)
func TestHTTPRegistrarSignsAndDecryptsDynamicRegistration(t *testing.T) {
const productSecret = "1234567890abcdef-product-secret"
const deviceSecret = "per-device-secret-with-at-least-32-characters"
var received map[string]any
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if err := json.NewDecoder(request.Body).Decode(&received); err != nil {
t.Fatalf("decode request: %v", err)
}
content := "auth_type=1&device_name=" + received["device_name"].(string) +
"&random_num=" + strconv.FormatInt(int64(received["random_num"].(float64)), 10) +
"&product_key=" + received["product_key"].(string) +
"&timestamp=" + strconv.FormatInt(int64(received["timestamp"].(float64)), 10)
mac := hmac.New(sha256.New, []byte(productSecret))
_, _ = mac.Write([]byte(content))
if received["signature"] != base64.StdEncoding.EncodeToString(mac.Sum(nil)) {
t.Fatal("dynamic registration signature did not match canonical fields")
}
if _, leaked := received["product_secret"]; leaked {
t.Fatal("product secret must not be serialized into the provider request")
}
_ = json.NewEncoder(writer).Encode(map[string]any{"Result": map[string]any{
"payload": encryptDeviceSecretForTest(t, deviceSecret, productSecret), "RTCAppID": "rtc-app",
}})
}))
defer server.Close()
registrar, err := NewHTTPRegistrar(server.URL, productSecret, time.Second)
if err != nil {
t.Fatalf("NewHTTPRegistrar() error = %v", err)
}
registrar.now = func() time.Time { return time.UnixMilli(1_700_000_000_000) }
result, err := registrar.Register(context.Background(), RegisterInput{InstanceID: "instance-1", ProductKey: "product-1", DeviceName: "device-1"})
if err != nil {
t.Fatalf("Register() error = %v", err)
}
if result.DeviceSecret != deviceSecret || result.RTCAppID != "rtc-app" {
t.Fatalf("Register() result = %+v", result)
}
if received["InstanceID"] != "instance-1" || received["product_key"] != "product-1" || received["device_name"] != "device-1" {
t.Fatalf("unexpected registration request: %+v", received)
}
}
func TestDisabledProvisioningDoesNotRequireProductSecret(t *testing.T) {
service, err := NewService(nil, nil, Options{Enabled: false})
if err != nil {
t.Fatalf("NewService() error = %v", err)
}
_, err = service.Provision(context.Background(), deviceauth.Identity{}, "request", ProvisionInput{HardwareID: "device"})
if err != ErrUnavailable {
t.Fatalf("Provision() error = %v, want ErrUnavailable", err)
}
}
func TestHTTPRegistrarRTCAppIDVariantsAndMissingID(t *testing.T) {
for _, field := range []string{"RTCAppID", "rtc_app_id", "missing"} {
t.Run(field, func(t *testing.T) {
const productSecret = "1234567890abcdef-product-secret"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
result := map[string]any{"payload": encryptDeviceSecretForTest(t, "fixture-device-secret-123456789", productSecret)}
if field != "missing" {
result[field] = "rtc-app"
}
_ = json.NewEncoder(w).Encode(map[string]any{"Result": result, "ResponseMetadata": map[string]any{"Error": map[string]any{"CodeN": 0}}})
}))
defer server.Close()
r, err := NewHTTPRegistrar(server.URL, productSecret, time.Second)
if err != nil {
t.Fatal(err)
}
out, err := r.Register(context.Background(), RegisterInput{"instance", "product", "device"})
if field == "missing" {
if err == nil {
t.Fatal("missing RTCAppID was accepted")
}
return
}
if err != nil || out.RTCAppID != "rtc-app" {
t.Fatalf("app ID decoding failed: %v", err)
}
})
}
}
2026-09-06 17:04:11 +08:00
func encryptDeviceSecretForTest(t *testing.T, secret, productSecret string) string {
t.Helper()
key := []byte(productSecret)[:aes.BlockSize]
block, err := aes.NewCipher(key)
if err != nil {
t.Fatal(err)
}
plain := []byte(secret)
padding := aes.BlockSize - len(plain)%aes.BlockSize
for index := 0; index < padding; index++ {
plain = append(plain, byte(padding))
}
encrypted := make([]byte, len(plain))
cipher.NewCBCEncrypter(block, key).CryptBlocks(encrypted, plain)
return base64.StdEncoding.EncodeToString(encrypted)
}