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>
83 lines
2.7 KiB
Kotlin
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)
|
|
}
|
|
}
|