mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-26 00:45:20 +00:00
Implement Noise XX Handshake Protocol for Direct Messages (#180)
* 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
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
package com.bitchat.android.noise
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||
import com.bitchat.android.noise.southernstorm.protocol.Noise
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Main Noise encryption service - 100% compatible with iOS implementation
|
||||
*
|
||||
* This service manages:
|
||||
* - Static identity keys (persistent across sessions)
|
||||
* - Noise session management for each peer
|
||||
* - Channel encryption using password-derived keys
|
||||
* - Peer fingerprint mapping and identity persistence
|
||||
*/
|
||||
class NoiseEncryptionService(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NoiseEncryptionService"
|
||||
|
||||
// Session limits for performance and security
|
||||
private const val REKEY_TIME_LIMIT = 3600000L // 1 hour (same as iOS)
|
||||
private const val REKEY_MESSAGE_LIMIT = 1000L // 1k messages (matches iOS) (same as iOS)
|
||||
}
|
||||
|
||||
// Static identity key (persistent across app restarts) - loaded from secure storage
|
||||
private val staticIdentityPrivateKey: ByteArray
|
||||
private val staticIdentityPublicKey: ByteArray
|
||||
|
||||
// Session management
|
||||
private val sessionManager: NoiseSessionManager
|
||||
|
||||
// Channel encryption for password-protected channels
|
||||
private val channelEncryption = NoiseChannelEncryption()
|
||||
|
||||
// Identity management for peer ID rotation support
|
||||
private val identityStateManager: SecureIdentityStateManager
|
||||
|
||||
// Peer fingerprints mapping (peerID -> fingerprint and fingerprint -> peerID)
|
||||
private val peerFingerprints = ConcurrentHashMap<String, String>() // peerID -> fingerprint
|
||||
private val fingerprintToPeerID = ConcurrentHashMap<String, String>() // fingerprint -> current peerID
|
||||
|
||||
// Callbacks
|
||||
var onPeerAuthenticated: ((String, String) -> Unit)? = null // (peerID, fingerprint)
|
||||
var onHandshakeRequired: ((String) -> Unit)? = null // peerID needs handshake
|
||||
|
||||
init {
|
||||
// Initialize identity state manager for persistent storage
|
||||
identityStateManager = SecureIdentityStateManager(context)
|
||||
|
||||
// Load or create static identity key (persistent across sessions)
|
||||
val loadedKeyPair = identityStateManager.loadStaticKey()
|
||||
if (loadedKeyPair != null) {
|
||||
staticIdentityPrivateKey = loadedKeyPair.first
|
||||
staticIdentityPublicKey = loadedKeyPair.second
|
||||
Log.d(TAG, "Loaded existing static identity key")
|
||||
} else {
|
||||
// Generate new identity key pair
|
||||
val keyPair = generateKeyPair()
|
||||
staticIdentityPrivateKey = keyPair.first
|
||||
staticIdentityPublicKey = keyPair.second
|
||||
|
||||
// Save to secure storage
|
||||
identityStateManager.saveStaticKey(staticIdentityPrivateKey, staticIdentityPublicKey)
|
||||
Log.d(TAG, "Generated and saved new static identity key")
|
||||
}
|
||||
|
||||
// Initialize session manager
|
||||
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey)
|
||||
|
||||
// Set up session callbacks
|
||||
sessionManager.onSessionEstablished = { peerID, remoteStaticKey ->
|
||||
handleSessionEstablished(peerID, remoteStaticKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public Interface
|
||||
|
||||
/**
|
||||
* Get our static public key data for sharing (32 bytes)
|
||||
*/
|
||||
fun getStaticPublicKeyData(): ByteArray {
|
||||
return staticIdentityPublicKey.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our identity fingerprint (SHA-256 hash of static public key)
|
||||
*/
|
||||
fun getIdentityFingerprint(): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(staticIdentityPublicKey)
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get peer's public key data (if we have a session)
|
||||
*/
|
||||
fun getPeerPublicKeyData(peerID: String): ByteArray? {
|
||||
return sessionManager.getRemoteStaticKey(peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear persistent identity (for panic mode)
|
||||
*/
|
||||
fun clearPersistentIdentity() {
|
||||
identityStateManager.clearIdentityData()
|
||||
}
|
||||
|
||||
// MARK: - Handshake Management
|
||||
|
||||
/**
|
||||
* Initiate a Noise handshake with a peer
|
||||
* Returns the first handshake message to send
|
||||
*/
|
||||
fun initiateHandshake(peerID: String): ByteArray? {
|
||||
return try {
|
||||
sessionManager.initiateHandshake(peerID)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initiate handshake with $peerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming handshake message
|
||||
* Returns response message if needed, null if handshake complete or failed
|
||||
*/
|
||||
fun processHandshakeMessage(data: ByteArray, peerID: String): ByteArray? {
|
||||
return try {
|
||||
sessionManager.processHandshakeMessage(peerID, data)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to process handshake from $peerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have an established session with a peer
|
||||
*/
|
||||
fun hasEstablishedSession(peerID: String): Boolean {
|
||||
return sessionManager.hasEstablishedSession(peerID)
|
||||
}
|
||||
|
||||
// MARK: - Encryption/Decryption
|
||||
|
||||
/**
|
||||
* Encrypt data for a specific peer using established Noise session
|
||||
*/
|
||||
fun encrypt(data: ByteArray, peerID: String): ByteArray? {
|
||||
if (!hasEstablishedSession(peerID)) {
|
||||
Log.w(TAG, "No established session with $peerID, handshake required. TODO: IMPLEMENT HANDSHAKE INIT")
|
||||
onHandshakeRequired?.invoke(peerID)
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
sessionManager.encrypt(data, peerID)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encrypt for $peerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt data from a specific peer using established Noise session
|
||||
*/
|
||||
fun decrypt(encryptedData: ByteArray, peerID: String): ByteArray? {
|
||||
if (!hasEstablishedSession(peerID)) {
|
||||
Log.w(TAG, "No established session with $peerID")
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
sessionManager.decrypt(encryptedData, peerID)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decrypt from $peerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Peer Management
|
||||
|
||||
/**
|
||||
* Get fingerprint for a peer (returns null if peer unknown)
|
||||
*/
|
||||
fun getPeerFingerprint(peerID: String): String? {
|
||||
return peerFingerprints[peerID]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current peer ID for a fingerprint (returns null if not currently online)
|
||||
*/
|
||||
fun getPeerID(fingerprint: String): String? {
|
||||
return fingerprintToPeerID[fingerprint]
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a peer session (called when peer disconnects)
|
||||
*/
|
||||
fun removePeer(peerID: String) {
|
||||
sessionManager.removeSession(peerID)
|
||||
|
||||
// Clean up fingerprint mappings
|
||||
val fingerprint = peerFingerprints.remove(peerID)
|
||||
if (fingerprint != null) {
|
||||
fingerprintToPeerID.remove(fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update peer ID mapping (for peer ID rotation)
|
||||
* This allows favorites/blocking to persist across peer ID changes
|
||||
*/
|
||||
fun updatePeerIDMapping(oldPeerID: String?, newPeerID: String, fingerprint: String) {
|
||||
// Remove old mapping if exists
|
||||
oldPeerID?.let { oldID ->
|
||||
peerFingerprints.remove(oldID)
|
||||
}
|
||||
|
||||
// Add new mapping
|
||||
peerFingerprints[newPeerID] = fingerprint
|
||||
fingerprintToPeerID[fingerprint] = newPeerID
|
||||
}
|
||||
|
||||
// MARK: - Channel Encryption
|
||||
|
||||
/**
|
||||
* Set password for a channel (derives encryption key)
|
||||
*/
|
||||
fun setChannelPassword(password: String, channel: String) {
|
||||
channelEncryption.setChannelPassword(password, channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt message for a password-protected channel
|
||||
*/
|
||||
fun encryptChannelMessage(message: String, channel: String): ByteArray? {
|
||||
return try {
|
||||
channelEncryption.encryptChannelMessage(message, channel)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encrypt channel message for $channel: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt channel message
|
||||
*/
|
||||
fun decryptChannelMessage(encryptedData: ByteArray, channel: String): String? {
|
||||
return try {
|
||||
channelEncryption.decryptChannelMessage(encryptedData, channel)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decrypt channel message for $channel: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove channel password (when leaving channel)
|
||||
*/
|
||||
fun removeChannelPassword(channel: String) {
|
||||
channelEncryption.removeChannelPassword(channel)
|
||||
}
|
||||
|
||||
// MARK: - Session Maintenance
|
||||
|
||||
/**
|
||||
* Get sessions that need rekey based on time or message count
|
||||
*/
|
||||
fun getSessionsNeedingRekey(): List<String> {
|
||||
return sessionManager.getSessionsNeedingRekey()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate rekey for a session (replaces old session with new handshake)
|
||||
*/
|
||||
fun initiateRekey(peerID: String): ByteArray? {
|
||||
Log.d(TAG, "Initiating rekey for session with $peerID")
|
||||
|
||||
// Remove old session
|
||||
sessionManager.removeSession(peerID)
|
||||
|
||||
// Start new handshake
|
||||
return initiateHandshake(peerID)
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/**
|
||||
* Generate a new Curve25519 key pair using the real Noise library
|
||||
* Returns (privateKey, publicKey) as 32-byte arrays
|
||||
*/
|
||||
private fun generateKeyPair(): Pair<ByteArray, ByteArray> {
|
||||
try {
|
||||
val dhState = com.bitchat.android.noise.southernstorm.protocol.Noise.createDH("25519")
|
||||
dhState.generateKeyPair()
|
||||
|
||||
val privateKey = ByteArray(32)
|
||||
val publicKey = ByteArray(32)
|
||||
|
||||
dhState.getPrivateKey(privateKey, 0)
|
||||
dhState.getPublicKey(publicKey, 0)
|
||||
|
||||
dhState.destroy()
|
||||
|
||||
return Pair(privateKey, publicKey)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to generate key pair: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle session establishment (called when Noise handshake completes)
|
||||
*/
|
||||
private fun handleSessionEstablished(peerID: String, remoteStaticKey: ByteArray) {
|
||||
// Calculate fingerprint from remote static key
|
||||
val fingerprint = calculateFingerprint(remoteStaticKey)
|
||||
|
||||
// Store fingerprint mapping
|
||||
peerFingerprints[peerID] = fingerprint
|
||||
fingerprintToPeerID[fingerprint] = peerID
|
||||
|
||||
Log.d(TAG, "Session established with $peerID, fingerprint: ${fingerprint.take(16)}...")
|
||||
|
||||
// Notify about authentication
|
||||
onPeerAuthenticated?.invoke(peerID, fingerprint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate fingerprint from public key (SHA-256 hash)
|
||||
*/
|
||||
private fun calculateFingerprint(publicKey: ByteArray): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(publicKey)
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean shutdown
|
||||
*/
|
||||
fun shutdown() {
|
||||
sessionManager.shutdown()
|
||||
channelEncryption.clear()
|
||||
peerFingerprints.clear()
|
||||
fingerprintToPeerID.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Noise-specific errors
|
||||
*/
|
||||
sealed class NoiseEncryptionError(message: String) : Exception(message) {
|
||||
object HandshakeRequired : NoiseEncryptionError("Handshake required before encryption")
|
||||
object SessionNotEstablished : NoiseEncryptionError("No established Noise session")
|
||||
object InvalidMessage : NoiseEncryptionError("Invalid message format")
|
||||
class HandshakeFailed(cause: Throwable) : NoiseEncryptionError("Handshake failed: ${cause.message}")
|
||||
}
|
||||
Reference in New Issue
Block a user