# How Bitchat's Binary Protocol Uses PKCS#7 Padding for Traffic Analysis Protection

> Discover how Bitchat's binary protocol uses PKCS#7 padding to encrypt payloads into fixed sizes, thwarting traffic analysis by obscuring plaintext lengths. Learn about its strong privacy protections.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-07-27

---

**The Bitchat binary protocol applies PKCS#7 padding exclusively to Noise-encrypted frames, rounding payloads into fixed cryptographic buckets (256 B, 512 B, 1 KiB, or 2 KiB) to prevent adversaries from inferring exact plaintext lengths through packet size observation.**

The `permissionlesstech/bitchat` repository implements a privacy-focused messaging system where metadata resistance is critical. By padding encrypted payloads using the PKCS#7 scheme, the protocol ensures that network observers only see standardized packet sizes rather than the precise length of underlying message content, directly addressing traffic analysis vulnerabilities.

## Fixed-Size Buckets and Block Selection

Traffic analysis attacks exploit packet length metadata to infer communication patterns. To neutralize this signal, Bitchat categorizes all Noise-encrypted frames into discrete size classes, or "buckets."

The protocol defines four specific bucket sizes: **256 bytes**, **512 bytes**, **1 KiB**, and **2 KiB**. The `MessagePadding.optimalBlockSize(for:)` method computes the smallest bucket capable of containing the encrypted payload plus its mandatory **16-byte AES-GCM authentication tag**【MessagePadding.swift†L55-L63】. This deterministic sizing ensures that no padded frame exceeds the next standard tier, collapsing variance into four observable categories.

## PKCS#7 Implementation in MessagePadding.swift

The core padding logic resides in [`MessagePadding.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessagePadding.swift), handling both padding creation and cryptographically secure validation.

### Padding Logic and Byte Pattern

The `MessagePadding.pad(_:toSize:)` function implements classic PKCS#7 padding through three precise steps:

1. It verifies the payload fits within the target bucket.
2. It calculates `paddingNeeded = targetSize - data.count`.
3. It appends `paddingNeeded` copies of the byte `UInt8(paddingNeeded)` to the data【MessagePadding.swift†L26-L38】.

This produces a padding block where every byte value equals the total padding length. For instance, 10 bytes of padding adds ten sequential `0x0A` bytes to the payload, creating a recognizable pattern that supports deterministic removal.

### Unpadding and Tamper Detection

After decryption, `MessagePadding.unpad(_:)` reads the final byte to determine the claimed padding length. It then validates that the last *N* bytes all match this value before stripping them【MessagePadding.swift†L40-L51】. Mismatched bytes trigger immediate rejection, protecting against padding oracle attacks and malformed packet injection.

## Protocol Constraints and Trade-offs

The [`BitchatProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatProtocol.swift) header explicitly documents that "PKCS#7-style padding for privacy" applies only to Noise frames, while other packet types travel at natural length【BitchatProtocol.swift†L29-L31】【WHITEPAPER.md†L49-L53】.

A critical limitation stems from PKCS#7's single-byte length encoding: the maximum padding is **255 bytes**. If a payload requires more than 255 bytes to reach the next bucket, the frame is transmitted **unpadded**. This caps bucket sizes at predefined intervals and represents a deliberate bandwidth-versus-privacy trade-off.

## Practical Implementation Example

The following Swift code demonstrates the complete padding workflow from bucket selection to transmission:

```swift
import Foundation
import BitFoundation

// 1️⃣ Determine the optimal bucket for an encrypted payload
let payload = Data([0x01, 0x02, 0x03])  // Example encrypted data
let bucket = MessagePadding.optimalBlockSize(for: payload.count) // Returns 256

// 2️⃣ Apply PKCS#7 padding to reach the exact bucket size
let padded = MessagePadding.pad(payload, toSize: bucket)

// 3️⃣ Transmit the padded frame via the Noise-encrypted packet type
// (BitchatProtocol encoder handles the packet wrapping)

// 4️⃣ On reception, remove padding after decryption
let received = padded
let unpadded = MessagePadding.unpad(received)  // Returns original payload

```

## Summary

- **Fixed buckets**: Noise frames are rounded to 256 B, 512 B, 1 KiB, or 2 KiB using PKCS#7 padding.
- **Single-byte encoding**: Each pad byte contains the length value, limiting maximum padding to 255 bytes.
- **Selective application**: Only end-to-end encrypted Noise frames are padded; other traffic uses natural lengths.
- **Validation**: The `unpad` method verifies padding consistency to detect tampering.
- **Trade-off**: Payloads requiring >255 bytes of padding transmit unpadded to avoid excessive overhead.

## Frequently Asked Questions

### What is PKCS#7 padding and how does it differ from other padding schemes?

PKCS#7 is a cryptographic padding scheme where every added byte contains the same value—the number of padding bytes appended. Unlike zero-padding or random padding, PKCS#7 allows deterministic removal by reading the last byte as an unambiguous length indicator, ensuring reliable recovery of the original message without separator bytes.

### Why does Bitchat only pad Noise-encrypted frames and not other packet types?

The protocol intentionally limits padding to **Noise-encrypted frames**—the packets carrying end-to-end encrypted payloads—because these contain the sensitive content requiring privacy protection. Control packets and handshake messages travel at natural length to minimize bandwidth overhead while keeping the encryption layer's metadata obfuscated【WHITEPAPER.md†L49-L53】.

### What happens if a payload requires more than 255 bytes of padding?

If the gap between the encrypted payload (plus the 16-byte tag) and the next bucket exceeds 255 bytes, the frame is emitted **unpadded**. This occurs because PKCS#7 uses a single byte to encode padding length, capping the value at 0xFF (255). This represents a protocol trade-off that prioritizes reasonable overhead over perfect privacy for payloads near bucket boundaries.

### How effective is PKCS#7 padding against traffic analysis attacks?

The padding reduces granularity by collapsing all payload sizes into four discrete buckets, forcing observers to distinguish between coarse size classes rather than exact byte counts. While effective against precise length inference, sophisticated adversaries may still analyze timing patterns or frequency distributions within these buckets to infer communication metadata.