mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 03:45:21 +00:00
wip
This commit is contained in:
@@ -24,6 +24,8 @@ class SecureIdentityStateManager(private val context: Context) {
|
||||
private const val PREFS_NAME = "bitchat_identity"
|
||||
private const val KEY_STATIC_PRIVATE_KEY = "static_private_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_NEXT_ROTATION_INTERVAL = "next_rotation_interval"
|
||||
|
||||
@@ -108,6 +110,64 @@ class SecureIdentityStateManager(private val context: Context) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
@@ -575,8 +575,15 @@ class BluetoothMeshService(private val context: Context) {
|
||||
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
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey)
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
|
||||
val tlvPayload = announcement.encode()
|
||||
if (tlvPayload == null) {
|
||||
Log.e(TAG, "Failed to encode announcement as TLV")
|
||||
@@ -610,8 +617,15 @@ class BluetoothMeshService(private val context: Context) {
|
||||
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
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey)
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
|
||||
val tlvPayload = announcement.encode()
|
||||
if (tlvPayload == null) {
|
||||
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
|
||||
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 publicKey = announcement.publicKey
|
||||
val noisePublicKey = announcement.noisePublicKey
|
||||
val signingPublicKey = announcement.signingPublicKey
|
||||
|
||||
// Notify delegate to handle peer management with nickname
|
||||
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(
|
||||
newPeerID = peerID,
|
||||
nickname = nickname,
|
||||
publicKey = publicKey,
|
||||
publicKey = noisePublicKey,
|
||||
previousPeerID = null
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import com.bitchat.android.util.*
|
||||
@Parcelize
|
||||
data class IdentityAnnouncement(
|
||||
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 {
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,8 @@ data class IdentityAnnouncement(
|
||||
*/
|
||||
private enum class TLVType(val value: UByte) {
|
||||
NICKNAME(0x01u),
|
||||
NOISE_PUBLIC_KEY(0x02u);
|
||||
NOISE_PUBLIC_KEY(0x02u),
|
||||
SIGNING_PUBLIC_KEY(0x03u); // NEW: Ed25519 signing public key
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: UByte): TLVType? {
|
||||
@@ -35,7 +37,7 @@ data class IdentityAnnouncement(
|
||||
val nicknameData = nickname.toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Check size limits
|
||||
if (nicknameData.size > 255 || publicKey.size > 255) {
|
||||
if (nicknameData.size > 255 || noisePublicKey.size > 255 || signingPublicKey.size > 255) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -46,10 +48,15 @@ data class IdentityAnnouncement(
|
||||
result.add(nicknameData.size.toByte())
|
||||
result.addAll(nicknameData.toList())
|
||||
|
||||
// TLV for public key
|
||||
// TLV for noise public key
|
||||
result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte())
|
||||
result.add(publicKey.size.toByte())
|
||||
result.addAll(publicKey.toList())
|
||||
result.add(noisePublicKey.size.toByte())
|
||||
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()
|
||||
}
|
||||
@@ -64,12 +71,13 @@ data class IdentityAnnouncement(
|
||||
|
||||
var offset = 0
|
||||
var nickname: String? = null
|
||||
var publicKey: ByteArray? = null
|
||||
var noisePublicKey: ByteArray? = null
|
||||
var signingPublicKey: ByteArray? = null
|
||||
|
||||
while (offset + 2 <= dataCopy.size) {
|
||||
// Read TLV type
|
||||
val typeValue = dataCopy[offset].toUByte()
|
||||
val type = TLVType.fromValue(typeValue) ?: return null
|
||||
val type = TLVType.fromValue(typeValue)
|
||||
offset += 1
|
||||
|
||||
// Read TLV length
|
||||
@@ -83,19 +91,27 @@ data class IdentityAnnouncement(
|
||||
val value = dataCopy.sliceArray(offset until offset + length)
|
||||
offset += length
|
||||
|
||||
// Process known TLV types, skip unknown ones for forward compatibility
|
||||
when (type) {
|
||||
TLVType.NICKNAME -> {
|
||||
nickname = String(value, Charsets.UTF_8)
|
||||
}
|
||||
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
|
||||
return if (nickname != null && publicKey != null) {
|
||||
IdentityAnnouncement(nickname, publicKey)
|
||||
// All three fields are required
|
||||
return if (nickname != null && noisePublicKey != null && signingPublicKey != null) {
|
||||
IdentityAnnouncement(nickname, noisePublicKey, signingPublicKey)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -110,18 +126,20 @@ data class IdentityAnnouncement(
|
||||
other as IdentityAnnouncement
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = nickname.hashCode()
|
||||
result = 31 * result + publicKey.contentHashCode()
|
||||
result = 31 * result + noisePublicKey.contentHashCode()
|
||||
result = 31 * result + signingPublicKey.contentHashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
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 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
|
||||
|
||||
@@ -69,6 +73,23 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
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
|
||||
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey)
|
||||
|
||||
@@ -86,6 +107,13 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
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)
|
||||
@@ -338,6 +366,118 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -56,7 +56,7 @@ data class BitchatPacket(
|
||||
val recipientID: ByteArray? = null,
|
||||
val timestamp: ULong,
|
||||
val payload: ByteArray,
|
||||
val signature: ByteArray? = null,
|
||||
var signature: ByteArray? = null, // Changed from val to var for packet signing
|
||||
var ttl: UByte
|
||||
) : Parcelable {
|
||||
|
||||
@@ -80,6 +80,26 @@ data class BitchatPacket(
|
||||
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 {
|
||||
fun fromBinaryData(data: ByteArray): BitchatPacket? {
|
||||
return BinaryProtocol.decode(data)
|
||||
|
||||
Reference in New Issue
Block a user