This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user