mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 02:05:20 +00:00
Migrate private media without silent downgrades
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.content.Context
|
||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||
import com.bitchat.android.model.PeerCapabilities
|
||||
import java.security.MessageDigest
|
||||
|
||||
internal interface PrivateMediaCapabilityPinStore {
|
||||
fun contains(fingerprint: String): Boolean
|
||||
fun insert(fingerprint: String)
|
||||
}
|
||||
|
||||
internal class SecurePrivateMediaCapabilityPinStore(context: Context) :
|
||||
PrivateMediaCapabilityPinStore {
|
||||
private val identityState = SecureIdentityStateManager(context.applicationContext)
|
||||
|
||||
override fun contains(fingerprint: String): Boolean =
|
||||
identityState.isPrivateMediaCapable(fingerprint)
|
||||
|
||||
override fun insert(fingerprint: String) {
|
||||
identityState.markPrivateMediaCapable(fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed interface PrivateMediaPolicyDecision {
|
||||
data object Encrypted : PrivateMediaPolicyDecision
|
||||
data object RequiresLegacyConsent : PrivateMediaPolicyDecision
|
||||
data object NeedsHandshake : PrivateMediaPolicyDecision
|
||||
data class Blocked(val reason: String) : PrivateMediaPolicyDecision
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds an advertised private-media capability to a live Noise remote-static
|
||||
* key and persists an HSTS-style pin by that authenticated key's SHA-256
|
||||
* fingerprint. Announcements alone can never create a pin.
|
||||
*/
|
||||
internal class PrivateMediaSecurityController(
|
||||
private val peerInfoProvider: (String) -> PeerInfo?,
|
||||
private val authenticatedRemoteStaticProvider: (String) -> ByteArray?,
|
||||
private val pinStore: PrivateMediaCapabilityPinStore
|
||||
) {
|
||||
fun refreshAuthenticatedCapability(peerID: String): Boolean {
|
||||
val remoteStatic = authenticatedRemoteStaticProvider(peerID)
|
||||
?.takeIf { it.size == 32 }
|
||||
?: return false
|
||||
val peer = peerInfoProvider(peerID) ?: return false
|
||||
if (!peer.hasVerifiedAnnouncement) return false
|
||||
val announcedStatic = peer.verifiedAnnouncementNoisePublicKey ?: return false
|
||||
if (!announcedStatic.contentEquals(remoteStatic)) return false
|
||||
if (peer.capabilities?.contains(PeerCapabilities.PRIVATE_MEDIA) != true) return false
|
||||
|
||||
pinStore.insert(fingerprint(remoteStatic))
|
||||
return true
|
||||
}
|
||||
|
||||
fun sendPolicy(peerID: String): PrivateMediaPolicyDecision {
|
||||
val remoteStatic = authenticatedRemoteStaticProvider(peerID)
|
||||
?.takeIf { it.size == 32 }
|
||||
?: return PrivateMediaPolicyDecision.NeedsHandshake
|
||||
val authenticatedFingerprint = fingerprint(remoteStatic)
|
||||
|
||||
// Once a fingerprint has authenticated encrypted media support, never
|
||||
// downgrade it because a later announce omits or clears the bit.
|
||||
if (pinStore.contains(authenticatedFingerprint)) {
|
||||
return PrivateMediaPolicyDecision.Encrypted
|
||||
}
|
||||
|
||||
val peer = peerInfoProvider(peerID)
|
||||
?: return PrivateMediaPolicyDecision.Blocked(
|
||||
"No signature-verified identity announcement is available"
|
||||
)
|
||||
if (!peer.hasVerifiedAnnouncement) {
|
||||
return PrivateMediaPolicyDecision.Blocked(
|
||||
"The peer identity announcement has not been verified"
|
||||
)
|
||||
}
|
||||
val announcedStatic = peer.verifiedAnnouncementNoisePublicKey
|
||||
?: return PrivateMediaPolicyDecision.Blocked(
|
||||
"The verified announcement did not contain a Noise identity"
|
||||
)
|
||||
if (!announcedStatic.contentEquals(remoteStatic)) {
|
||||
return PrivateMediaPolicyDecision.Blocked(
|
||||
"The authenticated Noise identity does not match the verified announcement"
|
||||
)
|
||||
}
|
||||
|
||||
return if (peer.capabilities?.contains(PeerCapabilities.PRIVATE_MEDIA) == true) {
|
||||
pinStore.insert(authenticatedFingerprint)
|
||||
PrivateMediaPolicyDecision.Encrypted
|
||||
} else {
|
||||
// Both a missing TLV and an explicitly empty bitfield are legacy.
|
||||
PrivateMediaPolicyDecision.RequiresLegacyConsent
|
||||
}
|
||||
}
|
||||
|
||||
internal fun authenticatedFingerprint(peerID: String): String? =
|
||||
authenticatedRemoteStaticProvider(peerID)
|
||||
?.takeIf { it.size == 32 }
|
||||
?.let(::fingerprint)
|
||||
|
||||
private fun fingerprint(publicKey: ByteArray): String =
|
||||
MessageDigest.getInstance("SHA-256")
|
||||
.digest(publicKey)
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
Reference in New Issue
Block a user