Source added

This commit is contained in:
Fr4nz D13trich 2025-11-20 09:26:33 +01:00
parent b2864b500e
commit ba28ca859e
8352 changed files with 1487182 additions and 1 deletions

1
microbenchmark/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,36 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontobfuscate
-ignorewarnings
-keepattributes *Annotation*
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn androidx.test.**
-dontwarn org.junit.**
-dontwarn com.squareup.javawriter.JavaWriter
-keepclasseswithmembers @org.junit.runner.RunWith public class *

View file

@ -0,0 +1,64 @@
@file:Suppress("UnstableApiUsage")
plugins {
id("com.android.library")
id("androidx.benchmark")
id("org.jetbrains.kotlin.android")
id("ktlint")
}
val signalBuildToolsVersion: String by rootProject.extra
val signalCompileSdkVersion: String by rootProject.extra
val signalTargetSdkVersion: Int by rootProject.extra
val signalMinSdkVersion: Int by rootProject.extra
val signalJavaVersion: JavaVersion by rootProject.extra
val signalKotlinJvmTarget: String by rootProject.extra
android {
namespace = "org.signal.microbenchmark"
compileSdkVersion = signalCompileSdkVersion
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = signalJavaVersion
targetCompatibility = signalJavaVersion
}
kotlinOptions {
jvmTarget = signalKotlinJvmTarget
}
defaultConfig {
minSdk = signalMinSdkVersion
testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
}
testBuildType = "release"
buildTypes {
debug {
// Since isDebuggable can't be modified by gradle for library modules,
// it must be done in a manifest - see src/androidTest/AndroidManifest.xml
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro")
}
release {
isDefault = true
}
}
}
dependencies {
coreLibraryDesugaring(libs.android.tools.desugar)
lintChecks(project(":lintchecks"))
implementation(project(":core-util"))
// Base dependencies
androidTestImplementation(testLibs.junit.junit)
androidTestImplementation(benchmarkLibs.androidx.test.ext.junit)
androidTestImplementation(benchmarkLibs.androidx.benchmark.micro)
// Dependencies of modules being tested
androidTestImplementation(project(":libsignal-service"))
androidTestImplementation(libs.libsignal.android)
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--
Important: disable debugging for accurate performance results
In a com.android.library project, this flag must be disabled from this
manifest, as it is not possible to override this flag from Gradle.
-->
<application
android:debuggable="false"
tools:ignore="HardcodedDebugMode"
tools:replace="android:debuggable" />
</manifest>

View file

@ -0,0 +1,124 @@
package org.signal.microbenchmark
import android.util.Log
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.libsignal.protocol.logging.SignalProtocolLogger
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
import org.signal.util.SignalClient
import org.whispersystems.signalservice.api.push.DistributionId
import java.util.Optional
/**
* Benchmarks for decrypting messages.
*
* Note that in order to isolate all costs to just the process of decryption itself,
* all operations are performed in in-memory stores.
*/
@RunWith(AndroidJUnit4::class)
class ProtocolBenchmarks {
@get:Rule
val benchmarkRule = BenchmarkRule()
@Before
fun setup() {
SignalProtocolLoggerProvider.setProvider { priority, tag, message ->
when (priority) {
SignalProtocolLogger.VERBOSE -> Log.v(tag, message)
SignalProtocolLogger.DEBUG -> Log.d(tag, message)
SignalProtocolLogger.INFO -> Log.i(tag, message)
SignalProtocolLogger.WARN -> Log.w(tag, message)
SignalProtocolLogger.ERROR -> Log.w(tag, message)
SignalProtocolLogger.ASSERT -> Log.e(tag, message)
}
}
}
@Test
fun decrypt_unsealedSender() {
val (alice, bob) = buildAndInitializeClients()
benchmarkRule.measureRepeated {
val envelope = runWithTimingDisabled {
alice.encryptUnsealedSender(bob)
}
bob.decryptMessage(envelope)
// Respond so that the session ratchets
runWithTimingDisabled {
alice.decryptMessage(bob.encryptUnsealedSender(alice))
}
}
}
@Test
fun decrypt_sealedSender() {
val (alice, bob) = buildAndInitializeClients()
benchmarkRule.measureRepeated {
val envelope = runWithTimingDisabled {
alice.encryptSealedSender(bob)
}
bob.decryptMessage(envelope)
// Respond so that the session ratchets
runWithTimingDisabled {
alice.decryptMessage(bob.encryptSealedSender(alice))
}
}
}
@Test
fun multi_encrypt_sealedSender() {
val recipientCount = 10
val clients = buildAndInitializeClients(recipientCount)
val alice = clients.first()
val others = clients.filterNot { it == alice }
val distributionId = DistributionId.create()
clients.forEach {
it.initializedGroupSession(distributionId)
}
benchmarkRule.measureRepeated {
alice.multiEncryptSealedSender(distributionId, others, Optional.empty())
}
}
private fun buildAndInitializeClients(): Pair<SignalClient, SignalClient> {
val clients = buildAndInitializeClients(2)
return clients[0] to clients[1]
}
private fun buildAndInitializeClients(recipientCount: Int): List<SignalClient> {
val clients = ArrayList<SignalClient>(recipientCount)
for (n in 1..recipientCount) {
clients.add(SignalClient())
}
clients.forEach { alice ->
clients.filterNot { it == alice }.forEach { bob ->
alice.initializeSession(bob)
bob.initializeSession(alice)
alice.decryptMessage(bob.encryptUnsealedSender(alice))
bob.decryptMessage(alice.encryptUnsealedSender(bob))
alice.decryptMessage(bob.encryptSealedSender(alice))
bob.decryptMessage(alice.encryptSealedSender(bob))
}
}
return clients
}
}

View file

@ -0,0 +1,210 @@
package org.signal.util
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
import org.signal.libsignal.protocol.state.IdentityKeyStore
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.state.SessionRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
import org.whispersystems.signalservice.api.push.DistributionId
import java.util.UUID
/**
* An in-memory datastore specifically designed for tests.
*/
class InMemorySignalServiceAccountDataStore : SignalServiceAccountDataStore {
private val identityKey: IdentityKeyPair = IdentityKeyPair.generate()
private val identities: MutableMap<SignalProtocolAddress, IdentityKey> = mutableMapOf()
private val oneTimeEcPreKeys: MutableMap<Int, PreKeyRecord> = mutableMapOf()
private val signedPreKeys: MutableMap<Int, SignedPreKeyRecord> = mutableMapOf()
private var sessions: MutableMap<SignalProtocolAddress, SessionRecord> = mutableMapOf()
private val senderKeys: MutableMap<SenderKeyLocator, SenderKeyRecord> = mutableMapOf()
private val kyberPreKeys: MutableMap<Int, KyberPreKeyRecord> = mutableMapOf()
override fun getIdentityKeyPair(): IdentityKeyPair {
return identityKey
}
override fun getLocalRegistrationId(): Int {
return 1
}
override fun saveIdentity(address: SignalProtocolAddress, identityKey: IdentityKey): IdentityChange {
val previous = identities.put(address, identityKey)
return if (previous == null || previous == identityKey) {
IdentityChange.NEW_OR_UNCHANGED
} else {
IdentityChange.REPLACED_EXISTING
}
}
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean {
return true
}
override fun getIdentity(address: SignalProtocolAddress): IdentityKey? {
return identities[address]
}
override fun loadPreKey(preKeyId: Int): PreKeyRecord {
return oneTimeEcPreKeys[preKeyId]!!
}
override fun storePreKey(preKeyId: Int, record: PreKeyRecord) {
oneTimeEcPreKeys[preKeyId] = record
}
override fun containsPreKey(preKeyId: Int): Boolean {
return oneTimeEcPreKeys.containsKey(preKeyId)
}
override fun removePreKey(preKeyId: Int) {
oneTimeEcPreKeys.remove(preKeyId)
}
override fun loadSession(address: SignalProtocolAddress): SessionRecord {
return sessions.getOrPut(address) { SessionRecord() }
}
override fun loadExistingSessions(addresses: List<SignalProtocolAddress>): List<SessionRecord> {
return addresses.map { sessions[it]!! }
}
override fun getSubDeviceSessions(name: String): List<Int> {
return sessions
.filter { it.key.name == name && it.key.deviceId != 1 && it.value.isValid() }
.map { it.key.deviceId }
}
override fun storeSession(address: SignalProtocolAddress, record: SessionRecord) {
sessions[address] = record
}
override fun containsSession(address: SignalProtocolAddress): Boolean {
return sessions[address]?.isValid() ?: false
}
override fun deleteSession(address: SignalProtocolAddress) {
sessions -= address
}
override fun deleteAllSessions(name: String) {
sessions = sessions.filter { it.key.name == name }.toMutableMap()
}
override fun loadSignedPreKey(signedPreKeyId: Int): SignedPreKeyRecord {
return signedPreKeys[signedPreKeyId]!!
}
override fun loadSignedPreKeys(): List<SignedPreKeyRecord> {
return signedPreKeys.values.toList()
}
override fun storeSignedPreKey(signedPreKeyId: Int, record: SignedPreKeyRecord) {
signedPreKeys[signedPreKeyId] = record
}
override fun containsSignedPreKey(signedPreKeyId: Int): Boolean {
return signedPreKeys.containsKey(signedPreKeyId)
}
override fun removeSignedPreKey(signedPreKeyId: Int) {
signedPreKeys -= signedPreKeyId
}
override fun storeSenderKey(sender: SignalProtocolAddress, distributionId: UUID, record: SenderKeyRecord) {
senderKeys[SenderKeyLocator(sender, distributionId)] = record
}
override fun loadSenderKey(sender: SignalProtocolAddress, distributionId: UUID): SenderKeyRecord? {
return senderKeys[SenderKeyLocator(sender, distributionId)]
}
override fun loadKyberPreKey(kyberPreKeyId: Int): KyberPreKeyRecord {
return kyberPreKeys[kyberPreKeyId]!!
}
override fun loadKyberPreKeys(): List<KyberPreKeyRecord> {
return kyberPreKeys.values.toList()
}
override fun storeKyberPreKey(kyberPreKeyId: Int, record: KyberPreKeyRecord?) {
error("Not used")
}
override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean {
return kyberPreKeys.containsKey(kyberPreKeyId)
}
override fun markKyberPreKeyUsed(kyberPreKeyId: Int, signedPreKeyId: Int, baseKey: ECPublicKey) {
kyberPreKeys.remove(kyberPreKeyId)
}
override fun deleteAllStaleOneTimeEcPreKeys(threshold: Long, minCount: Int) {
error("Not used")
}
override fun markAllOneTimeEcPreKeysStaleIfNecessary(staleTime: Long) {
error("Not used")
}
override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) {
error("Not used")
}
override fun removeKyberPreKey(kyberPreKeyId: Int) {
error("Not used")
}
override fun markAllOneTimeKyberPreKeysStaleIfNecessary(staleTime: Long) {
error("Not used")
}
override fun deleteAllStaleOneTimeKyberPreKeys(threshold: Long, minCount: Int) {
error("Not used")
}
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> {
error("Not used")
}
override fun archiveSession(address: SignalProtocolAddress) {
sessions[address]!!.archiveCurrentState()
}
override fun getAllAddressesWithActiveSessions(addressNames: MutableList<String>): MutableMap<SignalProtocolAddress, SessionRecord> {
return sessions
.filter { it.key.name in addressNames }
.filter { it.value.isValid() }
.toMutableMap()
}
override fun getSenderKeySharedWith(distributionId: DistributionId): Set<SignalProtocolAddress> {
error("Not used")
}
override fun markSenderKeySharedWith(distributionId: DistributionId, addresses: Collection<SignalProtocolAddress>) {
// Called, but not needed
}
override fun clearSenderKeySharedWith(addresses: Collection<SignalProtocolAddress>) {
// Called, but not needed
}
override fun isMultiDevice(): Boolean {
return false
}
private fun SessionRecord.isValid(): Boolean {
return this.hasSenderChain()
}
private data class SenderKeyLocator(val address: SignalProtocolAddress, val distributionId: UUID)
}

View file

@ -0,0 +1,205 @@
package org.signal.util
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.libsignal.metadata.certificate.CertificateValidator
import org.signal.libsignal.metadata.certificate.SenderCertificate
import org.signal.libsignal.metadata.certificate.ServerCertificate
import org.signal.libsignal.protocol.SessionBuilder
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.protocol.groups.GroupSessionBuilder
import org.signal.libsignal.protocol.kem.KEMKeyPair
import org.signal.libsignal.protocol.kem.KEMKeyType
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage
import org.signal.libsignal.protocol.state.PreKeyBundle
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
import org.whispersystems.signalservice.api.SignalSessionLock
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.util.toByteArray
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.util.Util
import java.util.Optional
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
import kotlin.random.Random
/**
* An in-memory signal client that can encrypt and decrypt messages.
*
* Has a single prekey bundle that can be used to initialize a session with another client.
*/
class SignalClient {
companion object {
private val trustRoot: ECKeyPair = ECKeyPair.generate()
}
private val lock = TestSessionLock()
private val aci: ACI = ACI.from(UUID.randomUUID())
private val store: SignalServiceAccountDataStore = InMemorySignalServiceAccountDataStore()
private var prekeyIndex = 0
private val unidentifiedAccessKey: ByteArray = Util.getSecretBytes(32)
private val senderCertificate: SenderCertificate = createCertificateFor(
trustRoot = trustRoot,
uuid = aci.rawUuid,
e164 = "+${Random.nextLong(1111111111L, 9999999999L)}",
deviceId = 1,
identityKey = store.identityKeyPair.publicKey.publicKey,
expires = Long.MAX_VALUE
)
private val cipher = SignalServiceCipher(SignalServiceAddress(aci), 1, store, lock, CertificateValidator(trustRoot.publicKey))
/**
* Sets up sessions using the [to] client's [preKeyBundles]. Note that you can only initialize a client up to 1,000 times because that's how many prekeys we have.
*/
fun initializeSession(to: SignalClient) {
val address = SignalProtocolAddress(to.aci.toString(), 1)
SessionBuilder(store, address).process(to.createPreKeyBundle())
}
fun initializedGroupSession(distributionId: DistributionId): SenderKeyDistributionMessage {
val self = SignalProtocolAddress(aci.toString(), 1)
return SignalGroupSessionBuilder(lock, GroupSessionBuilder(store)).create(self, distributionId.asUuid())
}
fun encryptUnsealedSender(to: SignalClient): Envelope {
val sentTimestamp = System.currentTimeMillis()
val content = Content(
dataMessage = DataMessage(
body = "Test Message",
timestamp = sentTimestamp
)
)
val outgoingPushMessage: OutgoingPushMessage = cipher.encrypt(
SignalProtocolAddress(to.aci.toString(), 1),
SealedSenderAccess.NONE,
EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty())
)
val encryptedContent: ByteArray = Base64.decode(outgoingPushMessage.content)
val serviceGuid = UUID.randomUUID()
return Envelope(
sourceServiceId = aci.toString(),
sourceDevice = 1,
destinationServiceId = to.aci.toString(),
timestamp = sentTimestamp,
serverTimestamp = sentTimestamp,
serverGuid = serviceGuid.toString(),
type = Envelope.Type.fromValue(outgoingPushMessage.type),
urgent = true,
content = encryptedContent.toByteString(),
sourceServiceIdBinary = aci.toByteString(),
destinationServiceIdBinary = to.aci.toByteString(),
serverGuidBinary = serviceGuid.toByteArray().toByteString()
)
}
fun encryptSealedSender(to: SignalClient): Envelope {
val sentTimestamp = System.currentTimeMillis()
val content = Content(
dataMessage = DataMessage(
body = "Test Message",
timestamp = sentTimestamp
)
)
val outgoingPushMessage: OutgoingPushMessage = cipher.encrypt(
SignalProtocolAddress(to.aci.toString(), 1),
SealedSenderAccess.forIndividual(UnidentifiedAccess(to.unidentifiedAccessKey, senderCertificate.serialized, false)),
EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty())
)
val encryptedContent: ByteArray = Base64.decode(outgoingPushMessage.content)
val serverGuid = UUID.randomUUID()
return Envelope(
sourceServiceId = aci.toString(),
sourceDevice = 1,
destinationServiceId = to.aci.toString(),
timestamp = sentTimestamp,
serverTimestamp = sentTimestamp,
serverGuid = serverGuid.toString(),
type = Envelope.Type.fromValue(outgoingPushMessage.type),
urgent = true,
content = encryptedContent.toByteString(),
sourceServiceIdBinary = aci.toByteString(),
destinationServiceIdBinary = to.aci.toByteString(),
serverGuidBinary = serverGuid.toByteArray().toByteString()
)
}
fun multiEncryptSealedSender(distributionId: DistributionId, others: List<SignalClient>, groupId: Optional<ByteArray>): ByteArray {
val sentTimestamp = System.currentTimeMillis()
val content = Content(
dataMessage = DataMessage(
body = "Test Message",
timestamp = sentTimestamp
)
)
val destinations = others.map { bob ->
SignalProtocolAddress(bob.aci.toString(), 1)
}
return cipher.encryptForGroup(distributionId, destinations, null, senderCertificate, content.encode(), ContentHint.DEFAULT, groupId)
}
fun decryptMessage(envelope: Envelope) {
cipher.decrypt(envelope, System.currentTimeMillis())
}
private fun createPreKeyBundle(): PreKeyBundle {
val prekeyId = prekeyIndex++
val preKeyRecord = PreKeyRecord(prekeyId, ECKeyPair.generate())
val signedPreKeyPair = ECKeyPair.generate()
val signedPreKeySignature = store.identityKeyPair.privateKey.calculateSignature(signedPreKeyPair.publicKey.serialize())
val kyerPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024)
store.storePreKey(prekeyId, preKeyRecord)
store.storeSignedPreKey(prekeyId, SignedPreKeyRecord(prekeyId, System.currentTimeMillis(), signedPreKeyPair, signedPreKeySignature))
return PreKeyBundle(
prekeyId, prekeyId, prekeyId, preKeyRecord.keyPair.publicKey, prekeyId, signedPreKeyPair.publicKey, signedPreKeySignature, store.identityKeyPair.publicKey,
PreKeyBundle.NULL_PRE_KEY_ID, kyerPair.publicKey, kyerPair.secretKey.serialize()
)
}
}
private fun createCertificateFor(trustRoot: ECKeyPair, uuid: UUID, e164: String, deviceId: Int, identityKey: ECPublicKey, expires: Long): SenderCertificate {
val serverKey: ECKeyPair = ECKeyPair.generate()
val serverCertificate = ServerCertificate(trustRoot.privateKey, 1, serverKey.publicKey)
return serverCertificate.issue(serverKey.privateKey, uuid.toString(), Optional.of(e164), deviceId, identityKey, expires)
}
private class TestSessionLock : SignalSessionLock {
val lock = ReentrantLock()
override fun acquire(): SignalSessionLock.Lock {
lock.lock()
return SignalSessionLock.Lock { lock.unlock() }
}
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />