package main // Decoding of the retired MMUR1 user-data record (meta-watch record/canonical.rs // at meta-core v1.2.0) and the private-tier envelope (record/crypto.rs). // // record = canonical CBOR array(7) [cid, uid, key, value, version, ts, sig] // bytes_to_sign = "MMUR1\n" || array(6) [cid, uid, key, value, version, ts] // sig = DER ECDSA-secp256k1 over SHA-256(bytes_to_sign) // // value is CBOR null / bool / int / float / text / bytes (bytes = a private-tier // XChaCha20-Poly1305 envelope whose plaintext is the CBOR of the typed value). // The signed bytes are rebuilt from the record's own raw element bytes (array // header 0x87 → 0x86, sig element cut off), so the old signature is checked // against exactly what was signed, whatever float width the encoder chose. import ( "encoding/binary" "errors" "fmt" "math" "strconv" "golang.org/x/crypto/chacha20poly1305" ) const mmur1Domain = "MMUR1\n" type cborKind uint8 const ( cNull cborKind = iota cBool cInt cFloat cText cBytes ) // cborItem is one decoded scalar plus its raw encoding. type cborItem struct { kind cborKind b bool i int64 f float64 s string bytes []byte raw []byte } // wireText is the MMUR2 text form of a typed value — meta-watch's // Value::to_wire_text: bools "true"/"false", ints in decimal, floats as Rust's // f64 Display (shortest round-trip, never an exponent), text verbatim. func (it cborItem) wireText() (string, bool) { switch it.kind { case cBool: if it.b { return "true", true } return "false", true case cInt: return strconv.FormatInt(it.i, 10), true case cFloat: return strconv.FormatFloat(it.f, 'f', -1, 64), true case cText: return it.s, true } return "", false } type mmur1Record struct { CID, UID, Key string Value cborItem Version uint64 TS int64 Sig []byte Signed []byte // the exact MMUR1 bytes the signature covers } var errCBOR = errors.New("malformed MMUR1 CBOR") // readHead parses a CBOR initial byte + argument (definite lengths only). func readHead(b []byte, off int) (major byte, ai byte, n uint64, next int, err error) { if off >= len(b) { return 0, 0, 0, 0, errCBOR } ib := b[off] major, ai = ib>>5, ib&31 off++ var size int switch { case ai < 24: return major, ai, uint64(ai), off, nil case ai == 24: size = 1 case ai == 25: size = 2 case ai == 26: size = 4 case ai == 27: size = 8 default: return 0, 0, 0, 0, fmt.Errorf("%w: indefinite or reserved length", errCBOR) } if off+size > len(b) { return 0, 0, 0, 0, errCBOR } for _, x := range b[off : off+size] { n = n<<8 | uint64(x) } return major, ai, n, off + size, nil } // readItem decodes one scalar item at off. func readItem(b []byte, off int) (cborItem, int, error) { start := off major, ai, n, off, err := readHead(b, off) if err != nil { return cborItem{}, 0, err } var it cborItem switch major { case 0: if n > math.MaxInt64 { return it, 0, fmt.Errorf("%w: int out of range", errCBOR) } it = cborItem{kind: cInt, i: int64(n)} case 1: if n > math.MaxInt64 { return it, 0, fmt.Errorf("%w: int out of range", errCBOR) } it = cborItem{kind: cInt, i: -1 - int64(n)} case 2, 3: if n > uint64(len(b)-off) { return it, 0, errCBOR } end := off + int(n) if major == 2 { it = cborItem{kind: cBytes, bytes: append([]byte(nil), b[off:end]...)} } else { it = cborItem{kind: cText, s: string(b[off:end])} } off = end case 7: switch ai { case 20, 21: it = cborItem{kind: cBool, b: ai == 21} case 22: it = cborItem{kind: cNull} case 25: it = cborItem{kind: cFloat, f: halfToFloat(uint16(n))} case 26: it = cborItem{kind: cFloat, f: float64(math.Float32frombits(uint32(n)))} case 27: it = cborItem{kind: cFloat, f: math.Float64frombits(n)} default: return it, 0, fmt.Errorf("%w: unsupported simple value %d", errCBOR, ai) } default: return it, 0, fmt.Errorf("%w: unsupported major type %d", errCBOR, major) } it.raw = b[start:off] return it, off, nil } func halfToFloat(h uint16) float64 { sign := 1.0 if h&0x8000 != 0 { sign = -1 } exp := int(h>>10) & 0x1f frac := float64(h & 0x3ff) switch exp { case 0: return sign * math.Ldexp(frac, -24) case 31: if frac == 0 { return math.Inf(int(sign)) } return math.NaN() } return sign * math.Ldexp(frac+1024, exp-25) } // decodeMMUR1 parses a stored record and rebuilds its signed bytes. func decodeMMUR1(b []byte) (*mmur1Record, error) { if len(b) == 0 || b[0] != 0x87 { return nil, fmt.Errorf("%w: want array(7)", errCBOR) } off := 1 items := make([]cborItem, 7) sigStart := 0 for i := range items { if i == 6 { sigStart = off } it, next, err := readItem(b, off) if err != nil { return nil, err } items[i], off = it, next } if off != len(b) { return nil, fmt.Errorf("%w: trailing bytes", errCBOR) } for i, want := range []cborKind{cText, cText, cText} { if items[i].kind != want { return nil, fmt.Errorf("%w: cid/uid/key must be text", errCBOR) } } if items[4].kind != cInt || items[4].i < 0 { return nil, fmt.Errorf("%w: version must be an unsigned int", errCBOR) } if items[5].kind != cInt { return nil, fmt.Errorf("%w: ts must be an int", errCBOR) } if items[6].kind != cBytes { return nil, fmt.Errorf("%w: sig must be bytes", errCBOR) } signed := make([]byte, 0, len(mmur1Domain)+sigStart) signed = append(signed, mmur1Domain...) signed = append(signed, 0x86) signed = append(signed, b[1:sigStart]...) return &mmur1Record{ CID: items[0].s, UID: items[1].s, Key: items[2].s, Value: items[3], Version: uint64(items[4].i), TS: items[5].i, Sig: items[6].bytes, Signed: signed, }, nil } // envelopeAAD is cid ‖ 0x1F ‖ uid ‖ 0x1F ‖ key ‖ 0x1F ‖ n_be(8) — n was the // MMUR1 version and is the MMUR2 ts. func envelopeAAD(cid, uid, key string, n uint64) []byte { b := make([]byte, 0, len(cid)+len(uid)+len(key)+11) b = append(b, cid...) b = append(b, 0x1f) b = append(b, uid...) b = append(b, 0x1f) b = append(b, key...) b = append(b, 0x1f) return binary.BigEndian.AppendUint64(b, n) } // openEnvelope decrypts nonce(24) ‖ ciphertext+tag. func openEnvelope(aeadKey, env, aad []byte) ([]byte, error) { a, err := chacha20poly1305.NewX(aeadKey) if err != nil { return nil, err } if len(env) < chacha20poly1305.NonceSizeX+a.Overhead() { return nil, errors.New("envelope too short") } return a.Open(nil, env[:chacha20poly1305.NonceSizeX], env[chacha20poly1305.NonceSizeX:], aad) } // sealEnvelope encrypts plain under a fresh random nonce. func sealEnvelope(aeadKey, plain, aad []byte, nonce []byte) ([]byte, error) { a, err := chacha20poly1305.NewX(aeadKey) if err != nil { return nil, err } out := make([]byte, 0, len(nonce)+len(plain)+a.Overhead()) out = append(out, nonce...) return a.Seal(out, nonce, plain, aad), nil }