go-p2p/ueps/reader.go
Snider 66bc0b862f
All checks were successful
Security Scan / security (push) Successful in 9s
Test / test (push) Successful in 2m7s
refactor: replace fmt.Errorf/os.* with go-log/go-io conventions
Replace all fmt.Errorf and errors.New in production code with
coreerr.E("caller.Method", "message", err) from go-log. Replace
os.ReadFile/os.WriteFile/os.MkdirAll/os.Remove with coreio.Local
equivalents from go-io. Test files left untouched.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-16 20:38:55 +00:00

115 lines
2.8 KiB
Go

package ueps
import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"io"
coreerr "forge.lthn.ai/core/go-log"
)
// ParsedPacket holds the verified data
type ParsedPacket struct {
Header UEPSHeader
Payload []byte
}
// ReadAndVerify reads a UEPS frame from the stream and validates the HMAC.
// It consumes the stream up to the end of the packet.
func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) {
// Buffer to reconstruct the data for HMAC verification
var signedData bytes.Buffer
header := UEPSHeader{}
var signature []byte
var payload []byte
// Loop through TLVs
for {
// 1. Read Tag
tag, err := r.ReadByte()
if err != nil {
return nil, err
}
// 2. Read Length (2-byte big-endian uint16)
lenBuf := make([]byte, 2)
if _, err := io.ReadFull(r, lenBuf); err != nil {
return nil, err
}
length := int(binary.BigEndian.Uint16(lenBuf))
// 3. Read Value
value := make([]byte, length)
if _, err := io.ReadFull(r, value); err != nil {
return nil, err
}
// 4. Handle Tag
switch tag {
case TagVersion:
header.Version = value[0]
signedData.WriteByte(tag)
signedData.Write(lenBuf)
signedData.Write(value)
case TagCurrentLay:
header.CurrentLayer = value[0]
signedData.WriteByte(tag)
signedData.Write(lenBuf)
signedData.Write(value)
case TagTargetLay:
header.TargetLayer = value[0]
signedData.WriteByte(tag)
signedData.Write(lenBuf)
signedData.Write(value)
case TagIntent:
header.IntentID = value[0]
signedData.WriteByte(tag)
signedData.Write(lenBuf)
signedData.Write(value)
case TagThreatScore:
header.ThreatScore = binary.BigEndian.Uint16(value)
signedData.WriteByte(tag)
signedData.Write(lenBuf)
signedData.Write(value)
case TagHMAC:
signature = value
// HMAC tag itself is not part of the signed data
case TagPayload:
payload = value
// Exit loop after payload (last tag in UEPS frame)
// Note: The HMAC covers the Payload but NOT the TagPayload/Length bytes
// to match the PacketBuilder.MarshalAndSign logic.
goto verify
default:
// Unknown tag (future proofing), verify it but ignore semantics
signedData.WriteByte(tag)
signedData.Write(lenBuf)
signedData.Write(value)
}
}
verify:
if len(signature) == 0 {
return nil, coreerr.E("ueps.ReadAndVerify", "UEPS packet missing HMAC signature", nil)
}
// 5. Verify HMAC
// Reconstruct: Headers (signedData) + Payload
mac := hmac.New(sha256.New, sharedSecret)
mac.Write(signedData.Bytes())
mac.Write(payload)
expectedMAC := mac.Sum(nil)
if !hmac.Equal(signature, expectedMAC) {
return nil, coreerr.E("ueps.ReadAndVerify", "integrity violation: HMAC mismatch (ThreatScore +100)", nil)
}
return &ParsedPacket{
Header: header,
Payload: payload,
}, nil
}