mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
106 lines
4.3 KiB
Kotlin
106 lines
4.3 KiB
Kotlin
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) }
|
|
}
|