Files
story/internal/onboarding/ble_session.go
T

60 lines
2.1 KiB
Go
Raw Normal View History

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
}