mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 07:25:20 +00:00
wip, use noise encryption
This commit is contained in:
@@ -1,259 +1,289 @@
|
|||||||
package com.bitchat.android.crypto
|
package com.bitchat.android.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.util.Log
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import com.bitchat.android.noise.NoiseEncryptionService
|
||||||
import android.security.keystore.KeyProperties
|
|
||||||
import org.bouncycastle.crypto.agreement.X25519Agreement
|
|
||||||
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
|
|
||||||
import org.bouncycastle.crypto.generators.X25519KeyPairGenerator
|
|
||||||
import org.bouncycastle.crypto.params.*
|
|
||||||
import org.bouncycastle.crypto.signers.Ed25519Signer
|
|
||||||
import org.bouncycastle.crypto.util.PrivateKeyFactory
|
|
||||||
import org.bouncycastle.crypto.util.PublicKeyFactory
|
|
||||||
import org.bouncycastle.jcajce.provider.digest.SHA256
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import javax.crypto.Cipher
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import javax.crypto.KeyGenerator
|
|
||||||
import javax.crypto.spec.GCMParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
import kotlin.experimental.and
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encryption service that's 100% compatible with iOS version
|
* Encryption service that now uses NoiseEncryptionService internally
|
||||||
* Uses the same cryptographic algorithms and key derivation
|
* Maintains the same public API for backward compatibility
|
||||||
|
*
|
||||||
|
* This is the main interface for all encryption/decryption operations in bitchat.
|
||||||
|
* It now uses the Noise protocol for secure transport encryption with proper session management.
|
||||||
*/
|
*/
|
||||||
class EncryptionService(private val context: Context) {
|
class EncryptionService(private val context: Context) {
|
||||||
|
|
||||||
// Key agreement keys for encryption
|
companion object {
|
||||||
private val privateKey: X25519PrivateKeyParameters
|
private const val TAG = "EncryptionService"
|
||||||
private val publicKey: X25519PublicKeyParameters
|
}
|
||||||
|
|
||||||
// Signing keys for authentication
|
// Core Noise encryption service
|
||||||
private val signingPrivateKey: Ed25519PrivateKeyParameters
|
private val noiseService: NoiseEncryptionService = NoiseEncryptionService(context)
|
||||||
private val signingPublicKey: Ed25519PublicKeyParameters
|
|
||||||
|
|
||||||
// Persistent identity for favorites (separate from ephemeral keys)
|
// Session tracking for established connections
|
||||||
private lateinit var identityKey: Ed25519PrivateKeyParameters
|
private val establishedSessions = ConcurrentHashMap<String, String>() // peerID -> fingerprint
|
||||||
private lateinit var identityPublicKey: Ed25519PublicKeyParameters
|
|
||||||
|
|
||||||
// Storage for peer keys
|
// Callbacks for UI state updates
|
||||||
private val peerPublicKeys = mutableMapOf<String, X25519PublicKeyParameters>()
|
var onSessionEstablished: ((String) -> Unit)? = null // peerID
|
||||||
private val peerSigningKeys = mutableMapOf<String, Ed25519PublicKeyParameters>()
|
var onSessionLost: ((String) -> Unit)? = null // peerID
|
||||||
private val peerIdentityKeys = mutableMapOf<String, Ed25519PublicKeyParameters>()
|
var onHandshakeRequired: ((String) -> Unit)? = null // peerID
|
||||||
private val sharedSecrets = mutableMapOf<String, ByteArray>()
|
|
||||||
|
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE)
|
|
||||||
private val secureRandom = SecureRandom()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Generate ephemeral key pairs for this session
|
// Set up NoiseEncryptionService callbacks
|
||||||
val x25519Generator = X25519KeyPairGenerator()
|
noiseService.onPeerAuthenticated = { peerID, fingerprint ->
|
||||||
x25519Generator.init(X25519KeyGenerationParameters(secureRandom))
|
Log.d(TAG, "✅ Noise session established with $peerID, fingerprint: ${fingerprint.take(16)}...")
|
||||||
val x25519KeyPair = x25519Generator.generateKeyPair()
|
establishedSessions[peerID] = fingerprint
|
||||||
privateKey = x25519KeyPair.private as X25519PrivateKeyParameters
|
onSessionEstablished?.invoke(peerID)
|
||||||
publicKey = x25519KeyPair.public as X25519PublicKeyParameters
|
}
|
||||||
|
|
||||||
val ed25519Generator = Ed25519KeyPairGenerator()
|
noiseService.onHandshakeRequired = { peerID ->
|
||||||
ed25519Generator.init(Ed25519KeyGenerationParameters(secureRandom))
|
Log.d(TAG, "🤝 Handshake required for $peerID")
|
||||||
val ed25519KeyPair = ed25519Generator.generateKeyPair()
|
onHandshakeRequired?.invoke(peerID)
|
||||||
signingPrivateKey = ed25519KeyPair.private as Ed25519PrivateKeyParameters
|
|
||||||
signingPublicKey = ed25519KeyPair.public as Ed25519PublicKeyParameters
|
|
||||||
|
|
||||||
// Load or create persistent identity key
|
|
||||||
val identityKeyBytes = prefs.getString("identity_key", null)
|
|
||||||
if (identityKeyBytes != null) {
|
|
||||||
try {
|
|
||||||
val keyBytes = android.util.Base64.decode(identityKeyBytes, android.util.Base64.DEFAULT)
|
|
||||||
identityKey = Ed25519PrivateKeyParameters(keyBytes, 0)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Create new identity key if loading fails
|
|
||||||
val newIdentityKeyPair = ed25519Generator.generateKeyPair()
|
|
||||||
identityKey = newIdentityKeyPair.private as Ed25519PrivateKeyParameters
|
|
||||||
saveIdentityKey()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// First run - create and save identity key
|
|
||||||
val newIdentityKeyPair = ed25519Generator.generateKeyPair()
|
|
||||||
identityKey = newIdentityKeyPair.private as Ed25519PrivateKeyParameters
|
|
||||||
saveIdentityKey()
|
|
||||||
}
|
}
|
||||||
identityPublicKey = Ed25519PublicKeyParameters(identityKey.encoded, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveIdentityKey() {
|
// MARK: - Public API (Maintains backward compatibility)
|
||||||
val keyBytes = android.util.Base64.encodeToString(identityKey.encoded, android.util.Base64.DEFAULT)
|
|
||||||
prefs.edit().putString("identity_key", keyBytes).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create combined public key data for exchange - exactly same format as iOS
|
* Get our static public key data (32 bytes for Noise)
|
||||||
* 96 bytes total: 32 (X25519) + 32 (Ed25519 signing) + 32 (Ed25519 identity)
|
* This replaces the old 96-byte combined key format
|
||||||
*/
|
*/
|
||||||
fun getCombinedPublicKeyData(): ByteArray {
|
fun getCombinedPublicKeyData(): ByteArray {
|
||||||
val combined = ByteArray(96)
|
return noiseService.getStaticPublicKeyData()
|
||||||
System.arraycopy(publicKey.encoded, 0, combined, 0, 32) // X25519 key
|
|
||||||
System.arraycopy(signingPublicKey.encoded, 0, combined, 32, 32) // Ed25519 signing key
|
|
||||||
System.arraycopy(identityPublicKey.encoded, 0, combined, 64, 32) // Ed25519 identity key
|
|
||||||
return combined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add peer's combined public keys - exactly same logic as iOS
|
* Add peer's public key and start handshake if needed
|
||||||
|
* For backward compatibility with old key exchange packets
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun addPeerPublicKey(peerID: String, publicKeyData: ByteArray) {
|
fun addPeerPublicKey(peerID: String, publicKeyData: ByteArray) {
|
||||||
if (publicKeyData.size != 96) {
|
Log.d(TAG, "Legacy addPeerPublicKey called for $peerID with ${publicKeyData.size} bytes")
|
||||||
throw Exception("Invalid public key data size: ${publicKeyData.size}, expected 96")
|
|
||||||
|
// If this is from old key exchange format, initiate new Noise handshake
|
||||||
|
if (!hasEstablishedSession(peerID)) {
|
||||||
|
Log.d(TAG, "No Noise session with $peerID, initiating handshake")
|
||||||
|
initiateHandshake(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract all three keys: 32 for key agreement + 32 for signing + 32 for identity
|
|
||||||
val keyAgreementData = publicKeyData.sliceArray(0..31)
|
|
||||||
val signingKeyData = publicKeyData.sliceArray(32..63)
|
|
||||||
val identityKeyData = publicKeyData.sliceArray(64..95)
|
|
||||||
|
|
||||||
val peerPublicKey = X25519PublicKeyParameters(keyAgreementData, 0)
|
|
||||||
peerPublicKeys[peerID] = peerPublicKey
|
|
||||||
|
|
||||||
val peerSigningKey = Ed25519PublicKeyParameters(signingKeyData, 0)
|
|
||||||
peerSigningKeys[peerID] = peerSigningKey
|
|
||||||
|
|
||||||
val peerIdentityKey = Ed25519PublicKeyParameters(identityKeyData, 0)
|
|
||||||
peerIdentityKeys[peerID] = peerIdentityKey
|
|
||||||
|
|
||||||
// Generate shared secret for encryption using X25519
|
|
||||||
val agreement = X25519Agreement()
|
|
||||||
agreement.init(privateKey)
|
|
||||||
val sharedSecret = ByteArray(32)
|
|
||||||
agreement.calculateAgreement(peerPublicKey, sharedSecret, 0)
|
|
||||||
|
|
||||||
// Derive symmetric key using HKDF with same salt as iOS
|
|
||||||
val salt = "bitchat-v1".toByteArray()
|
|
||||||
val derivedKey = hkdf(sharedSecret, salt, byteArrayOf(), 32)
|
|
||||||
sharedSecrets[peerID] = derivedKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get peer's persistent identity key for favorites
|
* Get peer's identity key (fingerprint) for favorites
|
||||||
*/
|
*/
|
||||||
fun getPeerIdentityKey(peerID: String): ByteArray? {
|
fun getPeerIdentityKey(peerID: String): ByteArray? {
|
||||||
return peerIdentityKeys[peerID]?.encoded
|
val fingerprint = getPeerFingerprint(peerID) ?: return null
|
||||||
|
return fingerprint.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear persistent identity (for panic mode)
|
* Clear persistent identity (for panic mode)
|
||||||
*/
|
*/
|
||||||
fun clearPersistentIdentity() {
|
fun clearPersistentIdentity() {
|
||||||
prefs.edit().remove("identity_key").apply()
|
noiseService.clearPersistentIdentity()
|
||||||
|
establishedSessions.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt data for a specific peer using AES-256-GCM
|
* Encrypt data for a specific peer using Noise transport encryption
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun encrypt(data: ByteArray, peerID: String): ByteArray {
|
fun encrypt(data: ByteArray, peerID: String): ByteArray {
|
||||||
val symmetricKey = sharedSecrets[peerID]
|
val encrypted = noiseService.encrypt(data, peerID)
|
||||||
?: throw Exception("No shared secret for peer $peerID")
|
if (encrypted == null) {
|
||||||
|
throw Exception("Failed to encrypt for $peerID - no established session")
|
||||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
}
|
||||||
val keySpec = SecretKeySpec(symmetricKey, "AES")
|
return encrypted
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec)
|
|
||||||
|
|
||||||
val iv = cipher.iv
|
|
||||||
val ciphertext = cipher.doFinal(data)
|
|
||||||
|
|
||||||
// Combine IV and ciphertext (same format as iOS AES.GCM.SealedBox.combined)
|
|
||||||
val combined = ByteArray(iv.size + ciphertext.size)
|
|
||||||
System.arraycopy(iv, 0, combined, 0, iv.size)
|
|
||||||
System.arraycopy(ciphertext, 0, combined, iv.size, ciphertext.size)
|
|
||||||
|
|
||||||
return combined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt data from a specific peer
|
* Decrypt data from a specific peer using Noise transport encryption
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun decrypt(data: ByteArray, peerID: String): ByteArray {
|
fun decrypt(data: ByteArray, peerID: String): ByteArray {
|
||||||
val symmetricKey = sharedSecrets[peerID]
|
val decrypted = noiseService.decrypt(data, peerID)
|
||||||
?: throw Exception("No shared secret for peer $peerID")
|
if (decrypted == null) {
|
||||||
|
throw Exception("Failed to decrypt from $peerID - no established session")
|
||||||
if (data.size < 16) { // 12 bytes IV + 16 bytes tag minimum for GCM
|
|
||||||
throw Exception("Invalid encrypted data size")
|
|
||||||
}
|
}
|
||||||
|
return decrypted
|
||||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
||||||
val keySpec = SecretKeySpec(symmetricKey, "AES")
|
|
||||||
|
|
||||||
// Extract IV and ciphertext
|
|
||||||
val iv = data.sliceArray(0..11) // GCM IV is 12 bytes
|
|
||||||
val ciphertext = data.sliceArray(12 until data.size)
|
|
||||||
|
|
||||||
val gcmSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec)
|
|
||||||
|
|
||||||
return cipher.doFinal(ciphertext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign data using Ed25519
|
* Sign data using our static identity key
|
||||||
|
* Note: This is now done at the packet level, not per-message
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun sign(data: ByteArray): ByteArray {
|
fun sign(data: ByteArray): ByteArray {
|
||||||
val signer = Ed25519Signer()
|
// Note: In Noise protocol, authentication is built into the handshake
|
||||||
signer.init(true, signingPrivateKey)
|
// For compatibility, we return empty signature
|
||||||
signer.update(data, 0, data.size)
|
return ByteArray(0)
|
||||||
return signer.generateSignature()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify signature using Ed25519
|
* Verify signature using peer's identity key
|
||||||
|
* Note: This is now done at the packet level, not per-message
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun verify(signature: ByteArray, data: ByteArray, peerID: String): Boolean {
|
fun verify(signature: ByteArray, data: ByteArray, peerID: String): Boolean {
|
||||||
val verifyingKey = peerSigningKeys[peerID]
|
// Note: In Noise protocol, authentication is built into the transport
|
||||||
?: throw Exception("No signing key for peer $peerID")
|
// Messages are authenticated automatically when decrypted
|
||||||
|
return hasEstablishedSession(peerID)
|
||||||
val signer = Ed25519Signer()
|
}
|
||||||
signer.init(false, verifyingKey)
|
|
||||||
signer.update(data, 0, data.size)
|
// MARK: - Noise Protocol Interface
|
||||||
return signer.verifySignature(signature)
|
|
||||||
|
/**
|
||||||
|
* Check if we have an established Noise session with a peer
|
||||||
|
*/
|
||||||
|
fun hasEstablishedSession(peerID: String): Boolean {
|
||||||
|
return noiseService.hasEstablishedSession(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HKDF implementation using SHA256 - same as iOS HKDF
|
* Get encryption icon state for UI
|
||||||
*/
|
*/
|
||||||
private fun hkdf(ikm: ByteArray, salt: ByteArray, info: ByteArray, length: Int): ByteArray {
|
fun shouldShowEncryptionIcon(peerID: String): Boolean {
|
||||||
// Extract
|
return hasEstablishedSession(peerID)
|
||||||
val hmac = javax.crypto.Mac.getInstance("HmacSHA256")
|
}
|
||||||
val saltKey = SecretKeySpec(if (salt.isEmpty()) ByteArray(32) else salt, "HmacSHA256")
|
|
||||||
hmac.init(saltKey)
|
/**
|
||||||
val prk = hmac.doFinal(ikm)
|
* Get peer fingerprint for favorites/blocking
|
||||||
|
*/
|
||||||
|
fun getPeerFingerprint(peerID: String): String? {
|
||||||
|
return noiseService.getPeerFingerprint(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current peer ID for a fingerprint (for peer ID rotation)
|
||||||
|
*/
|
||||||
|
fun getCurrentPeerID(fingerprint: String): String? {
|
||||||
|
return noiseService.getPeerID(fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate a Noise handshake with a peer
|
||||||
|
*/
|
||||||
|
fun initiateHandshake(peerID: String): ByteArray? {
|
||||||
|
Log.d(TAG, "🤝 Initiating Noise handshake with $peerID")
|
||||||
|
return noiseService.initiateHandshake(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an incoming handshake message
|
||||||
|
*/
|
||||||
|
fun processHandshakeMessage(data: ByteArray, peerID: String): ByteArray? {
|
||||||
|
Log.d(TAG, "🤝 Processing handshake message from $peerID")
|
||||||
|
return noiseService.processHandshakeMessage(data, peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a peer session (called when peer disconnects)
|
||||||
|
*/
|
||||||
|
fun removePeer(peerID: String) {
|
||||||
|
establishedSessions.remove(peerID)
|
||||||
|
noiseService.removePeer(peerID)
|
||||||
|
onSessionLost?.invoke(peerID)
|
||||||
|
Log.d(TAG, "🗑️ Removed session for $peerID")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update peer ID mapping (for peer ID rotation)
|
||||||
|
*/
|
||||||
|
fun updatePeerIDMapping(oldPeerID: String?, newPeerID: String, fingerprint: String) {
|
||||||
|
oldPeerID?.let { establishedSessions.remove(it) }
|
||||||
|
establishedSessions[newPeerID] = fingerprint
|
||||||
|
noiseService.updatePeerIDMapping(oldPeerID, newPeerID, fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Channel Encryption
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set password for a channel (derives encryption key using Argon2id)
|
||||||
|
*/
|
||||||
|
fun setChannelPassword(password: String, channel: String) {
|
||||||
|
noiseService.setChannelPassword(password, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt message for a password-protected channel
|
||||||
|
*/
|
||||||
|
fun encryptChannelMessage(message: String, channel: String): ByteArray? {
|
||||||
|
return noiseService.encryptChannelMessage(message, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt channel message
|
||||||
|
*/
|
||||||
|
fun decryptChannelMessage(encryptedData: ByteArray, channel: String): String? {
|
||||||
|
return noiseService.decryptChannelMessage(encryptedData, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove channel password (when leaving channel)
|
||||||
|
*/
|
||||||
|
fun removeChannelPassword(channel: String) {
|
||||||
|
noiseService.removeChannelPassword(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Session Management
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all peers with established sessions
|
||||||
|
*/
|
||||||
|
fun getEstablishedPeers(): List<String> {
|
||||||
|
return establishedSessions.keys.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sessions that need rekeying
|
||||||
|
*/
|
||||||
|
fun getSessionsNeedingRekey(): List<String> {
|
||||||
|
return noiseService.getSessionsNeedingRekey()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate rekey for a session
|
||||||
|
*/
|
||||||
|
fun initiateRekey(peerID: String): ByteArray? {
|
||||||
|
Log.d(TAG, "🔄 Initiating rekey for $peerID")
|
||||||
|
establishedSessions.remove(peerID) // Will be re-added when new session is established
|
||||||
|
return noiseService.initiateRekey(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get our identity fingerprint
|
||||||
|
*/
|
||||||
|
fun getIdentityFingerprint(): String {
|
||||||
|
return noiseService.getIdentityFingerprint()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get debug information about encryption state
|
||||||
|
*/
|
||||||
|
fun getDebugInfo(): String = buildString {
|
||||||
|
appendLine("=== EncryptionService Debug ===")
|
||||||
|
appendLine("Established Sessions: ${establishedSessions.size}")
|
||||||
|
appendLine("Our Fingerprint: ${getIdentityFingerprint().take(16)}...")
|
||||||
|
|
||||||
// Expand
|
if (establishedSessions.isNotEmpty()) {
|
||||||
hmac.init(SecretKeySpec(prk, "HmacSHA256"))
|
appendLine("Active Encrypted Sessions:")
|
||||||
val result = ByteArray(length)
|
establishedSessions.forEach { (peerID, fingerprint) ->
|
||||||
var offset = 0
|
appendLine(" $peerID -> ${fingerprint.take(16)}...")
|
||||||
var counter = 1
|
|
||||||
|
|
||||||
while (offset < length) {
|
|
||||||
hmac.reset()
|
|
||||||
if (counter > 1) {
|
|
||||||
hmac.update(result, offset - 32, 32)
|
|
||||||
}
|
}
|
||||||
hmac.update(info)
|
|
||||||
hmac.update(counter.toByte())
|
|
||||||
|
|
||||||
val t = hmac.doFinal()
|
|
||||||
val remaining = length - offset
|
|
||||||
val toCopy = minOf(t.size, remaining)
|
|
||||||
System.arraycopy(t, 0, result, offset, toCopy)
|
|
||||||
|
|
||||||
offset += toCopy
|
|
||||||
counter++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
appendLine("")
|
||||||
|
appendLine(noiseService.toString()) // Include NoiseService state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown encryption service
|
||||||
|
*/
|
||||||
|
fun shutdown() {
|
||||||
|
establishedSessions.clear()
|
||||||
|
noiseService.shutdown()
|
||||||
|
Log.d(TAG, "🔌 EncryptionService shut down")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +325,7 @@ object MessagePadding {
|
|||||||
if (data.isEmpty()) return data
|
if (data.isEmpty()) return data
|
||||||
|
|
||||||
// Last byte tells us how much padding to remove
|
// Last byte tells us how much padding to remove
|
||||||
val paddingLength = (data.last() and 0xFF.toByte()).toInt()
|
val paddingLength = (data.last().toInt() and 0xFF)
|
||||||
if (paddingLength <= 0 || paddingLength > data.size) return data
|
if (paddingLength <= 0 || paddingLength > data.size) return data
|
||||||
|
|
||||||
return data.sliceArray(0 until (data.size - paddingLength))
|
return data.sliceArray(0 until (data.size - paddingLength))
|
||||||
|
|||||||
@@ -122,44 +122,19 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun processNoiseHandshake(peerID: String, payload: ByteArray, isInitiation: Boolean): Boolean {
|
override fun sendHandshakeResponse(peerID: String, response: ByteArray) {
|
||||||
return try {
|
// Send Noise handshake response
|
||||||
Log.d(TAG, "Processing Noise handshake from $peerID, payload size: ${payload.size} bytes")
|
val responsePacket = BitchatPacket(
|
||||||
|
version = 1u,
|
||||||
// Use proper Noise protocol implementation
|
type = MessageType.NOISE_HANDSHAKE_RESP.value,
|
||||||
val response = noiseEncryptionService.processHandshakeMessage(payload, peerID)
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
|
recipientID = hexStringToByteArray(peerID),
|
||||||
if (response != null) {
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
// Send response back
|
payload = response,
|
||||||
val responsePacket = BitchatPacket(
|
ttl = 1u
|
||||||
version = 1u,
|
)
|
||||||
type = MessageType.NOISE_HANDSHAKE_RESP.value,
|
connectionManager.broadcastPacket(RoutedPacket(responsePacket))
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
|
||||||
recipientID = hexStringToByteArray(peerID),
|
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
|
||||||
payload = response,
|
|
||||||
ttl = 1u
|
|
||||||
)
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(responsePacket))
|
|
||||||
Log.d(TAG, "Sent Noise handshake response to $peerID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if session is now established
|
|
||||||
if (noiseEncryptionService.hasEstablishedSession(peerID)) {
|
|
||||||
Log.d(TAG, "Noise session established with $peerID")
|
|
||||||
// Get the peer's public key for fingerprinting
|
|
||||||
val peerPublicKey = noiseEncryptionService.getPeerPublicKeyData(peerID)
|
|
||||||
if (peerPublicKey != null) {
|
|
||||||
// Notify delegate about successful handshake
|
|
||||||
delegate?.registerPeerPublicKey(peerID, peerPublicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,6 +609,41 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun getPeerRSSI(): Map<String, Int> = peerManager.getAllPeerRSSI()
|
fun getPeerRSSI(): Map<String, Int> = peerManager.getAllPeerRSSI()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we have an established Noise session with a peer
|
||||||
|
*/
|
||||||
|
fun hasEstablishedSession(peerID: String): Boolean {
|
||||||
|
return encryptionService.hasEstablishedSession(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get peer fingerprint for identity management
|
||||||
|
*/
|
||||||
|
fun getPeerFingerprint(peerID: String): String? {
|
||||||
|
return encryptionService.getPeerFingerprint(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get our identity fingerprint
|
||||||
|
*/
|
||||||
|
fun getIdentityFingerprint(): String {
|
||||||
|
return encryptionService.getIdentityFingerprint()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if encryption icon should be shown for a peer
|
||||||
|
*/
|
||||||
|
fun shouldShowEncryptionIcon(peerID: String): Boolean {
|
||||||
|
return encryptionService.shouldShowEncryptionIcon(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all peers with established encrypted sessions
|
||||||
|
*/
|
||||||
|
fun getEncryptedPeers(): List<String> {
|
||||||
|
return encryptionService.getEstablishedPeers()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get device address for a specific peer ID
|
* Get device address for a specific peer ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -197,11 +197,14 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
if (peerID == myPeerID) return
|
if (peerID == myPeerID) return
|
||||||
|
|
||||||
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
|
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
|
||||||
|
var recipientIDString = ""
|
||||||
|
if (recipientID != null) {
|
||||||
|
recipientIDString = String(recipientID).replace("\u0000", "")
|
||||||
|
}
|
||||||
if (recipientID == null) {
|
if (recipientID == null) {
|
||||||
// BROADCAST MESSAGE
|
// BROADCAST MESSAGE
|
||||||
handleBroadcastMessage(routed)
|
handleBroadcastMessage(routed)
|
||||||
} else if (String(recipientID).replace("\u0000", "") == myPeerID) {
|
} else if (recipientIDString == myPeerID) {
|
||||||
// PRIVATE MESSAGE FOR US
|
// PRIVATE MESSAGE FOR US
|
||||||
handlePrivateMessage(packet, peerID)
|
handlePrivateMessage(packet, peerID)
|
||||||
} else if (packet.ttl > 0u) {
|
} else if (packet.ttl > 0u) {
|
||||||
|
|||||||
@@ -114,19 +114,29 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
processedKeyExchanges.add(exchangeKey)
|
processedKeyExchanges.add(exchangeKey)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process the Noise handshake through the delegate (NoiseEncryptionService)
|
// Process the Noise handshake through the updated EncryptionService
|
||||||
val success = delegate?.processNoiseHandshake(peerID, packet.payload, step == 1) ?: false
|
val response = encryptionService.processHandshakeMessage(packet.payload, peerID)
|
||||||
|
|
||||||
if (success) {
|
if (response != null) {
|
||||||
Log.d(TAG, "Successfully processed Noise handshake step $step from $peerID")
|
Log.d(TAG, "Successfully processed Noise handshake step $step from $peerID, sending response")
|
||||||
|
|
||||||
// Notify delegate
|
// Send handshake response through delegate
|
||||||
|
delegate?.sendHandshakeResponse(peerID, response)
|
||||||
|
|
||||||
|
// Notify delegate of handshake completion
|
||||||
delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
|
delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to process Noise handshake from $peerID")
|
// Check if session is now established (handshake complete)
|
||||||
return false
|
if (encryptionService.hasEstablishedSession(peerID)) {
|
||||||
|
Log.d(TAG, "✅ Noise handshake completed with $peerID")
|
||||||
|
delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to process Noise handshake from $peerID")
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -258,9 +268,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
* Check if we have encryption keys for a peer
|
* Check if we have encryption keys for a peer
|
||||||
*/
|
*/
|
||||||
fun hasKeysForPeer(peerID: String): Boolean {
|
fun hasKeysForPeer(peerID: String): Boolean {
|
||||||
// This would need to be implemented in EncryptionService
|
return encryptionService.hasEstablishedSession(peerID)
|
||||||
// For now, we'll assume we have keys if we processed a key exchange
|
|
||||||
return processedKeyExchanges.any { it.startsWith("$peerID-") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -369,5 +377,5 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
*/
|
*/
|
||||||
interface SecurityManagerDelegate {
|
interface SecurityManagerDelegate {
|
||||||
fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?)
|
fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?)
|
||||||
fun processNoiseHandshake(peerID: String, payload: ByteArray, isInitiation: Boolean): Boolean
|
fun sendHandshakeResponse(peerID: String, response: ByteArray)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,34 +261,7 @@ class NoiseSessionManager(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate against known XX pattern message sizes
|
return true
|
||||||
when (message.size) {
|
|
||||||
32 -> {
|
|
||||||
// First message: -> e
|
|
||||||
Log.d(TAG, "Received XX pattern message 1 from $peerID (32 bytes)")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
80 -> {
|
|
||||||
// Second message: <- e, ee, s, es
|
|
||||||
Log.d(TAG, "Received XX pattern message 2 from $peerID (80 bytes)")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
48 -> {
|
|
||||||
// Third message: -> s, se
|
|
||||||
Log.d(TAG, "Received XX pattern message 3 from $peerID (48 bytes)")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// Allow some flexibility for payload or different implementations
|
|
||||||
if (message.size >= 32 && message.size <= 100) {
|
|
||||||
Log.d(TAG, "Received handshake message from $peerID with size ${message.size} (non-standard but acceptable)")
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Invalid handshake message size from $peerID: ${message.size} bytes")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Encryption/Decryption
|
// MARK: - Encryption/Decryption
|
||||||
|
|||||||
@@ -156,13 +156,15 @@ fun ChatHeaderContent(
|
|||||||
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
||||||
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer)
|
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer)
|
||||||
val isFavorite = favoritePeers.contains(fingerprint)
|
val isFavorite = favoritePeers.contains(fingerprint)
|
||||||
|
val hasEncryption = viewModel.meshService.shouldShowEncryptionIcon(selectedPrivatePeer)
|
||||||
|
|
||||||
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, fingerprint=$fingerprint, isFav=$isFavorite")
|
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, fingerprint=$fingerprint, isFav=$isFavorite, encrypted=$hasEncryption")
|
||||||
|
|
||||||
PrivateChatHeader(
|
PrivateChatHeader(
|
||||||
peerID = selectedPrivatePeer,
|
peerID = selectedPrivatePeer,
|
||||||
peerNicknames = viewModel.meshService.getPeerNicknames(),
|
peerNicknames = viewModel.meshService.getPeerNicknames(),
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
|
hasEncryption = hasEncryption,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
|
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
|
||||||
)
|
)
|
||||||
@@ -195,6 +197,7 @@ private fun PrivateChatHeader(
|
|||||||
peerID: String,
|
peerID: String,
|
||||||
peerNicknames: Map<String, String>,
|
peerNicknames: Map<String, String>,
|
||||||
isFavorite: Boolean,
|
isFavorite: Boolean,
|
||||||
|
hasEncryption: Boolean,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onToggleFavorite: () -> Unit
|
onToggleFavorite: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -243,6 +246,18 @@ private fun PrivateChatHeader(
|
|||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = Color(0xFFFF9500) // Orange to match private message theme
|
tint = Color(0xFFFF9500) // Orange to match private message theme
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Show encryption status icon if session is established
|
||||||
|
if (hasEncryption) {
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Security,
|
||||||
|
contentDescription = "End-to-end encrypted",
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = Color(0xFF00C851) // Green to indicate verified encryption
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = peerNickname,
|
text = peerNickname,
|
||||||
|
|||||||
Reference in New Issue
Block a user