package com.bitchat.android.noise import android.content.Context import android.util.Log import com.bitchat.android.identity.SecureIdentityStateManager import com.bitchat.android.mesh.PeerFingerprintManager 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 // Centralized fingerprint management - NO LOCAL STORAGE private val fingerprintManager = PeerFingerprintManager.getInstance() // 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: ${calculateFingerprint(staticIdentityPublicKey)}") } 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) } /** * Get session state for a peer (for UI state display) */ fun getSessionState(peerID: String): NoiseSession.NoiseSessionState { return sessionManager.getSessionState(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 fingerprintManager.getFingerprintForPeer(peerID) } /** * Get current peer ID for a fingerprint (returns null if not currently online) */ fun getPeerID(fingerprint: String): String? { return fingerprintManager.getPeerIDForFingerprint(fingerprint) } /** * Remove a peer session (called when peer disconnects) */ fun removePeer(peerID: String) { sessionManager.removeSession(peerID) // Clean up fingerprint mappings via centralized manager fingerprintManager.removePeer(peerID) } /** * 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) { // Use centralized fingerprint manager for peer ID rotation fingerprintManager.updatePeerIDMapping(oldPeerID, newPeerID, fingerprint) } // 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 { 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 { 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) { // Store fingerprint mapping via centralized manager // This is the ONLY place where fingerprints are stored - after successful Noise handshake fingerprintManager.storeFingerprintForPeer(peerID, remoteStaticKey) // Calculate fingerprint for logging and callback val fingerprint = calculateFingerprint(remoteStaticKey) 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() // No need to clear fingerprints here - they are managed centrally } } /** * 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}") }