package onboarding import ( "context" "crypto/hmac" "fmt" "time" "data-terminal-cloud/internal/identity" ) type BLESessionInput struct { DeviceID string `json:"device_id"` Model string `json:"model"` KeyVersion int64 `json:"key_version"` ClientNonce string `json:"client_nonce"` ServerNonce string `json:"server_nonce"` Proof string `json:"proof"` } func bleTranscript(in BLESessionInput) string { return fmt.Sprintf("%s\n%s\n%d\n%s\n%s", in.DeviceID, in.Model, in.KeyVersion, in.ClientNonce, in.ServerNonce) } // A fresh link proves possession of the factory key. Only an eligible signed-in // customer receives its short-lived SRP password over HTTPS, never the factory // key or printed setup password. No account password goes to the device. func (s *Service) BLESession(ctx context.Context, p identity.Principal, in BLESessionInput) (map[string]any, error) { if !s.Enabled() || s.options.PauseClaims { return nil, fail(503, "ENROLLMENT_PAUSED", "设备绑定暂时暂停") } if !factoryPattern.MatchString(in.DeviceID) || in.KeyVersion < 1 || !validB64(in.ClientNonce, 16) || !validB64(in.ServerNonce, 16) || !validDigest(in.Proof) { return nil, errAccess } d, err := s.findFactoryDevice(ctx, in.DeviceID, in.KeyVersion) if err != nil { return nil, err } if !deviceAllowed(d) || d.Model != in.Model || !hmac.Equal([]byte(in.Proof), []byte(domainMAC(d.Key, "BLE-PROOF1\n"+bleTranscript(in)))) { return nil, errAccess } if _, err = s.BLEDeviceState(ctx, p, BLEDeviceInput{DeviceID: in.DeviceID}); err != nil { return nil, err } // A captured bootstrap cannot be redeemed by a different account after use. key := "onboarding:ble-link:" + digest([]byte(in.Proof)) actor := digest([]byte(p.Issuer + "\n" + p.Subject)) if _, err = s.redis.SetNX(ctx, key, actor, 10*time.Minute).Result(); err != nil { return nil, err } owner, err := s.redis.Get(ctx, key).Result() if err != nil { return nil, err } if owner != actor { return nil, errAccess } return map[string]any{"device_id": d.FactoryID, "model": d.Model, "session_password": domainMAC(d.Key, "BLE-PASS1\n"+bleTranscript(in))}, nil }