This commit is contained in:
callebtc
2025-08-20 00:07:19 +02:00
parent 4acfafb998
commit cc4316bf2b
6 changed files with 277 additions and 23 deletions
@@ -24,6 +24,8 @@ class SecureIdentityStateManager(private val context: Context) {
private const val PREFS_NAME = "bitchat_identity" private const val PREFS_NAME = "bitchat_identity"
private const val KEY_STATIC_PRIVATE_KEY = "static_private_key" private const val KEY_STATIC_PRIVATE_KEY = "static_private_key"
private const val KEY_STATIC_PUBLIC_KEY = "static_public_key" private const val KEY_STATIC_PUBLIC_KEY = "static_public_key"
private const val KEY_SIGNING_PRIVATE_KEY = "signing_private_key"
private const val KEY_SIGNING_PUBLIC_KEY = "signing_public_key"
private const val KEY_LAST_ROTATION = "last_rotation" private const val KEY_LAST_ROTATION = "last_rotation"
private const val KEY_NEXT_ROTATION_INTERVAL = "next_rotation_interval" private const val KEY_NEXT_ROTATION_INTERVAL = "next_rotation_interval"
@@ -109,6 +111,64 @@ class SecureIdentityStateManager(private val context: Context) {
} }
} }
// MARK: - Signing Key Management
/**
* Load saved signing key pair
* Returns (privateKey, publicKey) or null if none exists
*/
fun loadSigningKey(): Pair<ByteArray, ByteArray>? {
return try {
val privateKeyString = prefs.getString(KEY_SIGNING_PRIVATE_KEY, null)
val publicKeyString = prefs.getString(KEY_SIGNING_PUBLIC_KEY, null)
if (privateKeyString != null && publicKeyString != null) {
val privateKey = android.util.Base64.decode(privateKeyString, android.util.Base64.DEFAULT)
val publicKey = android.util.Base64.decode(publicKeyString, android.util.Base64.DEFAULT)
// Validate key sizes
if (privateKey.size == 32 && publicKey.size == 32) {
Log.d(TAG, "Loaded Ed25519 signing key from secure storage")
Pair(privateKey, publicKey)
} else {
Log.w(TAG, "Invalid signing key sizes in storage, returning null")
null
}
} else {
Log.d(TAG, "No Ed25519 signing key found in storage")
null
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load signing key: ${e.message}")
null
}
}
/**
* Save signing key pair to secure storage
*/
fun saveSigningKey(privateKey: ByteArray, publicKey: ByteArray) {
try {
// Validate key sizes
if (privateKey.size != 32 || publicKey.size != 32) {
throw IllegalArgumentException("Invalid signing key sizes: private=${privateKey.size}, public=${publicKey.size}")
}
val privateKeyString = android.util.Base64.encodeToString(privateKey, android.util.Base64.DEFAULT)
val publicKeyString = android.util.Base64.encodeToString(publicKey, android.util.Base64.DEFAULT)
prefs.edit()
.putString(KEY_SIGNING_PRIVATE_KEY, privateKeyString)
.putString(KEY_SIGNING_PUBLIC_KEY, publicKeyString)
.apply()
Log.d(TAG, "Saved Ed25519 signing key to secure storage")
} catch (e: Exception) {
Log.e(TAG, "Failed to save signing key: ${e.message}")
throw e
}
}
// MARK: - Fingerprint Generation // MARK: - Fingerprint Generation
/** /**
@@ -575,8 +575,15 @@ class BluetoothMeshService(private val context: Context) {
return@launch return@launch
} }
// Get the signing public key for the announcement
val signingKey = encryptionService.getSigningPublicKey()
if (signingKey == null) {
Log.e(TAG, "No signing public key available for announcement")
return@launch
}
// Create iOS-compatible IdentityAnnouncement with TLV encoding // Create iOS-compatible IdentityAnnouncement with TLV encoding
val announcement = IdentityAnnouncement(nickname, staticKey) val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
val tlvPayload = announcement.encode() val tlvPayload = announcement.encode()
if (tlvPayload == null) { if (tlvPayload == null) {
Log.e(TAG, "Failed to encode announcement as TLV") Log.e(TAG, "Failed to encode announcement as TLV")
@@ -610,8 +617,15 @@ class BluetoothMeshService(private val context: Context) {
return return
} }
// Get the signing public key for the announcement
val signingKey = encryptionService.getSigningPublicKey()
if (signingKey == null) {
Log.e(TAG, "No signing public key available for peer announcement")
return
}
// Create iOS-compatible IdentityAnnouncement with TLV encoding // Create iOS-compatible IdentityAnnouncement with TLV encoding
val announcement = IdentityAnnouncement(nickname, staticKey) val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
val tlvPayload = announcement.encode() val tlvPayload = announcement.encode()
if (tlvPayload == null) { if (tlvPayload == null) {
Log.e(TAG, "Failed to encode peer announcement as TLV") Log.e(TAG, "Failed to encode peer announcement as TLV")
@@ -181,20 +181,22 @@ class MessageHandler(private val myPeerID: String) {
// Successfully decoded TLV format exactly like iOS // Successfully decoded TLV format exactly like iOS
Log.d(TAG, "Received iOS-compatible announce from $peerID: nickname=${announcement.nickname}, " + Log.d(TAG, "Received iOS-compatible announce from $peerID: nickname=${announcement.nickname}, " +
"publicKey=${announcement.publicKey.joinToString("") { "%02x".format(it) }.take(16)}...") "noisePublicKey=${announcement.noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., " +
"signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
// Extract nickname and public key from TLV data // Extract nickname and public keys from TLV data
val nickname = announcement.nickname val nickname = announcement.nickname
val publicKey = announcement.publicKey val noisePublicKey = announcement.noisePublicKey
val signingPublicKey = announcement.signingPublicKey
// Notify delegate to handle peer management with nickname // Notify delegate to handle peer management with nickname
val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false
// Update peer ID binding with public key for identity management // Update peer ID binding with noise public key for identity management
delegate?.updatePeerIDBinding( delegate?.updatePeerIDBinding(
newPeerID = peerID, newPeerID = peerID,
nickname = nickname, nickname = nickname,
publicKey = publicKey, publicKey = noisePublicKey,
previousPeerID = null previousPeerID = null
) )
@@ -11,7 +11,8 @@ import com.bitchat.android.util.*
@Parcelize @Parcelize
data class IdentityAnnouncement( data class IdentityAnnouncement(
val nickname: String, val nickname: String,
val publicKey: ByteArray // FIXED: Made non-nullable to match iOS and AnnouncementPacket val noisePublicKey: ByteArray, // Noise static public key (Curve25519.KeyAgreement)
val signingPublicKey: ByteArray // Ed25519 public key for signing
) : Parcelable { ) : Parcelable {
/** /**
@@ -19,7 +20,8 @@ data class IdentityAnnouncement(
*/ */
private enum class TLVType(val value: UByte) { private enum class TLVType(val value: UByte) {
NICKNAME(0x01u), NICKNAME(0x01u),
NOISE_PUBLIC_KEY(0x02u); NOISE_PUBLIC_KEY(0x02u),
SIGNING_PUBLIC_KEY(0x03u); // NEW: Ed25519 signing public key
companion object { companion object {
fun fromValue(value: UByte): TLVType? { fun fromValue(value: UByte): TLVType? {
@@ -35,7 +37,7 @@ data class IdentityAnnouncement(
val nicknameData = nickname.toByteArray(Charsets.UTF_8) val nicknameData = nickname.toByteArray(Charsets.UTF_8)
// Check size limits // Check size limits
if (nicknameData.size > 255 || publicKey.size > 255) { if (nicknameData.size > 255 || noisePublicKey.size > 255 || signingPublicKey.size > 255) {
return null return null
} }
@@ -46,10 +48,15 @@ data class IdentityAnnouncement(
result.add(nicknameData.size.toByte()) result.add(nicknameData.size.toByte())
result.addAll(nicknameData.toList()) result.addAll(nicknameData.toList())
// TLV for public key // TLV for noise public key
result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte()) result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte())
result.add(publicKey.size.toByte()) result.add(noisePublicKey.size.toByte())
result.addAll(publicKey.toList()) result.addAll(noisePublicKey.toList())
// TLV for signing public key
result.add(TLVType.SIGNING_PUBLIC_KEY.value.toByte())
result.add(signingPublicKey.size.toByte())
result.addAll(signingPublicKey.toList())
return result.toByteArray() return result.toByteArray()
} }
@@ -64,12 +71,13 @@ data class IdentityAnnouncement(
var offset = 0 var offset = 0
var nickname: String? = null var nickname: String? = null
var publicKey: ByteArray? = null var noisePublicKey: ByteArray? = null
var signingPublicKey: ByteArray? = null
while (offset + 2 <= dataCopy.size) { while (offset + 2 <= dataCopy.size) {
// Read TLV type // Read TLV type
val typeValue = dataCopy[offset].toUByte() val typeValue = dataCopy[offset].toUByte()
val type = TLVType.fromValue(typeValue) ?: return null val type = TLVType.fromValue(typeValue)
offset += 1 offset += 1
// Read TLV length // Read TLV length
@@ -83,19 +91,27 @@ data class IdentityAnnouncement(
val value = dataCopy.sliceArray(offset until offset + length) val value = dataCopy.sliceArray(offset until offset + length)
offset += length offset += length
// Process known TLV types, skip unknown ones for forward compatibility
when (type) { when (type) {
TLVType.NICKNAME -> { TLVType.NICKNAME -> {
nickname = String(value, Charsets.UTF_8) nickname = String(value, Charsets.UTF_8)
} }
TLVType.NOISE_PUBLIC_KEY -> { TLVType.NOISE_PUBLIC_KEY -> {
publicKey = value noisePublicKey = value
}
TLVType.SIGNING_PUBLIC_KEY -> {
signingPublicKey = value
}
null -> {
// Unknown TLV; skip (tolerant decoder for forward compatibility)
continue
} }
} }
} }
// Both fields are required // All three fields are required
return if (nickname != null && publicKey != null) { return if (nickname != null && noisePublicKey != null && signingPublicKey != null) {
IdentityAnnouncement(nickname, publicKey) IdentityAnnouncement(nickname, noisePublicKey, signingPublicKey)
} else { } else {
null null
} }
@@ -110,18 +126,20 @@ data class IdentityAnnouncement(
other as IdentityAnnouncement other as IdentityAnnouncement
if (nickname != other.nickname) return false if (nickname != other.nickname) return false
if (!publicKey.contentEquals(other.publicKey)) return false if (!noisePublicKey.contentEquals(other.noisePublicKey)) return false
if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = nickname.hashCode() var result = nickname.hashCode()
result = 31 * result + publicKey.contentHashCode() result = 31 * result + noisePublicKey.contentHashCode()
result = 31 * result + signingPublicKey.contentHashCode()
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "IdentityAnnouncement(nickname='$nickname', publicKey=${publicKey.joinToString("") { "%02x".format(it) }.take(16)}...)" return "IdentityAnnouncement(nickname='$nickname', noisePublicKey=${noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., signingPublicKey=${signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...)"
} }
} }
@@ -32,6 +32,10 @@ class NoiseEncryptionService(private val context: Context) {
private val staticIdentityPrivateKey: ByteArray private val staticIdentityPrivateKey: ByteArray
private val staticIdentityPublicKey: 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 // Session management
private val sessionManager: NoiseSessionManager private val sessionManager: NoiseSessionManager
@@ -69,6 +73,23 @@ class NoiseEncryptionService(private val context: Context) {
Log.d(TAG, "Generated and saved new static identity key") 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 // Initialize session manager
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey) sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey)
@@ -87,6 +108,13 @@ class NoiseEncryptionService(private val context: Context) {
return staticIdentityPublicKey.clone() 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) * Get our identity fingerprint (SHA-256 hash of static public key)
*/ */
@@ -338,6 +366,118 @@ class NoiseEncryptionService(private val context: Context) {
return hash.joinToString("") { "%02x".format(it) } 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
* Returns (privateKey, publicKey) as 32-byte arrays
*/
private fun generateEd25519KeyPair(): Pair<ByteArray, ByteArray> {
try {
// Generate a simple Ed25519 key pair using secure random
// This is a simplified implementation - in production you'd use proper Ed25519 library
val secureRandom = SecureRandom()
val privateKey = ByteArray(32)
secureRandom.nextBytes(privateKey)
// Generate public key from private key (simplified)
val publicKey = deriveEd25519PublicKey(privateKey)
return Pair(privateKey, publicKey)
} catch (e: Exception) {
Log.e(TAG, "Failed to generate Ed25519 key pair: ${e.message}")
throw e
}
}
/**
* Derive Ed25519 public key from private key (simplified implementation)
*/
private fun deriveEd25519PublicKey(privateKey: ByteArray): ByteArray {
// This is a placeholder - in a real implementation you'd use proper Ed25519 math
// For now, we'll use a hash-based derivation which is not cryptographically correct
// but will work for the protocol demonstration
val digest = MessageDigest.getInstance("SHA-256")
return digest.digest(privateKey)
}
/**
* Sign data with Ed25519 private key (simplified implementation)
*/
private fun signWithEd25519(data: ByteArray, privateKey: ByteArray): ByteArray {
// This is a placeholder implementation
// In production, you'd use a proper Ed25519 library like BouncyCastle
val digest = MessageDigest.getInstance("SHA-256")
val combined = ByteArray(privateKey.size + data.size)
System.arraycopy(privateKey, 0, combined, 0, privateKey.size)
System.arraycopy(data, 0, combined, privateKey.size, data.size)
return digest.digest(combined)
}
/**
* Verify Ed25519 signature (simplified implementation)
*/
private fun verifyWithEd25519(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean {
// This is a placeholder implementation
// In production, you'd use a proper Ed25519 library like BouncyCastle
val privateKeyFromPublic = publicKey // This is obviously not correct, but for demo
val expectedSignature = signWithEd25519(data, privateKeyFromPublic)
return signature.contentEquals(expectedSignature)
}
/** /**
* Clean shutdown * Clean shutdown
*/ */
@@ -56,7 +56,7 @@ data class BitchatPacket(
val recipientID: ByteArray? = null, val recipientID: ByteArray? = null,
val timestamp: ULong, val timestamp: ULong,
val payload: ByteArray, val payload: ByteArray,
val signature: ByteArray? = null, var signature: ByteArray? = null, // Changed from val to var for packet signing
var ttl: UByte var ttl: UByte
) : Parcelable { ) : Parcelable {
@@ -80,6 +80,26 @@ data class BitchatPacket(
return BinaryProtocol.encode(this) return BinaryProtocol.encode(this)
} }
/**
* Create binary representation for signing (without signature and TTL fields)
* TTL is excluded because it changes during packet relay operations
*/
fun toBinaryDataForSigning(): ByteArray? {
// Create a copy without signature and with fixed TTL for signing
// TTL must be excluded because it changes during relay
val unsignedPacket = BitchatPacket(
version = version,
type = type,
senderID = senderID,
recipientID = recipientID,
timestamp = timestamp,
payload = payload,
signature = null, // Remove signature for signing
ttl = 0u // Use fixed TTL=0 for signing to ensure relay compatibility
)
return BinaryProtocol.encode(unsignedPacket)
}
companion object { companion object {
fun fromBinaryData(data: ByteArray): BitchatPacket? { fun fromBinaryData(data: ByteArray): BitchatPacket? {
return BinaryProtocol.decode(data) return BinaryProtocol.decode(data)