Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9e25ee4
Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025
actions-user Sep 21, 2025
6cccaae
Merge remote-tracking branch 'origin/main'
yet300 Sep 24, 2025
95358ac
Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025
actions-user Sep 28, 2025
696f698
refactor: new close button like ios(but not liquid glass)
yet300 Sep 30, 2025
e0c7240
Merge remote-tracking branch 'origin/main'
yet300 Sep 30, 2025
776d6c5
Automated update of relay data - Sun Oct 5 06:20:09 UTC 2025
actions-user Oct 5, 2025
ce31b85
Automated update of relay data - Sun Oct 12 06:20:12 UTC 2025
actions-user Oct 12, 2025
2703c52
Merge branch 'main' into main
yet300 Oct 13, 2025
19f6473
Automated update of relay data - Sun Oct 19 06:21:51 UTC 2025
actions-user Oct 19, 2025
f24d170
Merge remote-tracking branch 'origin/main'
yet300 Oct 25, 2025
cd02bc8
Automated update of relay data - Sun Oct 26 06:21:31 UTC 2025
actions-user Oct 26, 2025
4b84db0
Automated update of relay data - Sun Nov 2 06:22:16 UTC 2025
actions-user Nov 2, 2025
bb3dd9c
Automated update of relay data - Sun Nov 9 06:21:43 UTC 2025
actions-user Nov 9, 2025
cae1e3d
Automated update of relay data - Sun Nov 16 06:22:37 UTC 2025
actions-user Nov 16, 2025
1143e49
Merge remote-tracking branch 'upstream/main'
yet300 Nov 19, 2025
57f4c03
Merge remote-tracking branch 'origin/main'
yet300 Nov 19, 2025
0bd034b
Automated update of relay data - Sun Nov 23 06:22:51 UTC 2025
actions-user Nov 23, 2025
205cc3c
Automated update of relay data - Sun Nov 30 06:24:08 UTC 2025
actions-user Nov 30, 2025
3f0d533
Automated update of relay data - Sun Dec 7 06:22:59 UTC 2025
actions-user Dec 7, 2025
32806ea
Merge remote-tracking branch 'upstream/main'
yet300 Dec 13, 2025
2edf8e2
Automated update of relay data - Sun Dec 14 06:24:33 UTC 2025
actions-user Dec 14, 2025
d0a27be
Automated update of relay data - Sun Dec 21 06:24:49 UTC 2025
actions-user Dec 21, 2025
069dcc0
Automated update of relay data - Sun Dec 28 06:25:38 UTC 2025
actions-user Dec 28, 2025
0096772
feat: Add ZXing dependency for QR code scanning
yet300 Dec 30, 2025
a642b22
feat: Request camera permission for QR verification
yet300 Dec 30, 2025
0b814be
Add QR verification payloads and mesh wiring
yet300 Dec 31, 2025
4cff0ad
Wire verification state, system messages, and notifications
yet300 Dec 31, 2025
3017c7a
Add verification sheets and UI affordances
yet300 Dec 31, 2025
bd14442
Show verified badges in sidebar and add strings
yet300 Dec 31, 2025
0c99012
Persist fingerprint caches for offline verification
yet300 Dec 31, 2025
0e75e38
Handle bitchat://verify deep links
yet300 Dec 31, 2025
e9aca10
feat: Replace zxing-android-embedded with ML Kit and CameraX
yet300 Jan 3, 2026
4a04433
Refactor(Verification): Replace zxing with MLKit for QR scanning
yet300 Jan 3, 2026
e07b79f
Replace `AndroidView` with `CameraXViewfinder` for camera preview
yet300 Jan 3, 2026
6b1ec53
Merge branch 'main' into feature/qr
callebtc Jan 4, 2026
00cc9b3
Refactor QR verification: Extract VerificationHandler and fix concurr…
callebtc Jan 4, 2026
3dee13c
Extract and translate strings for QR verification feature
callebtc Jan 4, 2026
ec2d6d7
Fix build errors: Escape ampersands in strings and restore missing me…
callebtc Jan 4, 2026
b8a5bc8
return to main
callebtc Jan 4, 2026
0a051ee
return to main 2
callebtc Jan 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ dependencies {

// Permissions
implementation(libs.accompanist.permissions)

// QR
implementation(libs.zxing.core)
implementation(libs.mlkit.barcode.scanning)

// CameraX
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.compose)

// Cryptography
implementation(libs.bundles.cryptography)
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

<!-- Microphone for voice notes -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Camera for QR verification -->
<uses-permission android:name="android.permission.CAMERA" />

<!-- Storage permissions for file sharing -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
Expand All @@ -47,6 +49,7 @@
<!-- Hardware features -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.camera" android:required="false" />

<permission
android:name="com.bitchat.android.permission.FORCE_FINISH"
Expand Down Expand Up @@ -89,6 +92,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitchat" android:host="verify" />
</intent-filter>
</activity>

<!-- Persistent foreground service to run the mesh in background -->
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/com/bitchat/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.bitchat.android.ui.ChatViewModel
import com.bitchat.android.ui.OrientationAwareActivity
import com.bitchat.android.ui.theme.BitchatTheme
import com.bitchat.android.nostr.PoWPreferenceManager
import com.bitchat.android.services.VerificationService
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -655,6 +656,7 @@ class MainActivity : OrientationAwareActivity() {

// Handle any notification intent
handleNotificationIntent(intent)
handleVerificationIntent(intent)

// Small delay to ensure mesh service is fully initialized
delay(500)
Expand Down Expand Up @@ -683,6 +685,7 @@ class MainActivity : OrientationAwareActivity() {
// Handle notification intents when app is already running
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
handleNotificationIntent(intent)
handleVerificationIntent(intent)
}
}

Expand Down Expand Up @@ -788,6 +791,17 @@ class MainActivity : OrientationAwareActivity() {
}
}

private fun handleVerificationIntent(intent: Intent) {
val uri = intent.data ?: return
if (uri.scheme != "bitchat" || uri.host != "verify") return

chatViewModel.showVerificationSheet()
val qr = VerificationService.verifyScannedQR(uri.toString())
if (qr != null) {
chatViewModel.beginQRVerification(qr)
}
}


override fun onDestroy() {
super.onDestroy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import java.security.MessageDigest
import android.util.Base64
import android.util.Log
import com.bitchat.android.util.hexEncodedString
import androidx.core.content.edit

/**
* Manages persistent identity storage and peer ID rotation - 100% compatible with iOS implementation
Expand All @@ -24,9 +27,15 @@ class SecureIdentityStateManager(private val context: Context) {
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_VERIFIED_FINGERPRINTS = "verified_fingerprints"
private const val KEY_CACHED_PEER_FINGERPRINTS = "cached_peer_fingerprints"
private const val KEY_CACHED_PEER_NOISE_KEYS = "cached_peer_noise_keys"
private const val KEY_CACHED_NOISE_FINGERPRINTS = "cached_noise_fingerprints"
private const val KEY_CACHED_FINGERPRINT_NICKNAMES = "cached_fingerprint_nicknames"
}

private val prefs: SharedPreferences
private val lock = Any()

init {
// Create master key for encryption
Expand Down Expand Up @@ -168,7 +177,7 @@ class SecureIdentityStateManager(private val context: Context) {
fun generateFingerprint(publicKeyData: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(publicKeyData)
return hash.joinToString("") { "%02x".format(it) }
return hash.hexEncodedString()
}

/**
Expand All @@ -178,6 +187,112 @@ class SecureIdentityStateManager(private val context: Context) {
// SHA-256 fingerprint should be 64 hex characters
return fingerprint.matches(Regex("^[a-fA-F0-9]{64}$"))
}

// MARK: - Verified Fingerprints

fun getVerifiedFingerprints(): Set<String> {
return prefs.getStringSet(KEY_VERIFIED_FINGERPRINTS, emptySet())?.toSet() ?: emptySet()
}

fun isVerifiedFingerprint(fingerprint: String): Boolean {
return getVerifiedFingerprints().contains(fingerprint)
}

fun setVerifiedFingerprint(fingerprint: String, verified: Boolean) {
if (!isValidFingerprint(fingerprint)) return
synchronized(lock) {
val current = prefs.getStringSet(KEY_VERIFIED_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
if (verified) {
current.add(fingerprint)
} else {
current.remove(fingerprint)
}
prefs.edit { putStringSet(KEY_VERIFIED_FINGERPRINTS, current) }
}
}

fun getCachedPeerFingerprint(peerID: String): String? {
val pid = peerID.lowercase()
// Reading is safe without lock for SharedPreferences, but synchronizing ensures memory visibility
// if we are paranoid, but SharedPreferences is generally thread-safe for reads.
// However, to ensure we don't read a partial update (unlikely with SP), we can leave it.
// The critical part is the write.
val entries = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet()) ?: return null
val entry = entries.firstOrNull { it.startsWith("$pid:") } ?: return null
return entry.substringAfter(':').takeIf { isValidFingerprint(it) }
}

fun cachePeerFingerprint(peerID: String, fingerprint: String) {
if (!isValidFingerprint(fingerprint)) return
val pid = peerID.lowercase()
synchronized(lock) {
val current = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
current.removeAll { it.startsWith("$pid:") }
current.add("$pid:$fingerprint")
prefs.edit { putStringSet(KEY_CACHED_PEER_FINGERPRINTS, current) }
}
}

fun getCachedNoiseKey(peerID: String): String? {
val pid = peerID.lowercase()
val entries = prefs.getStringSet(KEY_CACHED_PEER_NOISE_KEYS, emptySet()) ?: return null
val entry = entries.firstOrNull { it.startsWith("$pid=") } ?: return null
return entry.substringAfter('=').takeIf { it.matches(Regex("^[a-fA-F0-9]{64}$")) }
}

fun cachePeerNoiseKey(peerID: String, noiseKeyHex: String) {
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
val pid = peerID.lowercase()
synchronized(lock) {
val current = prefs.getStringSet(KEY_CACHED_PEER_NOISE_KEYS, emptySet())?.toMutableSet() ?: mutableSetOf()
current.removeAll { it.startsWith("$pid=") }
current.add("$pid=${noiseKeyHex.lowercase()}")
prefs.edit { putStringSet(KEY_CACHED_PEER_NOISE_KEYS, current) }
}
}

fun getCachedNoiseFingerprint(noiseKeyHex: String): String? {
val key = noiseKeyHex.lowercase()
val entries = prefs.getStringSet(KEY_CACHED_NOISE_FINGERPRINTS, emptySet()) ?: return null
val entry = entries.firstOrNull { it.startsWith("$key=") } ?: return null
return entry.substringAfter('=').takeIf { isValidFingerprint(it) }
}

fun cacheNoiseFingerprint(noiseKeyHex: String, fingerprint: String) {
if (!isValidFingerprint(fingerprint)) return
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
val key = noiseKeyHex.lowercase()
synchronized(lock) {
val current = prefs.getStringSet(KEY_CACHED_NOISE_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
current.removeAll { it.startsWith("$key=") }
current.add("$key=$fingerprint")
prefs.edit { putStringSet(KEY_CACHED_NOISE_FINGERPRINTS, current) }
}
}

fun getCachedFingerprintNickname(fingerprint: String): String? {
if (!isValidFingerprint(fingerprint)) return null
val key = fingerprint.lowercase()
val entries = prefs.getStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, emptySet()) ?: return null
val entry = entries.firstOrNull { it.startsWith("$key=") } ?: return null
val encoded = entry.substringAfter('=')
return runCatching {
val bytes = Base64.decode(encoded, Base64.NO_WRAP)
String(bytes, Charsets.UTF_8)
}.getOrNull()
}

fun cacheFingerprintNickname(fingerprint: String, nickname: String) {
if (!isValidFingerprint(fingerprint)) return
val key = fingerprint.lowercase()
val encoded = Base64.encodeToString(nickname.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
synchronized(lock) {
val current = prefs.getStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, emptySet())?.toMutableSet() ?: mutableSetOf()
current.removeAll { it.startsWith("$key=") }
current.add("$key=$encoded")
prefs.edit { putStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, current) }
}
}

// MARK: - Peer ID Rotation Management (removed)
// Android now derives peer ID from the persisted Noise identity fingerprint.
Expand Down
62 changes: 62 additions & 0 deletions app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.protocol.MessagePadding
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.model.IdentityAnnouncement
import com.bitchat.android.model.NoisePayload
import com.bitchat.android.model.NoisePayloadType
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.protocol.SpecialRecipients
import com.bitchat.android.model.RequestSyncPacket
import com.bitchat.android.sync.GossipSyncManager
import com.bitchat.android.util.toHexString
import com.bitchat.android.services.VerificationService
import kotlinx.coroutines.*
import java.util.*
import kotlin.math.sign
Expand Down Expand Up @@ -72,6 +75,7 @@ class BluetoothMeshService(private val context: Context) {

init {
Log.i(TAG, "Initializing BluetoothMeshService for peer=$myPeerID")
VerificationService.configure(encryptionService)
setupDelegates()
messageHandler.packetProcessor = packetProcessor
//startPeriodicDebugLogging()
Expand Down Expand Up @@ -414,6 +418,14 @@ class BluetoothMeshService(private val context: Context) {
override fun onReadReceiptReceived(messageID: String, peerID: String) {
delegate?.didReceiveReadReceipt(messageID, peerID)
}

override fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveVerifyChallenge(peerID, payload, timestampMs)
}

override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveVerifyResponse(peerID, payload, timestampMs)
}
}

// PacketProcessor delegates
Expand Down Expand Up @@ -939,6 +951,50 @@ class BluetoothMeshService(private val context: Context) {
}
}
}

// MARK: QR Verification over Noise

fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
val tlv = VerificationService.buildVerifyChallenge(noiseKeyHex, nonceA)
val payload = NoisePayload(
type = NoisePayloadType.VERIFY_CHALLENGE,
data = tlv
)
sendNoisePayloadToPeer(payload, peerID, "verify challenge")
}

fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
val tlv = VerificationService.buildVerifyResponse(noiseKeyHex, nonceA) ?: return
val payload = NoisePayload(
type = NoisePayloadType.VERIFY_RESPONSE,
data = tlv
)
sendNoisePayloadToPeer(payload, peerID, "verify response")
}

private fun sendNoisePayloadToPeer(payload: NoisePayload, recipientPeerID: String, label: String) {
serviceScope.launch {
try {
val encrypted = encryptionService.encrypt(payload.encode(), recipientPeerID)
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(recipientPeerID),
timestamp = System.currentTimeMillis().toULong(),
payload = encrypted,
signature = null,
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
)

val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
Log.d(TAG, "📤 Sent $label to $recipientPeerID (${payload.data.size} bytes)")
} catch (e: Exception) {
Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}")
}
}
}

/**
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
Expand Down Expand Up @@ -1127,6 +1183,10 @@ class BluetoothMeshService(private val context: Context) {
fun getIdentityFingerprint(): String {
return encryptionService.getIdentityFingerprint()
}

fun getStaticNoisePublicKey(): ByteArray? {
return encryptionService.getStaticPublicKey()
}

/**
* Check if encryption icon should be shown for a peer
Expand Down Expand Up @@ -1283,6 +1343,8 @@ interface BluetoothMeshDelegate {
fun didReceiveChannelLeave(channel: String, fromPeer: String)
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long)
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long)
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
fun getNickname(): String?
fun isFavorite(peerID: String): Boolean
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
// Simplified: Call delegate with messageID and peerID directly
delegate?.onReadReceiptReceived(messageID, peerID)
}
com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> {
Log.d(TAG, "🔐 Verify challenge received from $peerID (${noisePayload.data.size} bytes)")
delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong())
}
com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> {
Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)")
delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong())
}
}

} catch (e: Exception) {
Expand Down Expand Up @@ -611,4 +619,6 @@ interface MessageHandlerDelegate {
fun onChannelLeave(channel: String, fromPeer: String)
fun onDeliveryAckReceived(messageID: String, peerID: String)
fun onReadReceiptReceived(messageID: String, peerID: String)
fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long)
fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long)
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ enum class NoisePayloadType(val value: UByte) {
PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding
READ_RECEIPT(0x02u), // Message was read
DELIVERED(0x03u), // Message was delivered
VERIFY_CHALLENGE(0x10u), // Verification challenge
VERIFY_RESPONSE(0x11u), // Verification response
FILE_TRANSFER(0x20u);


Expand Down
Loading
Loading