Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ All fields are read sequentially. `[ ]` = conditionally present.
| 9 | [topic] | uint8 length + UTF-8 | Present iff pid is NOT present. Length may be 0. |
| 10 | type | uint8 + [US-ASCII string] | If flag bit 2 (common type) set: uint8 is a Common Media Type ID (see §4). Otherwise: uint8 is length of subsequent ASCII Media Type string. |
| 11 | size | uint32 | Byte length of data on the wire (after compression, if zlib-deflate set). |
| 12 | attachment headers | uint8 count + [headers] | Count may be 0. Each header: see §5. |
| 13 | data | bytes | Exactly _size_ bytes. |
| 14 | [attachments data] | bytes | Concatenated attachment payloads, sizes defined by headers. |
| 12 | [expanded size] | uint32 | Decompressed byte length. Present iff zlib-deflate set. |
| 13 | attachment headers | uint8 count + [headers] | Count may be 0. Each header: see §5. |
| 14 | data | bytes | Exactly _size_ bytes. |
| 15 | [attachments data] | bytes | Concatenated attachment payloads, sizes defined by headers. |

**Message header** = fields 1–12. **Message header hash** = SHA-256(message header). **Message hash** = SHA-256(entire message, fields 1–14).
**Message header** = fields 1–13. **Message header hash** = SHA-256(message header). **Message hash** = SHA-256(entire message, fields 1–15).

The hash MUST be computed over the full message bytes: message header fields exactly as transmitted, followed by message data and any attachments data. When the zlib-deflate flag is set for message data or an attachment's data, that data MUST be decompressed prior to inclusion in the hash computation.
The hash MUST be computed over the full message bytes: message header fields exactly as transmitted, followed by message data and any attachments data. When the zlib-deflate flag is set for message data or an attachment's data, that data MUST be decompressed prior to inclusion in the hash computation and MUST exactly match the corresponding _expanded size_; mismatch means invalid → TERMINATE.

**Sender** = _from_ when _has add to_ not set; _add to from_ when set.

Expand All @@ -50,7 +51,7 @@ The hash MUST be computed over the full message bytes: message header fields exa
| 2 | common type | type field is a 1-byte Common Media Type ID instead of length-prefixed string. |
| 3 | important | Sender flags message as important. |
| 4 | no reply | Sender will discard any reply. |
| 5 | zlib-deflate | Message data compressed with zlib/deflate (RFC 1950/1951). |
| 5 | zlib-deflate | Message data compressed with zlib/deflate (RFC 1950/1951); _expanded size_ field present. |
| 6–7 | reserved | Must be 0. |

## 4. Common Media Types
Expand Down Expand Up @@ -104,6 +105,7 @@ Each attachment header, in order:
| type | uint8 + [ASCII string] | Same encoding rule as message type, using this attachment's own common type flag. |
| filename | uint8 length + UTF-8 | < 256 bytes. Unicode letters/numbers, plus `-` `_` ` ` `.` non-consecutively, not at start/end. Unique per message (case-insensitive). |
| size | uint32 | Byte length of this attachment's data on the wire (after compression, if zlib-deflate set). |
| [expanded size] | uint32 | Decompressed byte length. Present iff this attachment's zlib-deflate flag set. |

Attachment data payloads follow all headers, concatenated in order.

Expand All @@ -129,7 +131,7 @@ Single-value codes (sent as first/only byte):
| 1 | invalid | Message header fails validation. |
| 2 | unsupported version | Version not supported. |
| 3 | undisclosed | No reason given. |
| 4 | too big | Exceeds MAX_SIZE. |
| 4 | too big | Exceeds MAX_SIZE or MAX_EXPANDED_SIZE. |
| 5 | insufficient resources | e.g. disk full. |
| 6 | parent not found | pid references unknown message. |
| 7 | too old | Timestamp too far in past. |
Expand Down Expand Up @@ -167,7 +169,8 @@ One message per connection. Two TCP connections used: Connection 1 (message tran

| Variable | Description |
|----------|-------------|
| MAX_SIZE | Max total bytes of data + attachment data. |
| MAX_SIZE | Max total bytes of data + attachment data on the wire. |
| MAX_EXPANDED_SIZE | Max total bytes after decompression; SHOULD normally equal MAX_SIZE. |
| MAX_MESSAGE_AGE | Max seconds a message time may be in the past. |
| MAX_TIME_SKEW | Max seconds a message time may be in the future. |

Expand Down Expand Up @@ -278,6 +281,7 @@ An add-to message is a duplicate of the original message with these differences:
## 13. Security Requirements

- Enforce MAX_SIZE before downloading data.
- Enforce MAX_EXPANDED_SIZE: reject when declared expanded size exceeds limit.
- Enforce per-connection and per-IP rate limits.
- Apply idle/slow-connection timeouts.
- Verify sender IP via DNS BEFORE issuing any challenge.
Expand Down
1 change: 1 addition & 0 deletions src/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ FMSG_ID_URL=http://127.0.0.1:8080


FMSG_MAX_MSG_SIZE=10240
FMSG_MAX_EXPANDED_SIZE=10240
FMSG_MAX_PAST_TIME_DELTA=604800
FMSG_MAX_FUTURE_TIME_DELTA=300
FMSG_MIN_DOWNLOAD_RATE=5000
Expand Down
29 changes: 16 additions & 13 deletions src/deflate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,15 @@ func TestGetMessageHash_WithDeflate(t *testing.T) {

// Build header with deflate flag pointing at compressed file
h := &FMsgHeader{
Version: 1,
Flags: FlagDeflate,
From: FMsgAddress{User: "alice", Domain: "example.com"},
To: []FMsgAddress{{User: "bob", Domain: "other.com"}},
Topic: "test",
Type: "text/plain;charset=UTF-8",
Size: cSize,
Filepath: dstPath,
Version: 1,
Flags: FlagDeflate,
From: FMsgAddress{User: "alice", Domain: "example.com"},
To: []FMsgAddress{{User: "bob", Domain: "other.com"}},
Topic: "test",
Type: "text/plain;charset=UTF-8",
Size: cSize,
ExpandedSize: uint32(len(original)),
Filepath: dstPath,
}

msgHash, err := h.GetMessageHash()
Expand Down Expand Up @@ -432,6 +433,7 @@ func TestGetMessageHash_DeflateChangesHash(t *testing.T) {
deflated := base
deflated.Flags = FlagDeflate
deflated.Size = cSize
deflated.ExpandedSize = uint32(len(original))
deflated.Filepath = dstPath
hashDeflated, err := deflated.GetMessageHash()
if err != nil {
Expand Down Expand Up @@ -472,11 +474,12 @@ func TestGetMessageHash_AttachmentDeflate(t *testing.T) {
Filepath: msgPath,
Attachments: []FMsgAttachmentHeader{
{
Flags: 1 << 1, // attachment deflate bit
Type: "text/csv",
Filename: "data.csv",
Size: attCSize,
Filepath: attDstPath,
Flags: 1 << 1, // attachment deflate bit
Type: "text/csv",
Filename: "data.csv",
Size: attCSize,
ExpandedSize: uint32(len(attOriginal)),
Filepath: attDstPath,
},
},
}
Expand Down
39 changes: 28 additions & 11 deletions src/defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ type FMsgAddress struct {
}

type FMsgAttachmentHeader struct {
Flags uint8
TypeID uint8
Type string
Filename string
Size uint32
Flags uint8
TypeID uint8
Type string
Filename string
Size uint32
ExpandedSize uint32

Filepath string
}
Expand All @@ -41,8 +42,9 @@ type FMsgHeader struct {
Type string

// Size in bytes of entire message
Size uint32
Attachments []FMsgAttachmentHeader
Size uint32
ExpandedSize uint32 // Decompressed size; present on wire iff FlagDeflate set
Attachments []FMsgAttachmentHeader

HeaderHash []byte
ChallengeHash [32]byte
Expand Down Expand Up @@ -117,6 +119,12 @@ func (h *FMsgHeader) Encode() []byte {
if err := binary.Write(&b, binary.LittleEndian, h.Size); err != nil {
panic(err)
}
// expanded size (uint32 LE) — present iff zlib-deflate flag set
if h.Flags&FlagDeflate != 0 {
if err := binary.Write(&b, binary.LittleEndian, h.ExpandedSize); err != nil {
panic(err)
}
}
// attachment headers
b.WriteByte(byte(len(h.Attachments)))
for _, att := range h.Attachments {
Expand All @@ -138,6 +146,12 @@ func (h *FMsgHeader) Encode() []byte {
if err := binary.Write(&b, binary.LittleEndian, att.Size); err != nil {
panic(err)
}
// attachment expanded size — present iff attachment zlib-deflate flag set
if att.Flags&(1<<1) != 0 {
if err := binary.Write(&b, binary.LittleEndian, att.ExpandedSize); err != nil {
panic(err)
}
}
}
return b.Bytes()
}
Expand Down Expand Up @@ -187,15 +201,15 @@ func (h *FMsgHeader) GetMessageHash() ([]byte, error) {
return nil, err
}

if err := hashPayload(hash, h.Filepath, int64(h.Size), h.Flags&FlagDeflate != 0); err != nil {
if err := hashPayload(hash, h.Filepath, int64(h.Size), h.Flags&FlagDeflate != 0, h.ExpandedSize); err != nil {
return nil, err
}

// include attachment data (sequential byte sequences following
// the message body, bounded by attachment header sizes)
for _, att := range h.Attachments {
compressed := att.Flags&(1<<1) != 0
if err := hashPayload(hash, att.Filepath, int64(att.Size), compressed); err != nil {
if err := hashPayload(hash, att.Filepath, int64(att.Size), compressed, att.ExpandedSize); err != nil {
return nil, fmt.Errorf("hash attachment %s: %w", att.Filename, err)
}
}
Expand All @@ -205,7 +219,7 @@ func (h *FMsgHeader) GetMessageHash() ([]byte, error) {
return h.messageHash, nil
}

func hashPayload(dst io.Writer, filepath string, wireSize int64, deflated bool) error {
func hashPayload(dst io.Writer, filepath string, wireSize int64, deflated bool, expandedSize uint32) error {
f, err := os.Open(filepath)
if err != nil {
return err
Expand All @@ -218,11 +232,14 @@ func hashPayload(dst io.Writer, filepath string, wireSize int64, deflated bool)
if err != nil {
return err
}
_, err = io.Copy(dst, zr)
written, err := io.Copy(dst, zr)
_ = zr.Close()
if err != nil {
return err
}
if uint32(written) != expandedSize {
return fmt.Errorf("decompressed size %d does not match declared expanded size %d", written, expandedSize)
}
return nil
}

Expand Down
Loading
Loading