mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
* 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>
533 lines
19 KiB
Kotlin
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}")
|
|
}
|