mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 20:05:22 +00:00
* docs: correct inaccurate privacy and metadata claims Several documented guarantees did not match the implementation. These matter more than ordinary doc drift: someone deciding whether to carry this phone to a protest reads these sentences as the threat model. - Peer IDs were described as "short ephemeral IDs derived per session" that "rotate periodically" and "prevent tracking". They are the first 8 bytes of the Noise static key fingerprint, stable across sessions and reboots, and replaced only by a panic wipe. Corrected in the whitepaper (§3, §8), IdentityModels, and BitchatProtocol, whose header notes claimed "no persistent identifiers in protocol headers" while every header carries exactly one. - "No plaintext message content is ever written to disk" was false for accepted media, which is stored unsealed under the platform's data-protection class. Narrowed to what actually holds. - Padding was described as applying to all packets but fragments. Only noiseEncrypted and noiseHandshake frames are padded; the pad bytes equal the pad length rather than being random; and because that length must fit one byte, a frame needing over 255 bytes of padding is emitted unpadded. Documented in the whitepaper (§4.1) and MessagePadding. - The gossip archive window is 6 hours in production, not the 15 minutes claimed in PRIVACY_POLICY.md and the privacy assessment. The 15-minute figure is the struct default that BLEService overrides. - The privacy assessment credited iOS BLE address randomization without noting that stable app-layer identifiers defeat it. The whitepaper's future-work list now names the changes these corrections imply: rotating on-air identity, padding for non-Noise types, and making the announce neighbor list optional. No behavior change; comments and documentation only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: correct the same claims in the README The README repeats two of the claims corrected elsewhere in this PR, and it is the document people actually read before deciding to trust the app. - "no persistent identifiers" is the inverse of what the mesh does; it now points at the whitepaper's identity and metadata sections. - "end-to-end encryption with forward secrecy" holds for live Noise sessions but not for sealed store-and-forward mail, which the whitepaper already flags as its main cryptographic trade-off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
71 lines
2.6 KiB
Swift
71 lines
2.6 KiB
Swift
//
|
|
// MessagePadding.swift
|
|
// bitchat
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import struct Foundation.Data
|
|
|
|
/// Provides privacy-preserving message padding to obscure actual content length.
|
|
///
|
|
/// PKCS#7-style: every pad byte equals the pad length, which is what `unpad`
|
|
/// verifies. The bytes are not random.
|
|
///
|
|
/// Two limits are worth knowing before relying on this for traffic analysis
|
|
/// resistance. Only Noise frames are padded at all (see the outbound packet
|
|
/// policy); everything else travels at its natural length. And because the pad
|
|
/// length has to fit in one byte, `pad` declines any request needing more than
|
|
/// 255 bytes — so a frame far below its target bucket is emitted unpadded
|
|
/// rather than padded to a smaller bucket.
|
|
struct MessagePadding {
|
|
// Standard block sizes for padding
|
|
static let blockSizes = [256, 512, 1024, 2048]
|
|
|
|
// Add PKCS#7-style padding to reach target size
|
|
static func pad(_ data: Data, toSize targetSize: Int) -> Data {
|
|
guard data.count < targetSize else { return data }
|
|
|
|
let paddingNeeded = targetSize - data.count
|
|
// Constrain to 255 to fit a single-byte pad length marker
|
|
guard paddingNeeded > 0 && paddingNeeded <= 255 else { return data }
|
|
|
|
var padded = data
|
|
// PKCS#7: All pad bytes are equal to the pad length
|
|
padded.append(contentsOf: Array(repeating: UInt8(paddingNeeded), count: paddingNeeded))
|
|
return padded
|
|
}
|
|
|
|
// Remove padding from data
|
|
static func unpad(_ data: Data) -> Data {
|
|
guard !data.isEmpty else { return data }
|
|
let last = data.last!
|
|
let paddingLength = Int(last)
|
|
// Must have at least 1 pad byte and not exceed data length
|
|
guard paddingLength > 0 && paddingLength <= data.count else { return data }
|
|
// Verify PKCS#7: all last N bytes equal to pad length
|
|
let start = data.count - paddingLength
|
|
let tail = data[start...]
|
|
for b in tail { if b != last { return data } }
|
|
return Data(data[..<start])
|
|
}
|
|
|
|
// Find optimal block size for data
|
|
static func optimalBlockSize(for dataSize: Int) -> Int {
|
|
// Account for encryption overhead (~16 bytes for AES-GCM tag)
|
|
let totalSize = dataSize + 16
|
|
|
|
// Find smallest block that fits
|
|
for blockSize in blockSizes {
|
|
if totalSize <= blockSize {
|
|
return blockSize
|
|
}
|
|
}
|
|
|
|
// For very large messages, just use the original size
|
|
// (will be fragmented anyway)
|
|
return dataSize
|
|
}
|
|
}
|