Files

173 lines
5.0 KiB
Go
Raw Permalink Normal View History

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
}