159 lines
4.4 KiB
Go
159 lines
4.4 KiB
Go
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
|
|
}
|