bitchat-android/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt
callebtc 3a983c5767
Wifi aware refactor mesh core (#711)
* wifi aware wip

* wifi aware wip

* starting to work

* werk

* dms work

* wip

* fix(wifi-aware): use bindSocket and scoped IPv6 instead of bindProcessToNetwork

* Merge branch 'upstream/main' into fix/wifi-aware-socket-binding

* Fix Wi-Fi Aware connectivity and UI integration post-merge

- Replace bindProcessToNetwork with bindSocket for VPN compatibility.
- Implement Scoped IPv6 address resolution (aware0) for mesh routing.
- Bridge Wi-Fi Aware incoming messages to AppStateStore for UI visibility.
- Fix syntax errors and variable name conflicts in Debug UI.

* Enhance Wi-Fi Aware robustness and debug UI display

- Clean up transport resources (sockets, server sockets, network callbacks) immediately on peer disconnection.
- Implement resolveScopedAddress to show scoped IPv6 (e.g., %aware0) in Debug UI.
- Fix Map type mismatch warning in ChatViewModel bridge.
- Filter self-ID from peer cleanup tables to prevent recursive self-removal.

* Share GossipSyncManager across transports to prevent redundant message synchronization

- Registered BluetoothMeshService's GossipSyncManager as a singleton in MeshServiceHolder.
- Modified WifiAwareMeshService to use the shared GossipSyncManager if available.
- Added background cleanup for peer mappings on socket disconnection.
- Fixed Kotlin type mismatch during nickname map merging.

* Restore VPN acquisition logic and improve peer cleanup

- Revert removal of NET_CAPABILITY_NOT_VPN to allow hardware handle acquisition while VPN is active.
- Refactor handlePeerDisconnection to more reliably cleanup initial and routed IDs.
- Switch cleanup logging to debug level to reduce log noise.

* Fix wifi aware socket binding 2 (#536)

* Background persistence (#505)

* persistence step 1

* fix build

* messages in the background work, notifications not yet

* app state store

* DM icon shows up

* notification launches when app is closed!

* keep ui updated

* lifecycle fixes

* extensive logging, maybe revert later

* send nickname in announcement

* quit in notification

* setting in about sheet

* fix quit bitchat

* lifecycle fixes

* power mode based on background state

* stats for both direciotns

* fix graph persistence

* better counting

* count per device

* only compute when debug sheet is open? untested

* fix read receipts

* fix read receipts fully

* fix unread badge if messages have been read in focus

* foreground promotion fix

* fix app kill in notification

* adjust to new tor

* nice

* about sheet design

* bump version 1.6.0 (#524)

* Automated update of relay data - Sun Dec 14 06:06:53 UTC 2025

* bump targetSdk (#526)

* Automated update of relay data - Sun Dec 21 06:06:56 UTC 2025

* Automated update of relay data - Sun Dec 28 06:07:12 UTC 2025

* Prevent quit notification from reappearing (#530)

* shutdown sequence

* Prevent quit notification from reappearing

* Restrict force-finish broadcast

* Cancel quit shutdown on relaunch

* fix(wifi-aware): use bindSocket and scoped IPv6 instead of bindProcessToNetwork

* Merge branch 'upstream/main' into fix/wifi-aware-socket-binding

* Fix Wi-Fi Aware connectivity and UI integration post-merge

- Replace bindProcessToNetwork with bindSocket for VPN compatibility.
- Implement Scoped IPv6 address resolution (aware0) for mesh routing.
- Bridge Wi-Fi Aware incoming messages to AppStateStore for UI visibility.
- Fix syntax errors and variable name conflicts in Debug UI.

* Enhance Wi-Fi Aware robustness and debug UI display

- Clean up transport resources (sockets, server sockets, network callbacks) immediately on peer disconnection.
- Implement resolveScopedAddress to show scoped IPv6 (e.g., %aware0) in Debug UI.
- Fix Map type mismatch warning in ChatViewModel bridge.
- Filter self-ID from peer cleanup tables to prevent recursive self-removal.

* Share GossipSyncManager across transports to prevent redundant message synchronization

- Registered BluetoothMeshService's GossipSyncManager as a singleton in MeshServiceHolder.
- Modified WifiAwareMeshService to use the shared GossipSyncManager if available.
- Added background cleanup for peer mappings on socket disconnection.
- Fixed Kotlin type mismatch during nickname map merging.

* Restore VPN acquisition logic and improve peer cleanup

- Revert removal of NET_CAPABILITY_NOT_VPN to allow hardware handle acquisition while VPN is active.
- Refactor handlePeerDisconnection to more reliably cleanup initial and routed IDs.
- Switch cleanup logging to debug level to reduce log noise.

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: aidenvalue <>

* Fix wifi aware routing (#534)

* Background persistence (#505)

* persistence step 1

* fix build

* messages in the background work, notifications not yet

* app state store

* DM icon shows up

* notification launches when app is closed!

* keep ui updated

* lifecycle fixes

* extensive logging, maybe revert later

* send nickname in announcement

* quit in notification

* setting in about sheet

* fix quit bitchat

* lifecycle fixes

* power mode based on background state

* stats for both direciotns

* fix graph persistence

* better counting

* count per device

* only compute when debug sheet is open? untested

* fix read receipts

* fix read receipts fully

* fix unread badge if messages have been read in focus

* foreground promotion fix

* fix app kill in notification

* adjust to new tor

* nice

* about sheet design

* bump version 1.6.0 (#524)

* Automated update of relay data - Sun Dec 14 06:06:53 UTC 2025

* bump targetSdk (#526)

* Automated update of relay data - Sun Dec 21 06:06:56 UTC 2025

* Automated update of relay data - Sun Dec 28 06:07:12 UTC 2025

* Prevent quit notification from reappearing (#530)

* shutdown sequence

* Prevent quit notification from reappearing

* Restrict force-finish broadcast

* Cancel quit shutdown on relaunch

* fix(wifi-aware): use bindSocket and scoped IPv6 instead of bindProcessToNetwork

* Merge branch 'upstream/main' into fix/wifi-aware-socket-binding

* Fix Wi-Fi Aware connectivity and UI integration post-merge

- Replace bindProcessToNetwork with bindSocket for VPN compatibility.
- Implement Scoped IPv6 address resolution (aware0) for mesh routing.
- Bridge Wi-Fi Aware incoming messages to AppStateStore for UI visibility.
- Fix syntax errors and variable name conflicts in Debug UI.

* Enhance Wi-Fi Aware robustness and debug UI display

- Clean up transport resources (sockets, server sockets, network callbacks) immediately on peer disconnection.
- Implement resolveScopedAddress to show scoped IPv6 (e.g., %aware0) in Debug UI.
- Fix Map type mismatch warning in ChatViewModel bridge.
- Filter self-ID from peer cleanup tables to prevent recursive self-removal.

* Share GossipSyncManager across transports to prevent redundant message synchronization

- Registered BluetoothMeshService's GossipSyncManager as a singleton in MeshServiceHolder.
- Modified WifiAwareMeshService to use the shared GossipSyncManager if available.
- Added background cleanup for peer mappings on socket disconnection.
- Fixed Kotlin type mismatch during nickname map merging.

* Restore VPN acquisition logic and improve peer cleanup

- Revert removal of NET_CAPABILITY_NOT_VPN to allow hardware handle acquisition while VPN is active.
- Refactor handlePeerDisconnection to more reliably cleanup initial and routed IDs.
- Switch cleanup logging to debug level to reduce log noise.

* fix wifi aware routing

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: aidenvalue <>

* unify connection tracker for ble and wifi (#537)

* unify connection tracker for ble and wifi

* cleanup nicely

* Wifi aware skip bluetooth (#538)

* unify connection tracker for ble and wifi

* bluetooth optional

* refactor mesh core

* tests

* share gossip

* refresh peer list more often

* patches: wifi aware mesh refactor core (#549)

* Refactor WifiAware memory management and lifecycle handling

* Fix Wi-Fi Aware reconnection by ensuring callback unregistration and session watchdog

* fix(wifiaware): allow WifiAwareController to restart service if session drops

* Enhance WiFi Aware logging with network request timeouts and detailed callback status

* Ensure explicit release of WiFi Aware network callbacks on failure or disconnection

---------

Co-authored-by: aidenvalue <>

* fix: wifi aware mesh private dms (#561)

* fix(wifi-aware): restore peer check and remove aggressive session restarts

* synchronized access to sockets.

* clear old peer socket `onClientConnected` if one was found.

* address second round of review

* fix: Restart publish session on termination in WifiAwareMeshService

* fix: cleanup connection tracker resources when handling peer disconnection without active socket

---------

Co-authored-by: aidenvalu3 <>

* fix(mesh): address memory leaks and background persistence issues in Wi-Fi Aware

- Move message persistence and background notifications for Wi-Fi Aware into the service layer via MeshCore hooks.
- Fix memory leak in MainActivity by properly detaching WifiAwareMeshDelegate on pause.
- Ensure BluetoothMeshService is initialized before WifiAwareMeshService to guarantee shared GossipSyncManager instance.
- Deduplicate persistence logic from MainActivity delegate.

* wifi aware working

* more robust

* fragments work

* new files

* wifi improvements

* handshake works

* improve

* wifi mesh more robust

* fix review

* check for support

* wifi aware fixes

---------

Co-authored-by: aidenvalue <>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: aidenvalu3 <erendentman@gmail.com>
Co-authored-by: a1denvalu3 <43107113+a1denvalu3@users.noreply.github.com>
Co-authored-by: CC <cc@ggg.local>
2026-06-16 12:11:33 -05:00

533 lines
19 KiB
Kotlin

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 = com.bitchat.android.util.AppConstants.Noise.REKEY_TIME_LIMIT_MS // 1 hour (same as iOS)
private const val REKEY_MESSAGE_LIMIT = com.bitchat.android.util.AppConstants.Noise.REKEY_MESSAGE_LIMIT_ENCRYPTION // 1k messages (matches iOS) (same as iOS)
}
// Static identity key (persistent across app restarts) - loaded from secure storage
private var staticIdentityPrivateKey: ByteArray
private var staticIdentityPublicKey: ByteArray
// Ed25519 signing key (persistent across app restarts) - loaded from secure storage
private var signingPrivateKey: ByteArray
private var signingPublicKey: ByteArray
// Session management
private lateinit var 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 keys - temporary placeholders
staticIdentityPrivateKey = ByteArray(32)
staticIdentityPublicKey = ByteArray(32)
signingPrivateKey = ByteArray(32)
signingPublicKey = ByteArray(32)
loadOrGenerateKeys()
// Initialize session manager
initializeSessionManager()
}
private fun initializeSessionManager() {
// Create new session manager with current keys
val localPeerID = calculateFingerprint(staticIdentityPublicKey).take(16)
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey, localPeerID)
// Set up session callbacks
sessionManager.onSessionEstablished = { peerID, remoteStaticKey ->
handleSessionEstablished(peerID, remoteStaticKey)
}
// Ensure any other callbacks are wired if needed
// sessionManager.onSessionFailed could be wired if we exposed it
}
private fun loadOrGenerateKeys() {
// 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")
}
// Load or create Ed25519 signing key (persistent across sessions)
val loadedSigningKeyPair = identityStateManager.loadSigningKey()
if (loadedSigningKeyPair != null) {
signingPrivateKey = loadedSigningKeyPair.first
signingPublicKey = loadedSigningKeyPair.second
Log.d(TAG, "Loaded existing Ed25519 signing key")
} else {
// Generate new Ed25519 signing key pair
val signingKeyPair = generateEd25519KeyPair()
signingPrivateKey = signingKeyPair.first
signingPublicKey = signingKeyPair.second
// Save to secure storage
identityStateManager.saveSigningKey(signingPrivateKey, signingPublicKey)
Log.d(TAG, "Generated and saved new Ed25519 signing key")
}
}
// MARK: - Public Interface
/**
* Get our static public key data for sharing (32 bytes)
*/
fun getStaticPublicKeyData(): ByteArray {
return staticIdentityPublicKey.clone()
}
/**
* Get our signing public key data for sharing (32 bytes)
*/
fun getSigningPublicKeyData(): ByteArray {
return signingPublicKey.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() {
Log.w(TAG, "🚨 Panic Mode: Clearing persistent identity and rotating in-memory keys")
// 1. Clear storage
identityStateManager.clearIdentityData()
// 2. Clear all sessions immediately
if (::sessionManager.isInitialized) {
sessionManager.shutdown()
}
// 3. Regenerate keys immediately (in-memory rotation)
loadOrGenerateKeys()
// 4. Re-initialize SessionManager with new keys
initializeSessionManager()
Log.d(TAG, "✅ Identity cleared and keys rotated")
}
// 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<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) {
// 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) }
}
// MARK: - Packet Signing/Verification
/**
* Sign a BitchatPacket using our Ed25519 signing key
*/
fun signPacket(packet: com.bitchat.android.protocol.BitchatPacket): com.bitchat.android.protocol.BitchatPacket? {
// Create canonical packet bytes for signing
val packetData = packet.toBinaryDataForSigning() ?: return null
// Sign with our Ed25519 signing private key
val signature = signData(packetData) ?: return null
// Return new packet with signature
return packet.copy(signature = signature)
}
/**
* Verify a BitchatPacket signature using the provided public key
*/
fun verifyPacketSignature(packet: com.bitchat.android.protocol.BitchatPacket, publicKey: ByteArray): Boolean {
val signature = packet.signature ?: return false
// Create canonical packet bytes for verification (without signature)
val packetData = packet.toBinaryDataForSigning() ?: return false
// Verify signature using the provided Ed25519 public key
return verifySignature(signature, packetData, publicKey)
}
/**
* Sign data with our Ed25519 signing key
*/
fun signData(data: ByteArray): ByteArray? {
return try {
// For simplicity, we'll implement this using BouncyCastle which should be available
// In a production system, you might want to use the Android Keystore
signWithEd25519(data, signingPrivateKey)
} catch (e: Exception) {
Log.e(TAG, "Failed to sign data: ${e.message}")
null
}
}
/**
* Verify signature with a public key
*/
fun verifySignature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean {
return try {
verifyWithEd25519(signature, data, publicKey)
} catch (e: Exception) {
Log.e(TAG, "Failed to verify signature: ${e.message}")
false
}
}
/**
* Generate a new Ed25519 key pair for signing using BouncyCastle
* Returns (privateKey, publicKey) as 32-byte arrays
*/
private fun generateEd25519KeyPair(): Pair<ByteArray, ByteArray> {
try {
// Use BouncyCastle for proper Ed25519 key generation
val keyGen = org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator()
keyGen.init(org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters(SecureRandom()))
val keyPair = keyGen.generateKeyPair()
val privateKey = (keyPair.private as org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters).encoded
val publicKey = (keyPair.public as org.bouncycastle.crypto.params.Ed25519PublicKeyParameters).encoded
return Pair(privateKey, publicKey)
} catch (e: Exception) {
Log.e(TAG, "Failed to generate Ed25519 key pair: ${e.message}")
throw e
}
}
/**
* Sign data with Ed25519 private key using BouncyCastle
*/
private fun signWithEd25519(data: ByteArray, privateKey: ByteArray): ByteArray {
try {
val privateKeyParams = org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters(privateKey, 0)
val signer = org.bouncycastle.crypto.signers.Ed25519Signer()
signer.init(true, privateKeyParams)
signer.update(data, 0, data.size)
return signer.generateSignature()
} catch (e: Exception) {
Log.e(TAG, "Failed to sign data with Ed25519: ${e.message}")
throw e
}
}
/**
* Verify Ed25519 signature using BouncyCastle
*/
private fun verifyWithEd25519(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean {
try {
val publicKeyParams = org.bouncycastle.crypto.params.Ed25519PublicKeyParameters(publicKey, 0)
val verifier = org.bouncycastle.crypto.signers.Ed25519Signer()
verifier.init(false, publicKeyParams)
verifier.update(data, 0, data.size)
return verifier.verifySignature(signature)
} catch (e: Exception) {
Log.e(TAG, "Failed to verify Ed25519 signature: ${e.message}")
return false
}
}
/**
* Clean shutdown
*/
fun shutdown() {
if (::sessionManager.isInitialized) {
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}")
}