package onboarding import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "io" "strconv" "strings" "unicode/utf8" "github.com/google/uuid" ) const ContractVersion = "ONBOARDING-1.1" const maxBody = 8 << 10 func digest(b []byte) string { sum := sha256.Sum256(b); return hex.EncodeToString(sum[:]) } func randomToken(n int) string { return base64.RawURLEncoding.EncodeToString(randomBytes(n)) } func randomBytes(n int) []byte { b := make([]byte, n) if _, err := rand.Read(b); err != nil { panic("secure random source unavailable") } return b } func uuidV4(s string) bool { u, e := uuid.Parse(s) return e == nil && u.Version() == 4 && u.String() == s } func uuidValid(s string) bool { u, e := uuid.Parse(s); return e == nil && u.String() == s } func validB64(s string, n int) bool { b, e := base64.RawURLEncoding.DecodeString(s) return e == nil && len(b) == n && base64.RawURLEncoding.EncodeToString(b) == s } func validDigest(s string) bool { b, e := hex.DecodeString(s) return e == nil && len(b) == 32 && strings.ToLower(s) == s } func RequestMAC(key []byte, method, path, deviceID string, version, timestamp int64, nonce, requestID string, body []byte) string { canonical := strings.Join([]string{"DEVREQ1", strings.ToUpper(method), path, deviceID, strconv.FormatInt(version, 10), strconv.FormatInt(timestamp, 10), nonce, requestID, digest(body)}, "\n") return domainMAC(key, canonical) } func domainMAC(key []byte, message string) string { m := hmac.New(sha256.New, key) m.Write([]byte(message)) return hex.EncodeToString(m.Sum(nil)) } type cryptor struct { aead cipher.AEAD hashKey []byte } func newCryptor(master string) (cryptor, error) { key, err := hex.DecodeString(master) if err != nil || len(key) != 32 { return cryptor{}, errors.New("onboarding master key must be 32 bytes encoded as hex") } block, err := aes.NewCipher(key) if err != nil { return cryptor{}, err } aead, err := cipher.NewGCM(block) derived := sha256.Sum256(append([]byte("ONBOARDING-HASH-KEY1:"), key...)) return cryptor{aead: aead, hashKey: derived[:]}, err } func (c cryptor) seal(plain []byte, aad string) []byte { nonce := randomBytes(c.aead.NonceSize()) return c.aead.Seal(nonce, nonce, plain, []byte(aad)) } func (c cryptor) open(value []byte, aad string) ([]byte, error) { n := c.aead.NonceSize() if len(value) < n { return nil, errors.New("invalid encrypted onboarding record") } return c.aead.Open(nil, value[:n], value[n:], []byte(aad)) } func keyAAD(device string, version int64) string { return fmt.Sprintf("ONBOARDING-KEY1\n%s\n%d", device, version) } // SealDeviceKey stores a device HMAC key in the same format consumed by the // physical-confirm authentication path. The plaintext key is returned to the // caller only at device creation or explicit rotation time. func SealDeviceKey(masterKey, deviceID string, version int64, key []byte) ([]byte, error) { if len(key) != 32 || !uuidValid(deviceID) || version < 1 { return nil, errors.New("invalid onboarding device key input") } crypt, err := newCryptor(masterKey) if err != nil { return nil, err } return crypt.seal(key, keyAAD(deviceID, version)), nil } // Validate JSON before decoding structs: Go's regular decoder accepts duplicate // keys and repairs invalid UTF-8, both of which are forbidden by this contract. func StrictJSON(raw []byte, target any) error { if len(raw) == 0 || len(raw) > maxBody || !utf8.Valid(raw) { return fail(400, "INVALID_JSON", "请求正文不合法") } dec := json.NewDecoder(bytes.NewReader(raw)) dec.UseNumber() var visit func(int) error visit = func(depth int) error { if depth > 12 { return errors.New("JSON nesting exceeds 12") } token, err := dec.Token() if err != nil { return err } delim, ok := token.(json.Delim) if !ok { return nil } switch delim { case '{': seen := map[string]bool{} for dec.More() { k, err := dec.Token() if err != nil { return err } name, ok := k.(string) if !ok || seen[name] { return errors.New("duplicate JSON key") } seen[name] = true if err := visit(depth + 1); err != nil { return err } } case '[': for dec.More() { if err := visit(depth + 1); err != nil { return err } } default: return errors.New("invalid JSON delimiter") } _, err = dec.Token() return err } if err := visit(1); err != nil { return fail(400, "INVALID_JSON", "JSON格式、深度或字段重复") } if _, err := dec.Token(); err != io.EOF { return fail(400, "INVALID_JSON", "只能提交一个JSON对象") } trimmed := bytes.TrimSpace(raw) if len(trimmed) == 0 || trimmed[0] != '{' { return fail(400, "INVALID_JSON", "必须提交JSON对象") } decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { return fail(400, "VALIDATION_FAILED", "请求字段或类型不正确") } return nil }