mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 10:25:19 +00:00
497 lines
18 KiB
Kotlin
497 lines
18 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 val staticIdentityPrivateKey: ByteArray
|
|
private val staticIdentityPublicKey: ByteArray
|
|
|
|
// Ed25519 signing key (persistent across app restarts) - loaded from secure storage
|
|
private val signingPrivateKey: ByteArray
|
|
private val signingPublicKey: 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")
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
// Initialize session manager
|
|
val localPeerID = calculateFingerprint(staticIdentityPublicKey).take(16)
|
|
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey, localPeerID)
|
|
|
|
// 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 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() {
|
|
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<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() {
|
|
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}")
|
|
}
|