60 lines
2.1 KiB
Go
60 lines
2.1 KiB
Go
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
|
||
|
|
}
|