mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 13:25:21 +00:00
QR and Verification feature (#529)
* Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025 * Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025 * refactor: new close button like ios(but not liquid glass) * Automated update of relay data - Sun Oct 5 06:20:09 UTC 2025 * Automated update of relay data - Sun Oct 12 06:20:12 UTC 2025 * Automated update of relay data - Sun Oct 19 06:21:51 UTC 2025 * Automated update of relay data - Sun Oct 26 06:21:31 UTC 2025 * Automated update of relay data - Sun Nov 2 06:22:16 UTC 2025 * Automated update of relay data - Sun Nov 9 06:21:43 UTC 2025 * Automated update of relay data - Sun Nov 16 06:22:37 UTC 2025 * Automated update of relay data - Sun Nov 23 06:22:51 UTC 2025 * Automated update of relay data - Sun Nov 30 06:24:08 UTC 2025 * Automated update of relay data - Sun Dec 7 06:22:59 UTC 2025 * Automated update of relay data - Sun Dec 14 06:24:33 UTC 2025 * Automated update of relay data - Sun Dec 21 06:24:49 UTC 2025 * Automated update of relay data - Sun Dec 28 06:25:38 UTC 2025 * feat: Add ZXing dependency for QR code scanning * feat: Request camera permission for QR verification * Add QR verification payloads and mesh wiring * Wire verification state, system messages, and notifications * Add verification sheets and UI affordances * Show verified badges in sidebar and add strings * Persist fingerprint caches for offline verification * Handle bitchat://verify deep links * feat: Replace zxing-android-embedded with ML Kit and CameraX * Refactor(Verification): Replace zxing with MLKit for QR scanning * Replace `AndroidView` with `CameraXViewfinder` for camera preview * Refactor QR verification: Extract VerificationHandler and fix concurrency issues * Extract and translate strings for QR verification feature * Fix build errors: Escape ampersands in strings and restore missing methods in ChatViewModel * return to main * return to main 2 --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
co-authored by
GitHub Action
callebtc
parent
d73976537d
commit
c663e8ede0
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user