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>
This commit is contained in:
82
app/src/main/java/com/kecalek/chat/crypto/MessagePadding.kt
Normal file
82
app/src/main/java/com/kecalek/chat/crypto/MessagePadding.kt
Normal file
@@ -0,0 +1,82 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user