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:
callebtc
2025-07-24 12:01:46 +02:00
committed by GitHub
parent 9e9231353b
commit c3c395832c
58 changed files with 15209 additions and 517 deletions
@@ -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}")
}