Files
Kecalek/app/src/main/java/com/kecalek/chat/crypto/MessagePadding.kt
filip fe861cfafa Initial commit: Kecalek Android client
Complete Android client for encrypted chat platform.
78+ Kotlin files: crypto (X3DH, Double Ratchet, AES-GCM, Ed25519, X25519,
RSA-PSS), network (TCP/TLS, 50 endpoints), Hilt DI, Room+SQLCipher DB,
Jetpack Compose UI with Catppuccin Mocha theme.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 01:19:17 +01:00

83 lines
2.7 KiB
Kotlin

package com.kecalek.chat.crypto
import java.nio.ByteBuffer
import java.security.SecureRandom
/**
* Bucket-based message padding for metadata privacy.
* Pads messages to fixed bucket sizes to prevent message-length analysis.
*
* Format: 0x01 + plaintext + random_padding + pad_length(4 bytes big-endian)
* Compatible with Python pad_plaintext/unpad_plaintext.
*/
object MessagePadding {
private const val PAD_MAGIC: Byte = 0x01
private val PAD_BUCKETS = intArrayOf(64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536)
private val secureRandom = SecureRandom()
/**
* Pad plaintext to nearest bucket size.
* @param plaintext raw plaintext bytes
* @return padded bytes: 0x01 + plaintext + random_padding + pad_length(4B)
*/
fun pad(plaintext: ByteArray): ByteArray {
// content = magic + plaintext
val content = ByteArray(1 + plaintext.size)
content[0] = PAD_MAGIC
System.arraycopy(plaintext, 0, content, 1, plaintext.size)
// minimum total size = content + 4 bytes for pad_length
val minSize = content.size + 4
// find nearest bucket
var targetSize = minSize
for (bucket in PAD_BUCKETS) {
if (bucket >= minSize) {
targetSize = bucket
break
}
}
// pad_length includes itself (4 bytes) + random padding bytes
val padLength = targetSize - content.size
val randomPadSize = padLength - 4
val result = ByteArray(targetSize)
System.arraycopy(content, 0, result, 0, content.size)
// fill random padding
if (randomPadSize > 0) {
val randomBytes = ByteArray(randomPadSize)
secureRandom.nextBytes(randomBytes)
System.arraycopy(randomBytes, 0, result, content.size, randomPadSize)
}
// write pad_length as big-endian uint32 at the end
val lenBytes = ByteBuffer.allocate(4).putInt(padLength).array()
System.arraycopy(lenBytes, 0, result, targetSize - 4, 4)
return result
}
/**
* Remove padding from padded message.
* @param data padded message bytes
* @return original plaintext
*/
fun unpad(data: ByteArray): ByteArray {
// Legacy unpadded messages (JSON starting with '{')
if (data.isEmpty() || data[0] != PAD_MAGIC) return data
if (data.size < 5) return data
// Read pad_length from last 4 bytes
val padLength = ByteBuffer.wrap(data, data.size - 4, 4).int
// Validate
if (padLength < 4 || padLength > data.size - 1) return data
// Strip magic prefix (1 byte) and padding (padLength bytes from end)
return data.copyOfRange(1, data.size - padLength)
}
}