mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-15 06:56:30 +00:00
* noise * works? * noise * temporary * better * wip: use subnet * better * barely working * werk * subnet * fix peer ID * 8 byte peer ID * wip noise * wip fixes for noise * std lib for noise * noise handshake one step further * buffers * use fork * fix imports * simplify counter * remove trash * hashing * no prologue * nice * wip, use noise encryption * peer ID hex * simplify session manager * heavy logging * use singleton * Fix Noise session race condition with elegant per-peer actor serialization - Use Kotlin coroutine actors for per-peer packet processing - Each peer gets dedicated actor that processes packets sequentially - Eliminates race conditions in session management without complex locking - Single surgical change in PacketProcessor - minimal, maintainable - Leverages Kotlin's native concurrency primitives * decrypt correctly * iniator works now * clean code and fix signature to null * better * no signature in private message * small fixes * refactor ack * refactor but untested * messages working * wip ack * wip fix ack * more logging * pending tracker * keep pending connections on errors * less logging * refactor model * refactor frombinarydata * idendityannouncement refactor and update to new binary protocol * fix keys * refix keys * dms work * revert to mainnet * do not change bluetooth adapter name * keep code but uncomment * clean up comments * cleanup comments
277 lines
9.2 KiB
Kotlin
277 lines
9.2 KiB
Kotlin
package com.bitchat.android.noise
|
|
|
|
import android.util.Log
|
|
import java.security.MessageDigest
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
import javax.crypto.Cipher
|
|
import javax.crypto.SecretKeyFactory
|
|
import javax.crypto.spec.GCMParameterSpec
|
|
import javax.crypto.spec.PBEKeySpec
|
|
import javax.crypto.spec.SecretKeySpec
|
|
|
|
/**
|
|
* Channel encryption for password-protected channels - 100% compatible with iOS implementation
|
|
*
|
|
* Uses PBKDF2 key derivation with channel name as salt and AES-256-GCM for encryption.
|
|
* This is separate from Noise sessions and used for group channels with shared passwords.
|
|
*/
|
|
class NoiseChannelEncryption {
|
|
|
|
companion object {
|
|
private const val TAG = "NoiseChannelEncryption"
|
|
|
|
// PBKDF2 parameters (same as iOS)
|
|
private const val PBKDF2_ITERATIONS = 100000
|
|
private const val KEY_LENGTH = 256 // 256-bit AES key
|
|
}
|
|
|
|
// Channel keys storage (channelName -> AES key)
|
|
private val channelKeys = ConcurrentHashMap<String, SecretKeySpec>()
|
|
|
|
// Channel passwords (for rekey operations)
|
|
private val channelPasswords = ConcurrentHashMap<String, String>()
|
|
|
|
// MARK: - Channel Password Management
|
|
|
|
/**
|
|
* Set password for a channel and derive encryption key
|
|
*/
|
|
fun setChannelPassword(password: String, channel: String) {
|
|
try {
|
|
if (password.isEmpty()) {
|
|
Log.w(TAG, "Empty password provided for channel $channel")
|
|
return
|
|
}
|
|
|
|
// Derive key from password using PBKDF2 (same as iOS)
|
|
val key = deriveChannelKey(password, channel)
|
|
|
|
// Store key and password
|
|
channelKeys[channel] = key
|
|
channelPasswords[channel] = password
|
|
|
|
Log.d(TAG, "Set password for channel $channel")
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to set password for channel $channel: ${e.message}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove password for a channel
|
|
*/
|
|
fun removeChannelPassword(channel: String) {
|
|
channelKeys.remove(channel)
|
|
channelPasswords.remove(channel)
|
|
Log.d(TAG, "Removed password for channel $channel")
|
|
}
|
|
|
|
/**
|
|
* Check if we have a key for a channel
|
|
*/
|
|
fun hasChannelKey(channel: String): Boolean {
|
|
return channelKeys.containsKey(channel)
|
|
}
|
|
|
|
/**
|
|
* Get channel password (if available)
|
|
*/
|
|
fun getChannelPassword(channel: String): String? {
|
|
return channelPasswords[channel]
|
|
}
|
|
|
|
// MARK: - Encryption/Decryption
|
|
|
|
/**
|
|
* Encrypt a message for a channel
|
|
* Returns encrypted data including IV
|
|
*/
|
|
fun encryptChannelMessage(message: String, channel: String): ByteArray {
|
|
val key = channelKeys[channel]
|
|
?: throw IllegalStateException("No key available for channel $channel")
|
|
|
|
val messageBytes = message.toByteArray(Charsets.UTF_8)
|
|
|
|
return try {
|
|
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
cipher.init(Cipher.ENCRYPT_MODE, key)
|
|
|
|
val iv = cipher.iv
|
|
val encryptedData = cipher.doFinal(messageBytes)
|
|
|
|
// Combine IV and encrypted data (same format as iOS)
|
|
val result = ByteArray(iv.size + encryptedData.size)
|
|
System.arraycopy(iv, 0, result, 0, iv.size)
|
|
System.arraycopy(encryptedData, 0, result, iv.size, encryptedData.size)
|
|
|
|
result
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to encrypt channel message: ${e.message}")
|
|
throw e
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt a message for a channel
|
|
* Expects data format: IV + encrypted_data + auth_tag
|
|
*/
|
|
fun decryptChannelMessage(encryptedData: ByteArray, channel: String): String {
|
|
val key = channelKeys[channel]
|
|
?: throw IllegalStateException("No key available for channel $channel")
|
|
|
|
if (encryptedData.size < 16) { // 12 bytes IV + minimum ciphertext
|
|
throw IllegalArgumentException("Encrypted data too short")
|
|
}
|
|
|
|
return try {
|
|
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
|
|
// Extract IV (first 12 bytes for GCM) and ciphertext
|
|
val iv = encryptedData.sliceArray(0..11)
|
|
val ciphertext = encryptedData.sliceArray(12 until encryptedData.size)
|
|
|
|
val gcmSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag
|
|
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
|
|
|
|
val decryptedBytes = cipher.doFinal(ciphertext)
|
|
String(decryptedBytes, Charsets.UTF_8)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to decrypt channel message: ${e.message}")
|
|
throw e
|
|
}
|
|
}
|
|
|
|
// MARK: - Key Derivation
|
|
|
|
/**
|
|
* Derive AES key from password using PBKDF2 (same parameters as iOS)
|
|
*/
|
|
private fun deriveChannelKey(password: String, channel: String): SecretKeySpec {
|
|
try {
|
|
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
|
|
|
// Use channel name as salt (UTF-8 bytes)
|
|
val salt = channel.toByteArray(Charsets.UTF_8)
|
|
|
|
val spec = PBEKeySpec(
|
|
password.toCharArray(),
|
|
salt,
|
|
PBKDF2_ITERATIONS,
|
|
KEY_LENGTH
|
|
)
|
|
|
|
val secretKey = factory.generateSecret(spec)
|
|
return SecretKeySpec(secretKey.encoded, "AES")
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to derive channel key: ${e.message}")
|
|
throw e
|
|
}
|
|
}
|
|
|
|
// MARK: - Key Verification
|
|
|
|
/**
|
|
* Calculate key commitment (SHA-256 hash) for verification
|
|
* This allows peers to verify they have the same key without revealing it
|
|
*/
|
|
fun calculateKeyCommitment(channel: String): String? {
|
|
val key = channelKeys[channel] ?: return null
|
|
|
|
return try {
|
|
val digest = MessageDigest.getInstance("SHA-256")
|
|
val hash = digest.digest(key.encoded)
|
|
hash.joinToString("") { "%02x".format(it) }
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to calculate key commitment: ${e.message}")
|
|
null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify key commitment matches our derived key
|
|
*/
|
|
fun verifyKeyCommitment(channel: String, commitment: String): Boolean {
|
|
val ourCommitment = calculateKeyCommitment(channel)
|
|
return ourCommitment?.lowercase() == commitment.lowercase()
|
|
}
|
|
|
|
// MARK: - Channel Key Sharing
|
|
|
|
/**
|
|
* Create channel key packet for sharing via Noise session
|
|
* Returns encrypted packet that can be sent to other peers
|
|
*/
|
|
fun createChannelKeyPacket(password: String, channel: String): ByteArray? {
|
|
return try {
|
|
// Create key packet with channel and password
|
|
val packet = mapOf(
|
|
"channel" to channel,
|
|
"password" to password,
|
|
"timestamp" to System.currentTimeMillis()
|
|
)
|
|
|
|
// Simple JSON encoding for now (could be replaced with more efficient format)
|
|
val json = com.google.gson.Gson().toJson(packet)
|
|
json.toByteArray(Charsets.UTF_8)
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to create channel key packet: ${e.message}")
|
|
null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process received channel key packet
|
|
* Returns (channel, password) if successful
|
|
*/
|
|
fun processChannelKeyPacket(data: ByteArray): Pair<String, String>? {
|
|
return try {
|
|
val json = String(data, Charsets.UTF_8)
|
|
val packet = com.google.gson.Gson().fromJson(json, Map::class.java) as Map<String, Any>
|
|
|
|
val channel = packet["channel"] as? String
|
|
val password = packet["password"] as? String
|
|
|
|
if (channel != null && password != null) {
|
|
Pair(channel, password)
|
|
} else {
|
|
Log.w(TAG, "Invalid channel key packet format")
|
|
null
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to process channel key packet: ${e.message}")
|
|
null
|
|
}
|
|
}
|
|
|
|
// MARK: - Debug and Management
|
|
|
|
/**
|
|
* Get debug information
|
|
*/
|
|
fun getDebugInfo(): String = buildString {
|
|
appendLine("=== Channel Encryption Debug ===")
|
|
appendLine("Active channels: ${channelKeys.size}")
|
|
|
|
channelKeys.keys.forEach { channel ->
|
|
val hasPassword = channelPasswords.containsKey(channel)
|
|
val commitment = calculateKeyCommitment(channel)?.take(16)
|
|
appendLine(" $channel: hasPassword=$hasPassword, commitment=${commitment}...")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get list of channels with keys
|
|
*/
|
|
fun getActiveChannels(): Set<String> {
|
|
return channelKeys.keys.toSet()
|
|
}
|
|
|
|
/**
|
|
* Clear all channel data
|
|
*/
|
|
fun clear() {
|
|
channelKeys.clear()
|
|
channelPasswords.clear()
|
|
Log.d(TAG, "Cleared all channel encryption data")
|
|
}
|
|
}
|