package hardwareprovision import ( "context" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/hex" "errors" "fmt" "io" "regexp" "strings" "time" "data-terminal-cloud/internal/audit" "data-terminal-cloud/internal/deviceactivation" "data-terminal-cloud/internal/deviceauth" "data-terminal-cloud/internal/devicebinding" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ( ErrUnavailable = errors.New("hardware provider provisioning is unavailable") ErrInvalid = errors.New("invalid hardware provider provisioning input") ErrBindingMissing = errors.New("hardware agent binding is missing") ErrBindingMismatch = errors.New("hardware agent product binding does not match platform provisioning configuration") ErrHardwareMismatch = errors.New("hardware identity does not match the provisioned credential") ErrActivationState = errors.New("device activation state does not allow provider provisioning") ) var hardwareIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_\-.:@]{1,200}$`) type Options struct { Enabled bool InstanceID string ProductKey string BotID string Transport string MinimumSDKVersion string ProductSecret string CredentialMasterKey string RegisterURL string RegisterTimeout time.Duration DeviceNamePrefix string Registrar Registrar } type ProvisionInput struct { HardwareID string `json:"hardware_id"` } type ProvisionResponse struct { InstanceID string `json:"instance_id"` ProductKey string `json:"product_key"` DeviceName string `json:"device_name"` HardwareID string `json:"hardware_id"` DeviceSecret string `json:"device_secret"` CredentialRef string `json:"credential_ref"` CredentialVersion int64 `json:"credential_version"` CredentialSource string `json:"credential_source"` IssuedAt time.Time `json:"issued_at"` } type Service struct { enabled bool pool *pgxpool.Pool audit *audit.Repository registrar Registrar aead cipher.AEAD instanceID string productKey string botID string transport string minimumSDK string deviceNamePrefix string } func NewService(pool *pgxpool.Pool, auditRepository *audit.Repository, options Options) (*Service, error) { service := &Service{enabled: options.Enabled} if !options.Enabled { return service, nil } if pool == nil || auditRepository == nil { return nil, errors.New("hardware provider provisioning requires database and audit repository") } service.instanceID = strings.TrimSpace(options.InstanceID) service.productKey = strings.TrimSpace(options.ProductKey) service.botID = strings.TrimSpace(options.BotID) service.transport = strings.TrimSpace(options.Transport) service.minimumSDK = strings.TrimSpace(options.MinimumSDKVersion) service.deviceNamePrefix = strings.TrimSpace(options.DeviceNamePrefix) if service.deviceNamePrefix == "" { service.deviceNamePrefix = "dt-" } if service.instanceID == "" || len(service.instanceID) > 200 || service.productKey == "" || len(service.productKey) > 200 || service.botID == "" || len(service.botID) > 200 || (service.transport != "high_quality" && service.transport != "low_load") || service.minimumSDK == "" || len(service.minimumSDK) > 80 || !regexp.MustCompile(`^[a-zA-Z0-9_\-.:@]{1,32}$`).MatchString(service.deviceNamePrefix) { return nil, errors.New("hardware provider instance, product defaults and device-name prefix are invalid") } key, err := hex.DecodeString(strings.TrimSpace(options.CredentialMasterKey)) if err != nil || len(key) != 32 { return nil, errors.New("VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY must be 32 bytes encoded as 64 hexadecimal characters") } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("create hardware provider credential cipher: %w", err) } service.aead, err = cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("create hardware provider credential AEAD: %w", err) } service.registrar = options.Registrar if service.registrar == nil { service.registrar, err = NewHTTPRegistrar(options.RegisterURL, options.ProductSecret, options.RegisterTimeout) if err != nil { return nil, err } } service.pool = pool service.audit = auditRepository return service, nil } func (s *Service) Provision(ctx context.Context, identity deviceauth.Identity, requestID string, input ProvisionInput) (ProvisionResponse, error) { if !s.enabled { return ProvisionResponse{}, ErrUnavailable } hardwareID := strings.TrimSpace(input.HardwareID) if !hardwareIDPattern.MatchString(hardwareID) { return ProvisionResponse{}, ErrInvalid } tx, err := s.pool.Begin(ctx) if err != nil { return ProvisionResponse{}, fmt.Errorf("begin hardware provider provisioning: %w", err) } defer tx.Rollback(ctx) if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, identity.ID); err != nil { return ProvisionResponse{}, fmt.Errorf("lock hardware provider provisioning: %w", err) } if err := devicebinding.RequireLegacy(ctx, tx, identity.ID); err != nil { return ProvisionResponse{}, err } activationState, err := s.prepareActivation(ctx, tx, identity, requestID) if err != nil { return ProvisionResponse{}, err } if err := s.ensureProductBinding(ctx, tx, identity); err != nil { return ProvisionResponse{}, err } existing, found, err := s.readExisting(ctx, tx, identity.ID) if err != nil { return ProvisionResponse{}, err } if found { if existing.HardwareID != hardwareID { return ProvisionResponse{}, ErrHardwareMismatch } if existing.InstanceID != s.instanceID || existing.ProductKey != s.productKey { return ProvisionResponse{}, ErrBindingMismatch } secret, err := s.aead.Open(nil, existing.Nonce, existing.EncryptedSecret, credentialAAD(existing.CredentialRef, existing.KeyVersion, existing.InstanceID, existing.ProductKey, existing.DeviceName)) if err != nil { return ProvisionResponse{}, errors.New("decrypt hardware provider device credential") } response := ProvisionResponse{InstanceID: existing.InstanceID, ProductKey: existing.ProductKey, DeviceName: existing.DeviceName, HardwareID: existing.HardwareID, DeviceSecret: string(secret), CredentialRef: existing.CredentialRef, CredentialVersion: existing.KeyVersion, CredentialSource: "platform_stored", IssuedAt: existing.IssuedAt} if existing.Status == "SUSPENDED" { if _, err := tx.Exec(ctx, `UPDATE hardware_agent_provider_credential SET status='ACTIVE',revoked_at=NULL,updated_at=now() WHERE device_id=$1::uuid AND status='SUSPENDED'`, identity.ID); err != nil { return ProvisionResponse{}, fmt.Errorf("reactivate hardware provider credential: %w", err) } } if activationState == deviceactivation.Provisioning { if _, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: identity.ID, TenantID: identity.TenantID, Target: deviceactivation.Active, Reason: "火山运行凭据已就绪", Source: "provider-provisioning", ActorType: "device", ActorPublicID: &identity.PublicID, RequestID: requestID}); err != nil { return ProvisionResponse{}, fmt.Errorf("complete provider activation: %w", err) } } if err := s.recordAudit(ctx, tx, identity, requestID, "hardware_agent.provider_credential.resolve", response, true); err != nil { return ProvisionResponse{}, err } if err := tx.Commit(ctx); err != nil { return ProvisionResponse{}, fmt.Errorf("commit hardware provider credential resolution: %w", err) } return response, nil } var configuredProduct, configuredDeviceName string err = tx.QueryRow(ctx, ` SELECT COALESCE(product_key,''),COALESCE(provider_device_ref,'') FROM hardware_agent_device_binding WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND provider='volcengine' AND status='ACTIVE' FOR UPDATE`, identity.ID, identity.TenantID).Scan(&configuredProduct, &configuredDeviceName) if errors.Is(err, pgx.ErrNoRows) { return ProvisionResponse{}, ErrBindingMissing } if err != nil { return ProvisionResponse{}, fmt.Errorf("read hardware agent provider binding: %w", err) } if configuredProduct != "" && configuredProduct != s.productKey { return ProvisionResponse{}, ErrBindingMismatch } deviceName := strings.TrimSpace(configuredDeviceName) if deviceName == "" { deviceName = s.deviceNamePrefix + strings.ReplaceAll(identity.PublicID, "-", "") } if len(deviceName) > 128 || !regexp.MustCompile(`^[a-zA-Z0-9_\-.:@]{1,128}$`).MatchString(deviceName) { return ProvisionResponse{}, ErrInvalid } registered, err := s.registrar.Register(ctx, RegisterInput{InstanceID: s.instanceID, ProductKey: s.productKey, DeviceName: deviceName}) if err != nil { return ProvisionResponse{}, fmt.Errorf("register hardware provider device: %w", err) } secret := []byte(strings.TrimSpace(registered.DeviceSecret)) if len(secret) < 16 || len(secret) > 4096 { return ProvisionResponse{}, errors.New("hardware provider returned an invalid device credential") } credentialRef := "volc-device-secret://" + identity.PublicID const keyVersion int64 = 1 nonce := make([]byte, s.aead.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return ProvisionResponse{}, fmt.Errorf("generate hardware provider credential nonce: %w", err) } encrypted := s.aead.Seal(nil, nonce, secret, credentialAAD(credentialRef, keyVersion, s.instanceID, s.productKey, deviceName)) issuedAt := time.Now().UTC() _, err = tx.Exec(ctx, ` INSERT INTO hardware_agent_provider_credential( tenant_id,device_id,credential_ref,instance_id,product_key,provider_device_name,hardware_id, key_version,encrypted_secret,nonce,status,issued_at ) VALUES($1::uuid,$2::uuid,$3,$4,$5,$6,$7,$8,$9,$10,'ACTIVE',$11)`, identity.TenantID, identity.ID, credentialRef, s.instanceID, s.productKey, deviceName, hardwareID, keyVersion, encrypted, nonce, issuedAt) if err != nil { return ProvisionResponse{}, fmt.Errorf("store hardware provider device credential: %w", err) } _, err = tx.Exec(ctx, ` UPDATE hardware_agent_device_binding SET product_key=$2,provider_device_ref=$3,credential_ref=$4,updated_at=now() WHERE device_id=$1::uuid`, identity.ID, s.productKey, deviceName, credentialRef) if err != nil { return ProvisionResponse{}, fmt.Errorf("update hardware agent provider binding: %w", err) } response := ProvisionResponse{InstanceID: s.instanceID, ProductKey: s.productKey, DeviceName: deviceName, HardwareID: hardwareID, DeviceSecret: string(secret), CredentialRef: credentialRef, CredentialVersion: keyVersion, CredentialSource: "platform_dynamic_registration", IssuedAt: issuedAt} if activationState == deviceactivation.Provisioning { if _, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: identity.ID, TenantID: identity.TenantID, Target: deviceactivation.Active, Reason: "火山动态注册与运行凭据已就绪", Source: "provider-provisioning", ActorType: "device", ActorPublicID: &identity.PublicID, RequestID: requestID}); err != nil { return ProvisionResponse{}, fmt.Errorf("complete provider activation: %w", err) } } if err := s.recordAudit(ctx, tx, identity, requestID, "hardware_agent.provider_credential.provision", response, false); err != nil { return ProvisionResponse{}, err } if err := tx.Commit(ctx); err != nil { return ProvisionResponse{}, fmt.Errorf("commit hardware provider provisioning: %w", err) } return response, nil } // ensureProductBinding resolves the device runtime from the platform-owned // product defaults. DynamicRegister only creates the provider device identity; // it does not return a Bot ID, so devices must never supply or persist that // routing choice themselves. func (s *Service) ensureProductBinding(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity) error { var runtime string if err := tx.QueryRow(ctx, `SELECT COALESCE(desired_config->>'voice_runtime','legacy_voicechat') FROM device_twin WHERE device_id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, identity.ID, identity.TenantID).Scan(&runtime); errors.Is(err, pgx.ErrNoRows) { return ErrBindingMissing } else if err != nil { return fmt.Errorf("read hardware agent runtime: %w", err) } if runtime != "volc_hardware_agent" { return ErrBindingMissing } _, err := tx.Exec(ctx, ` INSERT INTO hardware_agent_device_binding( tenant_id,device_id,provider,product_key,bot_id,transport,minimum_sdk_version,status ) VALUES($1::uuid,$2::uuid,'volcengine',$3,$4,$5,$6,'ACTIVE') ON CONFLICT(device_id) DO UPDATE SET product_key=EXCLUDED.product_key,bot_id=EXCLUDED.bot_id,transport=EXCLUDED.transport, minimum_sdk_version=EXCLUDED.minimum_sdk_version,status='ACTIVE',updated_at=now()`, identity.TenantID, identity.ID, s.productKey, s.botID, s.transport, s.minimumSDK) if err != nil { return fmt.Errorf("bind hardware agent product defaults: %w", err) } _, err = tx.Exec(ctx, ` UPDATE device_twin SET desired_revision=desired_revision+1, desired_config=desired_config||jsonb_build_object( 'voice_runtime','volc_hardware_agent','hardware_agent_transport',$2::text,'minimum_sdk_version',$3::text ),updated_at=now() WHERE device_id=$1::uuid AND ( COALESCE(desired_config->>'hardware_agent_transport','')<>$2 OR COALESCE(desired_config->>'minimum_sdk_version','')<>$3 )`, identity.ID, s.transport, s.minimumSDK) if err != nil { return fmt.Errorf("publish hardware agent product defaults: %w", err) } return nil } type storedCredential struct { Status string CredentialRef string InstanceID string ProductKey string DeviceName string HardwareID string KeyVersion int64 EncryptedSecret []byte Nonce []byte IssuedAt time.Time } func (s *Service) readExisting(ctx context.Context, tx pgx.Tx, deviceID string) (storedCredential, bool, error) { var item storedCredential err := tx.QueryRow(ctx, ` SELECT status,credential_ref,instance_id,product_key,provider_device_name,hardware_id,key_version,encrypted_secret,nonce,issued_at FROM hardware_agent_provider_credential WHERE device_id=$1::uuid AND provider='volcengine' AND status IN ('ACTIVE','SUSPENDED') FOR UPDATE`, deviceID).Scan(&item.Status, &item.CredentialRef, &item.InstanceID, &item.ProductKey, &item.DeviceName, &item.HardwareID, &item.KeyVersion, &item.EncryptedSecret, &item.Nonce, &item.IssuedAt) if errors.Is(err, pgx.ErrNoRows) { return storedCredential{}, false, nil } if err != nil { return storedCredential{}, false, fmt.Errorf("read hardware provider device credential: %w", err) } return item, true, nil } func (s *Service) prepareActivation(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, requestID string) (deviceactivation.State, error) { var stateText string if err := tx.QueryRow(ctx, `SELECT activation_state FROM device WHERE id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, identity.ID, identity.TenantID).Scan(&stateText); err != nil { return "", fmt.Errorf("read device activation for provisioning: %w", err) } state, err := deviceactivation.Parse(stateText) if err != nil { return "", ErrActivationState } if state == deviceactivation.Bound { result, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: identity.ID, TenantID: identity.TenantID, Target: deviceactivation.Provisioning, Reason: "设备请求火山运行凭据", Source: "provider-provisioning", ActorType: "device", ActorPublicID: &identity.PublicID, RequestID: requestID}) if err != nil { return "", fmt.Errorf("start provider activation: %w", err) } return result.To, nil } if state != deviceactivation.Provisioning && state != deviceactivation.Active { return "", ErrActivationState } return state, nil } func (s *Service) recordAudit(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, requestID, action string, response ProvisionResponse, reused bool) error { return s.audit.Record(ctx, tx, audit.Event{TenantID: &identity.TenantID, ActorType: "device", ActorPublicID: &identity.PublicID, Action: action, ResourceType: "device", ResourceID: &identity.PublicID, RequestID: requestID, Result: "success", Detail: map[string]any{"provider": "volcengine", "product_key": response.ProductKey, "provider_device_name": response.DeviceName, "credential_version": response.CredentialVersion, "reused": reused}}) } func credentialAAD(reference string, version int64, instanceID, productKey, deviceName string) []byte { return []byte(fmt.Sprintf("hardware-provider-credential-v1\n%s\n%d\n%s\n%s\n%s", reference, version, instanceID, productKey, deviceName)) }