195 lines
4.5 KiB
Go
195 lines
4.5 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/niko/qcc/pkg/types"
|
|
)
|
|
|
|
type FrameHeader struct {
|
|
Magic [4]byte
|
|
Version uint16
|
|
FrameID uint64
|
|
PrevFrameID uint64
|
|
PrevHash [32]byte
|
|
Timestamp int64
|
|
NumRecords uint32
|
|
}
|
|
|
|
const FrameHeaderSize = 4 + 2 + 8 + 8 + 32 + 8 + 4
|
|
const RecordHeaderSize = 1 + 1 + 2 + 8 + 4 + 12 + 16
|
|
|
|
type RecordHeader struct {
|
|
RecType types.RecType
|
|
Flags uint8
|
|
TableID uint16
|
|
KeyHash uint64
|
|
PayloadSize uint32
|
|
Nonce [12]byte
|
|
AuthTag [16]byte
|
|
}
|
|
|
|
type Frame struct {
|
|
Header FrameHeader
|
|
Records []Record
|
|
}
|
|
|
|
type Record struct {
|
|
Header RecordHeader
|
|
Payload []byte
|
|
}
|
|
|
|
func NewFrameHeader(prevFrameID uint64, prevHash [32]byte) FrameHeader {
|
|
var magic [4]byte
|
|
copy(magic[:], []byte("QCCF"))
|
|
return FrameHeader{
|
|
Magic: magic,
|
|
Version: 1,
|
|
FrameID: prevFrameID + 1,
|
|
PrevFrameID: prevFrameID,
|
|
PrevHash: prevHash,
|
|
Timestamp: time.Now().UnixNano(),
|
|
}
|
|
}
|
|
|
|
func MarshalFrame(f *Frame) ([]byte, error) {
|
|
size := FrameHeaderSize
|
|
for _, rec := range f.Records {
|
|
size += RecordHeaderSize + len(rec.Payload)
|
|
}
|
|
padding := (8 - (size % 8)) % 8
|
|
size += padding + 32
|
|
|
|
buf := make([]byte, size)
|
|
off := 0
|
|
|
|
copy(buf[off:], f.Header.Magic[:])
|
|
off += 4
|
|
binary.BigEndian.PutUint16(buf[off:], f.Header.Version)
|
|
off += 2
|
|
binary.BigEndian.PutUint64(buf[off:], f.Header.FrameID)
|
|
off += 8
|
|
binary.BigEndian.PutUint64(buf[off:], f.Header.PrevFrameID)
|
|
off += 8
|
|
copy(buf[off:], f.Header.PrevHash[:])
|
|
off += 32
|
|
binary.BigEndian.PutUint64(buf[off:], uint64(f.Header.Timestamp))
|
|
off += 8
|
|
binary.BigEndian.PutUint32(buf[off:], uint32(len(f.Records)))
|
|
off += 4
|
|
|
|
for _, rec := range f.Records {
|
|
buf[off] = byte(rec.Header.RecType)
|
|
off++
|
|
buf[off] = rec.Header.Flags
|
|
off++
|
|
binary.BigEndian.PutUint16(buf[off:], rec.Header.TableID)
|
|
off += 2
|
|
binary.BigEndian.PutUint64(buf[off:], rec.Header.KeyHash)
|
|
off += 8
|
|
binary.BigEndian.PutUint32(buf[off:], rec.Header.PayloadSize)
|
|
off += 4
|
|
copy(buf[off:], rec.Header.Nonce[:])
|
|
off += 12
|
|
copy(buf[off:], rec.Header.AuthTag[:])
|
|
off += 16
|
|
copy(buf[off:], rec.Payload)
|
|
off += len(rec.Payload)
|
|
}
|
|
|
|
for i := 0; i < padding; i++ {
|
|
buf[off] = 0
|
|
off++
|
|
}
|
|
|
|
hash := sha256.Sum256(buf[:off])
|
|
copy(buf[off:], hash[:])
|
|
off += 32
|
|
|
|
return buf[:off], nil
|
|
}
|
|
|
|
func UnmarshalFrame(data []byte) (*Frame, error) {
|
|
if len(data) < FrameHeaderSize+32 {
|
|
return nil, fmt.Errorf("frame too short: %d", len(data))
|
|
}
|
|
dataLen := len(data)
|
|
checksumStart := dataLen - 32
|
|
storedChecksum := data[checksumStart:]
|
|
|
|
computed := sha256.Sum256(data[:checksumStart])
|
|
if !constantTimeEqual(storedChecksum, computed[:]) {
|
|
return nil, fmt.Errorf("frame checksum mismatch")
|
|
}
|
|
|
|
f := &Frame{}
|
|
off := 0
|
|
|
|
copy(f.Header.Magic[:], data[off:off+4])
|
|
off += 4
|
|
if string(f.Header.Magic[:]) != "QCCF" {
|
|
return nil, fmt.Errorf("invalid frame magic: %s", hex.EncodeToString(f.Header.Magic[:]))
|
|
}
|
|
f.Header.Version = binary.BigEndian.Uint16(data[off:])
|
|
off += 2
|
|
f.Header.FrameID = binary.BigEndian.Uint64(data[off:])
|
|
off += 8
|
|
f.Header.PrevFrameID = binary.BigEndian.Uint64(data[off:])
|
|
off += 8
|
|
copy(f.Header.PrevHash[:], data[off:off+32])
|
|
off += 32
|
|
f.Header.Timestamp = int64(binary.BigEndian.Uint64(data[off:]))
|
|
off += 8
|
|
f.Header.NumRecords = binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
|
|
for i := uint32(0); i < f.Header.NumRecords; i++ {
|
|
if off+RecordHeaderSize > checksumStart {
|
|
return nil, fmt.Errorf("record header %d exceeds frame bounds", i)
|
|
}
|
|
var rec Record
|
|
rec.Header.RecType = types.RecType(data[off])
|
|
off++
|
|
rec.Header.Flags = data[off]
|
|
off++
|
|
rec.Header.TableID = binary.BigEndian.Uint16(data[off:])
|
|
off += 2
|
|
rec.Header.KeyHash = binary.BigEndian.Uint64(data[off:])
|
|
off += 8
|
|
rec.Header.PayloadSize = binary.BigEndian.Uint32(data[off:])
|
|
off += 4
|
|
copy(rec.Header.Nonce[:], data[off:off+12])
|
|
off += 12
|
|
copy(rec.Header.AuthTag[:], data[off:off+16])
|
|
off += 16
|
|
|
|
if off+int(rec.Header.PayloadSize) > checksumStart {
|
|
return nil, fmt.Errorf("record payload %d exceeds frame bounds", i)
|
|
}
|
|
rec.Payload = make([]byte, rec.Header.PayloadSize)
|
|
copy(rec.Payload, data[off:off+int(rec.Header.PayloadSize)])
|
|
off += int(rec.Header.PayloadSize)
|
|
f.Records = append(f.Records, rec)
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
func ComputeFrameChecksum(frameData []byte) [32]byte {
|
|
return sha256.Sum256(frameData)
|
|
}
|
|
|
|
func constantTimeEqual(a, b []byte) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
var v byte
|
|
for i := 0; i < len(a); i++ {
|
|
v |= a[i] ^ b[i]
|
|
}
|
|
return v == 0
|
|
}
|