diff --git a/.gitignore b/.gitignore index 42afabf..dde283f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,38 @@ -/build \ No newline at end of file +*.iml +.gradle +/local.properties +/.idea/* +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +double_ratchet/build/* +double_ratchet/build/ +double_ratchet/build +/captures +.externalNativeBuild +.cxx +*.aab +*.apk +local.properties +keystore.properties +*.jks +ks.passwd +venv/* +/release.properties +*.sw* +gradle.properties +*.tmp.sh +*.logs +/double_ratchet/.idea/.gitignore +/double_ratchet/.ideadouble_ratchetInsightsSettings.xml +/double_ratchet/.idea/caches/deviceStreaming.xml +/double_ratchet/.idea/gradle.xml +/double_ratchet/.idea/migrations.xml +/double_ratchet/.idea/misc.xml +/double_ratchet/.idea/runConfigurations.xml +/double_ratchet/.idea/vcs.xml \ No newline at end of file diff --git a/build.gradle b/build.gradle index 607d7c6..9e0ca23 100644 --- a/build.gradle +++ b/build.gradle @@ -1,84 +1,31 @@ -plugins { - id 'com.android.library' -// id 'maven-publish' - id 'signing' - id 'org.jetbrains.kotlin.android' - id "com.vanniktech.maven.publish" version "0.34.0" - id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.10' -} - -android { - namespace 'com.afkanerd.smswithoutborders.libsignal_doubleratchet' - compileSdk 36 - - defaultConfig { - minSdk 24 - targetSdk 36 - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - aarMetadata { - minCompileSdk = 24 - } +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext { +// kotlin_version = '1.8.20-RC' +// kotlin_version = '1.9.23' + kotlin_version = '2.3.10' + agp_version = '8.5.0' + agp_version1 = '8.12.2' } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - nightly { - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + repositories { + google() + mavenCentral() } + dependencies { + classpath "com.android.tools.build:gradle:$agp_version1" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - testFixtures { - enable = true - } - - kotlinOptions { - jvmTarget = '17' + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files } } -import com.vanniktech.maven.publish.AndroidSingleVariantLibrary -mavenPublishing { - // the first parameter represennts which variant is published - // the second whether to publish a sources jar - // the third whether to publish a javadoc jar - configure(new AndroidSingleVariantLibrary("release", true, true)) +plugins { + // Existing plugins +// alias(libs.plugins.compose.compiler) apply false +// alias(libs.plugins.org.jetbrains.kotlin.android) apply false } - - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar', "*.aar"]) - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'com.google.guava:guava:33.4.8-jre' - implementation 'com.madgag.spongycastle:prov:1.58.0.0' - implementation 'org.conscrypt:conscrypt-android:2.5.3' - implementation 'androidx.core:core-ktx:1.13.1' - - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' - - implementation 'com.github.netricecake:x25519:2.0' - implementation 'com.google.code.gson:gson:2.11.0' - implementation 'at.favre.lib:hkdf:2.0.0' - - implementation "androidx.datastore:datastore-preferences:1.1.7" - - // optional - RxJava2 support - implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" - - // optional - RxJava3 support - implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" - - implementation "androidx.datastore:datastore-preferences-core:1.1.7" - - implementation 'com.google.code.gson:gson:2.11.0' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" -} +tasks.register('clean', Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/double_ratchet/build.gradle b/double_ratchet/build.gradle new file mode 100644 index 0000000..aedd88b --- /dev/null +++ b/double_ratchet/build.gradle @@ -0,0 +1,73 @@ +plugins { + id 'com.android.library' +// id 'maven-publish' + id 'signing' + id 'org.jetbrains.kotlin.android' + id "com.vanniktech.maven.publish" version "0.34.0" + id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.10' +} + +android { + namespace 'com.afkanerd.smswithoutborders.libsignal_doubleratchet' + compileSdk 36 + + defaultConfig { + minSdk 24 + targetSdk 36 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aarMetadata { + minCompileSdk = 24 + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + nightly { + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + testFixtures { + enable = true + } + + kotlinOptions { + jvmTarget = '17' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', "*.aar"]) + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.guava:guava:33.5.0-jre' + implementation 'org.conscrypt:conscrypt-android:2.5.3' + implementation 'androidx.core:core-ktx:1.18.0' + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' + + implementation 'com.google.code.gson:gson:2.13.2' + implementation 'at.favre.lib:hkdf:2.0.0' + + implementation "androidx.datastore:datastore-preferences:1.2.1" + + // optional - RxJava2 support + implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1" + + // optional - RxJava3 support + implementation "androidx.datastore:datastore-preferences-rxjava3:1.2.1" + + implementation "androidx.datastore:datastore-preferences-core:1.2.1" + + implementation 'com.google.code.gson:gson:2.13.2' + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0" + implementation("org.bouncycastle:bcprov-jdk18on:1.83") +} diff --git a/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/PoCTest.kt b/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/PoCTest.kt new file mode 100644 index 0000000..ba88ab8 --- /dev/null +++ b/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/PoCTest.kt @@ -0,0 +1,28 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import android.content.Context +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import org.bouncycastle.crypto.params.X25519PublicKeyParameters +import org.junit.Assert.assertArrayEquals +import org.junit.Test + +@SmallTest +class PoCTest { + + var context: Context = + InstrumentationRegistry.getInstrumentation().targetContext + val protocol = Protocols(context) + + @Test + fun zeroing() { + val keypair = protocol.generateDH() + val publicKey = X25519PublicKeyParameters(keypair.publicKey) + + publicKey.encoded.fill(0) + val expected = ByteArray(32) + assertArrayEquals(expected, publicKey.encoded) + } +} \ No newline at end of file diff --git a/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsTest.kt b/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsTest.kt new file mode 100644 index 0000000..9328291 --- /dev/null +++ b/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsTest.kt @@ -0,0 +1,230 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import android.content.Context +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoUtils +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoUtils.sha256 +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.Cryptography +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.extensions.generateRandomBytes +import org.bouncycastle.crypto.params.X25519PublicKeyParameters +import org.junit.Assert.assertArrayEquals +import org.junit.Before +import org.junit.Test +import java.security.SecureRandom + +@SmallTest +class RatchetsTest { + var context: Context = + InstrumentationRegistry.getInstrumentation().targetContext + val protocol = Protocols(context) + + + val salt = "completeRatchetTest_v1".encodeToByteArray() + + @Test + fun completeRatchetTest() { + val aliceKeypair = protocol.generateDH() + val bobStaticKeypair = protocol.generateDH() + val bobKeypair = protocol.generateDH() +// val info = context.generateRandomBytes(16) + +// aliceKeypair.publicKey + +// bobKeypair.publicKey + +// bobStaticKeypair.publicKey + val info = ByteArray(32) + + val ad = "RatchetsTest".encodeToByteArray().sha256() + val originalText = SecureRandom.getSeed(32); + var ratchetPayload: RatchetPayload? + + aliceKeypair.use { aliceKeypair -> + bobKeypair.use { bobKeypair -> + + val alicePublicKey = aliceKeypair.publicKey.copyOf() + val bobPublicKey = bobKeypair.publicKey.copyOf() + val authenticationPublicKey = bobStaticKeypair.publicKey.copyOf() + + val aliceKey = Cryptography.generateKeysNK( + context = context, + ephemeralKeyPair = aliceKeypair, + authenticationPublicKey = authenticationPublicKey, + ephemeralPublicKey = bobPublicKey, + salt = salt, + info = info + ) + + val bob = Cryptography.generateKeysNKServer( + context = context, + authenticationKeypair = bobStaticKeypair, + ephemeralKeyPair = bobKeypair, + ephemeralPublicKey = alicePublicKey, + salt = salt, + info = info + ) + + + val ratchets = RatchetsHE(context) + + aliceKey.use { alice -> + assertArrayEquals(alice.rk, bob.first) + assertArrayEquals(alice.hk, bob.second) + assertArrayEquals(alice.nhk, bob.third) + + val aliceState = States() + aliceState.use { aliceState -> + ratchets.ratchetInitAlice( + state = aliceState, + sk = alice.rk, + bobDhPublicKey = bobPublicKey.copyOf(), + sharedHka = alice.hk, + sharedNHka = alice.nhk + ) + + ratchetPayload = ratchets.ratchetEncrypt( + state = aliceState, + plaintext = originalText, + ad = ad + ) + } + } + + + val bobState = States() + bobState.use { bobState -> + ratchets.ratchetInitBob( + state = bobState, + sk = bob.first, + bobKeypair = bobKeypair, + sharedHka = bob.second, + sharedNHka = bob.third + ) + val plaintext = ratchets.ratchetDecrypt( + state = bobState, + encHeader = ratchetPayload!!.header, + cipherText = ratchetPayload.cipherText, + ad = ad + ) + assertArrayEquals(originalText, plaintext) + } + } + } + } + + @Test + fun completeRatchetOutOfOrderTest() { + val aliceKeypair = protocol.generateDH() + val bobStaticKeypair = protocol.generateDH() + val bobKeypair = protocol.generateDH() + val info = context.generateRandomBytes(16) + + aliceKeypair.publicKey + + bobKeypair.publicKey + + bobStaticKeypair.publicKey + Cryptography.generateKeysNK( + context = context, + ephemeralKeyPair = aliceKeypair, + authenticationPublicKey = bobStaticKeypair.publicKey, + ephemeralPublicKey = bobKeypair.publicKey, + salt = salt, + info = info + ).use { alice -> + Cryptography.generateKeysNKServer( + context = context, + authenticationKeypair = bobStaticKeypair, + ephemeralKeyPair = bobKeypair, + ephemeralPublicKey = aliceKeypair.publicKey, + salt = salt, + info = info + ).let { bob -> + assertArrayEquals(alice.rk, bob.first) + assertArrayEquals(alice.hk, bob.second) + assertArrayEquals(alice.nhk, bob.third) + + val ratchets = RatchetsHE(context) + val aliceState = States() + ratchets.ratchetInitAlice( + state = aliceState, + sk = alice.rk, + bobDhPublicKey = bobKeypair.publicKey, + sharedHka = alice.hk, + sharedNHka = alice.nhk + ) + + val bobState = States() + ratchets.ratchetInitBob( + state = bobState, + sk = bob.first, + bobKeypair = bobKeypair, + sharedHka = bob.second, + sharedNHka = bob.third + ) + + val originalText = SecureRandom.getSeed(32); + + val ad = "RatchetsTest".encodeToByteArray().sha256() + var ratchetPayload = ratchets.ratchetEncrypt( + state = aliceState, + plaintext = originalText, + ad = ad + ) + + var plaintext = ratchets.ratchetDecrypt( + state = bobState, + encHeader = ratchetPayload.header, + cipherText = ratchetPayload.cipherText, + ad = ad + ) + + assertArrayEquals(originalText, plaintext) + + ratchetPayload = ratchets.ratchetEncrypt( + state = bobState, + plaintext = originalText, + ad = ad + ) + + plaintext = ratchets.ratchetDecrypt( + state = aliceState, + encHeader = ratchetPayload.header, + cipherText = ratchetPayload.cipherText, + ad = ad + ) + + assertArrayEquals(originalText, plaintext) + + for(i in 1..5) { + ratchetPayload = ratchets.ratchetEncrypt( + state = aliceState, + plaintext = originalText, + ad = ad + ) + } + + plaintext = ratchets.ratchetDecrypt( + state = bobState, + encHeader = ratchetPayload.header, + cipherText = ratchetPayload.cipherText, + ad = ad + ) + + assertArrayEquals(originalText, plaintext) + + ratchetPayload = ratchets.ratchetEncrypt( + state = bobState, + plaintext = originalText, + ad = ad + ) + + plaintext = ratchets.ratchetDecrypt( + state = aliceState, + encHeader = ratchetPayload.header, + cipherText = ratchetPayload.cipherText, + ad = ad + ) + + assertArrayEquals(originalText, plaintext) + } + + } + } +} + diff --git a/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/StateTest.kt b/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/StateTest.kt new file mode 100644 index 0000000..addb3f4 --- /dev/null +++ b/double_ratchet/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/StateTest.kt @@ -0,0 +1,19 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import android.util.Pair +import androidx.test.filters.SmallTest +import junit.framework.TestCase.assertEquals +import kotlinx.serialization.json.Json +import org.junit.Test +import java.security.SecureRandom + +@SmallTest +class StateTest { + + @Test fun testStates() { + val state = States() + val serializedStates = Json.encodeToString(state) + val deserializedStates = Json.decodeFromString(serializedStates) + assertEquals(state, deserializedStates) + } +} \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/double_ratchet/src/main/AndroidManifest.xml similarity index 100% rename from src/main/AndroidManifest.xml rename to double_ratchet/src/main/AndroidManifest.xml diff --git a/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoUtils.kt b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoUtils.kt new file mode 100644 index 0000000..eb4ddf8 --- /dev/null +++ b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoUtils.kt @@ -0,0 +1,51 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet + +import android.content.Context +import at.favre.lib.hkdf.HKDF +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols +import com.google.common.primitives.Bytes +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.EphemeralKeyPair +import org.bouncycastle.crypto.params.X25519PublicKeyParameters +import java.security.GeneralSecurityException +import java.security.MessageDigest +import java.security.PublicKey +import java.security.SecureRandom +import javax.crypto.Mac +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +object CryptoUtils { + fun hkdf( + ikm: ByteArray, + salt: ByteArray?, + info: ByteArray?, + len: Int, + ): ByteArray { + return HKDF.fromHmacSha512() + .extractAndExpand( + salt, + ikm, + info, + len + ) + } + + fun hmac(data: ByteArray?): Mac { + val algorithm = "HmacSHA512" + val output = Mac.getInstance(algorithm) + val key: SecretKey = SecretKeySpec(data, algorithm) + output.init(key) + return output + } + + fun ByteArray.sha256(): ByteArray { + return MessageDigest + .getInstance("SHA-256") + .digest(this) + } + + + +} diff --git a/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/Cryptography.kt b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/Cryptography.kt new file mode 100644 index 0000000..b31b80b --- /dev/null +++ b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/Cryptography.kt @@ -0,0 +1,411 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet + +import android.content.Context +import androidx.datastore.core.Closeable +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoUtils.hkdf +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoUtils.sha256 +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.params.X25519PublicKeyParameters +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec +import java.security.SecureRandom +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object Cryptography { + + data class NoiseNKKeys( + val rk: ByteArray, + val hk: ByteArray, + val nhk: ByteArray, + ): AutoCloseable { + private var zeroed = false + + override fun close() { + if(!zeroed) { + rk.fill(0) + hk.fill(0) + nhk.fill(0) + zeroed = true + } + } + + // Prevent accidental logging/serialization of key material + override fun toString() = "NoiseNKKeys([REDACTED])" + } + + fun generateKeysNK( + context: Context, + ephemeralKeyPair: Protocols.CloseableCurve15519KeyPair, + authenticationPublicKey: ByteArray, + ephemeralPublicKey: ByteArray, + salt: ByteArray, + info: ByteArray, + ): NoiseNKKeys { + val protocols = Protocols(context) + + var dh1: ByteArray? = null + var dh2: ByteArray? = null + + ephemeralKeyPair.use { ekp -> + dh1 = protocols.dh(ekp.privateKey!!, authenticationPublicKey) + dh2 = protocols.dh(ekp.privateKey!!, ephemeralPublicKey) + } + + var hkdf1: ByteArray? = null + var hkdf2: ByteArray? = null + + try { + hkdf1 = hkdf(ikm = dh1!!, salt = salt, info = info, len = 32) + hkdf2 = hkdf(ikm = dh2!!, salt = hkdf1, info = info, len = 96) + + return NoiseNKKeys( + hkdf2.sliceArray(0 until 32), + hkdf2.sliceArray(32 until 64), + hkdf2.sliceArray(64 until 96), + ) + } finally { + dh1?.fill(0) + dh2?.fill(0) + hkdf1?.fill(0) + hkdf2?.fill(0) + // The sliceArray copies inside Triple are intentionally not zeroed — + // they are the return value and owned by the caller + } + } + + /** + * This exists only for test purposes, do not use for Production builds + */ + fun generateKeysNKServer( + context: Context, + authenticationKeypair: Protocols.CloseableCurve15519KeyPair, + ephemeralKeyPair: Protocols.CloseableCurve15519KeyPair, + ephemeralPublicKey: ByteArray, + salt: ByteArray, + info: ByteArray, + ): Triple { + val protocols = Protocols(context) + val dh1 = protocols.dh(authenticationKeypair.privateKey!!, ephemeralPublicKey) + val dh2 = protocols.dh(ephemeralKeyPair.privateKey!!, ephemeralPublicKey) + + var hkdf1: ByteArray? = null + var hkdf2: ByteArray? = null + + try { + hkdf1 = hkdf( ikm = dh1, salt = salt, info = info, len = 32, ) + hkdf2 = hkdf( ikm = dh2, salt = hkdf1, info = info, len = 96, ) + + return Triple( + hkdf2.sliceArray(0 until 32), + hkdf2.sliceArray(32 until 64), + hkdf2.sliceArray(64 until 96), + ) + } finally { + dh1.fill(0) + dh2.fill(0) + hkdf1?.fill(0) + hkdf2?.fill(0) + } + } + + data class NoiseIKKeys( + val rk: ByteArray, + val hk: ByteArray, + val nhk: ByteArray, + val ck: ByteArray? = null, + val h: ByteArray? = null, + ): AutoCloseable { + private var zeroed = false + + override fun close() { + if(!zeroed) { + rk.fill(0) + hk.fill(0) + nhk.fill(0) + ck?.fill(0) + h?.fill(0) + zeroed = true + } + } + + // Prevent accidental logging/serialization of key material + override fun toString() = "NoiseIKKeys([REDACTED])" + } + + fun generateKeysIK( + context: Context, + ephemeralKeyPair: Protocols.CloseableCurve15519KeyPair, + authenticationPublicKey: ByteArray, + staticKeyPair: Protocols.CloseableCurve15519KeyPair, + info: ByteArray, + headerInfo: ByteArray, + ) : NoiseIKKeys { + val protocols = Protocols(context) + + var h = "Noise_IK_25519_AESGCM_SHA256".encodeToByteArray().sha256() + var ck = h + + h = (h + authenticationPublicKey).sha256() + h = (h + ephemeralKeyPair.publicKey).sha256() + + var dhEs: ByteArray? = null + var dhSs: ByteArray? = null + + ephemeralKeyPair.use { ekp -> + staticKeyPair.use { skp -> + dhEs = protocols.dh(ekp.privateKey!!, authenticationPublicKey) + dhSs = protocols.dh(skp.privateKey!!, authenticationPublicKey) + } + } + + // Named references so we can zero them + var hkdf1: ByteArray? = null + var hkdf2: ByteArray? = null + var hkdf3: ByteArray? = null + var k: ByteArray? = null + var csPkEnc: ByteArray? = null + var ciphertext: ByteArray? = null + + try { + hkdf1 = hkdf(ikm = dhEs!!, salt = ck, info = info, len = 2) + ck = hkdf1.sliceArray(0 until 32) + k = hkdf1.sliceArray(32 until 64) + + csPkEnc = AesGcm.encrypt( + SecretKeySpec(k, "AES"), + staticKeyPair.publicKey, + h + ) + h = (h + csPkEnc).sha256() + + hkdf2 = hkdf(ikm = dhSs!!, salt = ck, info = info, len = 2) + ck = hkdf2.sliceArray(0 until 32) + k.fill(0) // zero previous k before reassigning + k = hkdf2.sliceArray(32 until 64) + + ciphertext = Cryptography.AesGcm.encrypt( + SecretKeySpec(k, "AES"), + "".encodeToByteArray(), + h + ) + h = (h + ciphertext).sha256() + + hkdf3 = hkdf(ikm = dhSs, salt = ck, info = headerInfo, len = 3) + + return NoiseIKKeys( + hkdf3.sliceArray(0 until 32), + hkdf3.sliceArray(32 until 64), + hkdf3.sliceArray(64 until 96), + ck, + h + ) + } finally { + // Zero everything sensitive regardless of success or exception + dhEs?.fill(0) + dhSs?.fill(0) + ck.fill(0) + k?.fill(0) + hkdf1?.fill(0) + hkdf2?.fill(0) + hkdf3?.fill(0) + // csPkEnc and ciphertext are non-secret ciphertext, but zero anyway + csPkEnc?.fill(0) + ciphertext?.fill(0) + } + } + + fun generateKeysIKForwardSecrecy( + context: Context, + h: ByteArray, + ck: ByteArray, + ephemeralKeyPair: Protocols.CloseableCurve15519KeyPair, + ephemeralResponderPublicKey: ByteArray, + authenticationPublicKey: ByteArray, + info: ByteArray, + headerInfo: ByteArray, + ) : NoiseIKKeys { + val protocols = Protocols(context) + + // Shadowed vars — use local mutable copies so we can zero them + // Note: the incoming h and ck are owned by the caller; don't zero them here + var localH = h + ephemeralResponderPublicKey.sha256() + var localCk = ck.copyOf() // defensive copy — we'll mutate and zero this + + var dhEe: ByteArray? = null + var dhSe: ByteArray? = null + ephemeralKeyPair.use { ekp -> + dhEe = protocols.dh(ekp.privateKey!!, ephemeralResponderPublicKey) + dhSe = protocols.dh(ekp.privateKey!!, authenticationPublicKey) + } + + var hkdf1: ByteArray? = null + var hkdf2: ByteArray? = null + var hkdf3: ByteArray? = null + var k: ByteArray? = null + var ciphertext1: ByteArray? = null + var ciphertext2: ByteArray? = null + + try { + hkdf1 = hkdf(ikm = dhEe!!, salt = localCk, info = info, len = 2) + localCk.fill(0) + localCk = hkdf1.sliceArray(0 until 32) + k = hkdf1.sliceArray(32 until 64) + + ciphertext1 = AesGcm.encrypt( + SecretKeySpec(k, "AES"), + "".encodeToByteArray(), + localH + ) + localH = (localH + ciphertext1).sha256() + + hkdf2 = hkdf(ikm = dhSe!!, salt = localCk, info = info, len = 2) + localCk.fill(0) + localCk = hkdf2.sliceArray(0 until 32) + k.fill(0) // zero previous k before reassign + k = hkdf2.sliceArray(32 until 64) + + ciphertext2 = AesGcm.encrypt( + SecretKeySpec(k, "AES"), + "".encodeToByteArray(), + localH + ) + localH = (localH + ciphertext2).sha256() + + hkdf3 = hkdf( + ikm = "".encodeToByteArray(), + salt = localCk, + info = headerInfo, + len = 3 + ) + + return NoiseIKKeys( + hkdf3.sliceArray(0 until 32), + hkdf3.sliceArray(32 until 64), + hkdf3.sliceArray(64 until 96), + h = localH + ) + } finally { + dhEe?.fill(0) + dhSe?.fill(0) + localCk.fill(0) + k?.fill(0) + hkdf1?.fill(0) + hkdf2?.fill(0) + hkdf3?.fill(0) + ciphertext1?.fill(0) + ciphertext2?.fill(0) + // Do NOT zero localH — it's returned inside NoiseIKKey + // Do NOT zero the caller's h and ck — we don't own them + } + } + + object AesGcm { + private const val ALGORITHM = "AES/GCM/NoPadding" + private const val KEY_SIZE_BITS = 256 + private const val IV_SIZE_BYTES = 12 // 96-bit IV recommended for GCM + private const val TAG_SIZE_BITS = 128 // authentication tag length + + data class CipherResult( + val ciphertext: ByteArray, // encrypted data (includes appended GCM auth tag) + val iv: ByteArray // IV — must be stored alongside ciphertext for decryption + ) + + + /** + * Encrypts [plaintext] with AES-256-GCM. + * + * @param key AES secret key (128, 192, or 256-bit) + * @param plaintext Data to encrypt + * @param associatedData AAD: authenticated but NOT encrypted (e.g. headers, context). + * Pass null if not needed. + * @return CipherResult containing the ciphertext+tag and the IV used. + */ + fun encrypt( + key: SecretKey, + plaintext: ByteArray, + iv: ByteArray? = null, + associatedData: ByteArray? = null + ): ByteArray { + val iv1 = iv ?: ByteArray(IV_SIZE_BYTES).also { SecureRandom().nextBytes(it) } + val spec = GCMParameterSpec(TAG_SIZE_BITS, iv1) + + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.ENCRYPT_MODE, key, spec) + associatedData?.let { cipher.updateAAD(it) } + + val ciphertext = cipher.doFinal(plaintext) + return if(iv != null) ciphertext else iv1 + ciphertext + } + + /** + * Decrypts and authenticates output from [encrypt]. + * Throws [javax.crypto.AEADBadTagException] if the tag or AAD doesn't match. + * + * @param key Same AES key used during encryption + * @param ciphertext Encrypted bytes (ciphertext + appended GCM tag) + * @param iv IV from the corresponding [CipherResult] + * @param associatedData Must be identical to the AAD used during encryption + */ + fun decrypt( + key: SecretKey, + ciphertext: ByteArray, + iv: ByteArray, + associatedData: ByteArray? = null + ): ByteArray { + val spec = GCMParameterSpec(TAG_SIZE_BITS, iv) + + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.DECRYPT_MODE, key, spec) + associatedData?.let { cipher.updateAAD(it) } + + return try { + cipher.doFinal(ciphertext) + } catch (e: Exception) { + e.printStackTrace() + throw e + } + } + } + + object AesCbc { + private const val ALGORITHM = "AES/CBC/PKCS5Padding" + private const val IV_SIZE = 16 // AES block size is always 16 bytes + + fun encrypt(key: ByteArray, plaintext: ByteArray, iv: ByteArray? = null): ByteArray { + val cipher = Cipher.getInstance(ALGORITHM) + + val finalIv = iv ?: ByteArray(IV_SIZE).apply { SecureRandom().nextBytes(this) } + val keySpec = SecretKeySpec(key, "AES") + val ivSpec = IvParameterSpec(finalIv) + + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) + val ciphertext = cipher.doFinal(plaintext) + + return if(iv == null) { + finalIv + ciphertext + } else { + ciphertext + } + } + + fun decrypt( + key: ByteArray, + ciphertext: ByteArray, + iv: ByteArray + ): ByteArray { + val cipher = Cipher.getInstance(ALGORITHM) + val keySpec = SecretKeySpec(key, "AES") + + val ivSpec = IvParameterSpec(iv) + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) + + val plaintext = cipher.doFinal(ciphertext) + return plaintext + } + } +} \ No newline at end of file diff --git a/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/extensions/context.kt b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/extensions/context.kt new file mode 100644 index 0000000..27c1a10 --- /dev/null +++ b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/extensions/context.kt @@ -0,0 +1,62 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.extensions + +import android.content.Context +import android.util.Base64 +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.core.stringSetPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.google.gson.Gson +import kotlinx.coroutines.flow.first +import java.io.IOException +import java.security.KeyPair +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import java.security.UnrecoverableEntryException +import java.security.cert.CertificateException +import javax.crypto.spec.SecretKeySpec + +val Context.dataStore: DataStore by preferencesDataStore(name = "secure_comms") + +@Throws( + KeyStoreException::class, + CertificateException::class, + IOException::class, + NoSuchAlgorithmException::class, + UnrecoverableEntryException::class +) +fun Context.getKeypairFromKeystore(keystoreAlias: String): KeyPair? { + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + + val entry = keyStore.getEntry(keystoreAlias, null) + if (entry is KeyStore.PrivateKeyEntry) { + val privateKey = entry.privateKey + val publicKey = keyStore.getCertificate(keystoreAlias).publicKey + return KeyPair(publicKey, privateKey) + } + return null +} + +data class SavedBinaryData( + val key: ByteArray, + val algorithm: String, + val data: ByteArray, +) + +/** + * Would overwrite anything with the same Keystore Alias + */ + +fun Context.generateRandomBytes(length: Int): ByteArray { + val random = SecureRandom() + val bytes = ByteArray(length) + random.nextBytes(bytes) + return bytes +} + diff --git a/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Headers.kt b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Headers.kt new file mode 100644 index 0000000..1dbc230 --- /dev/null +++ b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Headers.kt @@ -0,0 +1,36 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import com.google.common.primitives.Bytes +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.crypto.params.X25519PublicKeyParameters +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.KeyPair + +class Headers(var dh: Protocols.CloseableCurve15519KeyPair, pn: UByte, n: UByte) { + var pn: UByte = 0u + var n: UByte = 0u + + init { + this.pn = pn + this.n = n + } + + val serialized: ByteArray + get() { + return byteArrayOf(pn.toByte()) + byteArrayOf(n.toByte()) + dh.publicKey + } + + companion object { + fun deserialize(header: ByteArray): Headers { + val pn = header[0].toUByte() + val n = header[1].toUByte() + val pk = header.sliceArray(2 until header.size) + return Headers( + Protocols.CloseableCurve15519KeyPair( + pk, + null + ), pn, n) + } + } +} diff --git a/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Protocols.kt b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Protocols.kt new file mode 100644 index 0000000..c49d5ec --- /dev/null +++ b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Protocols.kt @@ -0,0 +1,272 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import android.content.Context +import android.util.Pair +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoUtils +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoUtils.hkdf +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoUtils.hmac +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.Cryptography +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.R +import com.google.common.primitives.Bytes +import kotlinx.serialization.Serializable +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.agreement.X25519Agreement +import org.bouncycastle.crypto.generators.X25519KeyPairGenerator +import org.bouncycastle.crypto.params.X25519KeyGenerationParameters +import org.bouncycastle.crypto.params.X25519PrivateKeyParameters +import org.bouncycastle.crypto.params.X25519PublicKeyParameters +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.lang.AutoCloseable +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.Security +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +/** + * These implementations are based on the signal protocols specifications. + * + * These are based on the recommended algorithms and parameters for the encryption + * and decryption. + * + * The goal for this would be to transform it into library which can be used across + * other SMS projects. + * + * [...](https://signal.org/docs/specifications/doubleratchet/) + */ +open class Protocols(private val context: Context) { + + private val MAC_LEN = 64 + + init { + Security.removeProvider("BC") + Security.addProvider(BouncyCastleProvider()) + } + + @Serializable + data class CloseableCurve15519KeyPair( + var publicKey: ByteArray, + var privateKey: ByteArray? + ): AutoCloseable { + private var isClosed = false + + override fun close() { + if(isClosed) return + publicKey.fill(0) + privateKey?.let{ it.fill(0); privateKey = null} + isClosed = true + } + + } + + fun generateDH(): CloseableCurve15519KeyPair { + val generator = X25519KeyPairGenerator() + generator.init(X25519KeyGenerationParameters(SecureRandom())) + + val keypair = generator.generateKeyPair() + return try { + CloseableCurve15519KeyPair( + publicKey = (keypair.public as X25519PublicKeyParameters).encoded, + privateKey = (keypair.private as X25519PrivateKeyParameters).encoded, + ) + } catch(e: Exception) { + e.printStackTrace() + (keypair.private as? X25519PrivateKeyParameters)?.encoded?.fill(0) + throw e + } + } + + fun dh(privateKey: ByteArray, publicKey: ByteArray): ByteArray { + val sharedSecret = ByteArray(32) + val agreement = X25519Agreement() + agreement.init(X25519PrivateKeyParameters(privateKey, 0)) + agreement.calculateAgreement( + X25519PublicKeyParameters(publicKey, 0), + sharedSecret, + 0 + ) + return sharedSecret + } + + fun kdfRk( + rk: ByteArray, + dhOut: ByteArray + ): Cryptography.NoiseNKKeys { + val info = context.getString(R.string.dr_rk_info).encodeToByteArray() + val hkdf = hkdf(dhOut, rk, info, 32*3) + val keys = Cryptography.NoiseNKKeys( + hkdf.sliceArray(0 until 32), + hkdf.sliceArray(32 until 64), + hkdf.sliceArray(64 until 96), + ) + hkdf.fill(0) + return keys + } + + fun kdfCk(ck: ByteArray?): Pair { + if(ck == null) throw Exception("CK came in null! Terminating") + + val mac = hmac(ck) + val newCk = mac.doFinal(byteArrayOf(0x01)) + val mk = mac.doFinal(byteArrayOf(0x02)) + return Pair(newCk, mk) + } + + fun encrypt( + mk: ByteArray, + plainText: ByteArray, + ad: ByteArray, + ): ByteArray { + val len = 76 + val hkdfOutput = hkdf( + ikm = mk, + salt = ByteArray(len), + info = context.getString(R.string.dr_encrypt_info).encodeToByteArray(), + len = len, + ) + try { + val key = hkdfOutput.sliceArray(0 until 32) + val authKey = hkdfOutput.sliceArray(32 until 64) + val iv = hkdfOutput.sliceArray(64 until 76) + + try { + val cipherText = Cryptography.AesGcm.encrypt( + key = SecretKeySpec(key, "AES"), + iv = iv, + plaintext = plainText, + ) + val mac = hmac(authKey) + mac.update(ad + cipherText) + return cipherText + mac.doFinal() + } finally { + key.fill(0) + iv.fill(0) + } + } finally { + hkdfOutput.fill(0) + } + } + + fun hEncrypt( + mk: ByteArray, + plainText: ByteArray, + ): ByteArray { + val len = 76 + val hkdfOutputs = hkdf( + ikm = mk, + salt = ByteArray(len), + info = context.getString(R.string.dr_encrypt_info).encodeToByteArray(), + len = len, + ) + + try { + val key = hkdfOutputs.sliceArray(0 until 32) + val authKey = hkdfOutputs.sliceArray(32 until 64) + val iv = hkdfOutputs.sliceArray(64 until 76) + + try { + val cipherText = Cryptography.AesGcm.encrypt( + key = SecretKeySpec(key, "AES"), + iv = iv, + plaintext = plainText, + ) + val mac = hmac(authKey) + mac.update(cipherText) + return cipherText + mac.doFinal() + } finally { + key.fill(0) + iv.fill(0) + } + } finally { + hkdfOutputs.fill(0) + } + } + + fun decrypt(mk: ByteArray, cipherText: ByteArray, ad: ByteArray): ByteArray { + val len = 76 + val hkdfOutput = hkdf( + ikm = mk, + salt = ByteArray(len), + info = context.getString(R.string.dr_encrypt_info).encodeToByteArray(), + len = len, + ) + + try { + val authKey = hkdfOutput.sliceArray(32 until 64) + val plaintextCiphertext = cipherText.dropLast(MAC_LEN).toByteArray() + + val mac = hmac(authKey) + mac.update(ad + plaintextCiphertext) + + val incomingMac = cipherText.takeLast(MAC_LEN).toByteArray() + if(!incomingMac.contentEquals(mac.doFinal())) { + throw Exception("Message failed authentication") + } + + val key = hkdfOutput.sliceArray(0 until 32) + val iv = hkdfOutput.sliceArray(64 until 76) + try { + return Cryptography.AesGcm.decrypt( + key = SecretKeySpec(key, "AES"), + ciphertext = plaintextCiphertext, + iv = iv, + ) + } finally { + key.fill(0) + iv.fill(0) + } + } finally { + hkdfOutput.fill(0) + } + } + + fun hDecrypt(mk: ByteArray?, cipherText: ByteArray): ByteArray? { + val len = 76 + if(mk == null) return null + + val hkdfOutputs = hkdf( + ikm = mk, + salt = ByteArray(len), + info = context.getString(R.string.dr_encrypt_info).encodeToByteArray(), + len = len, + ) + + try { + val authKey = hkdfOutputs.sliceArray(32 until 64) + val mac = hmac(authKey) + + val plainCiphertext = cipherText.dropLast(MAC_LEN).toByteArray() + mac.update(plainCiphertext) + + val incomingMac = cipherText.takeLast(MAC_LEN).toByteArray() + if(!incomingMac.contentEquals(mac.doFinal())) { + throw Exception("Message failed authentication") + } + + val key = hkdfOutputs.sliceArray(0 until 32) + val iv = hkdfOutputs.sliceArray(64 until 76) + return try { + Cryptography.AesGcm.decrypt( + key = SecretKeySpec(key, "AES"), + ciphertext = plainCiphertext, + iv = iv, + ) + } catch (e: Exception){ + e.fillInStackTrace() + throw e + } finally { + key.fill(0) + iv.fill(0) + authKey.fill(0) + } + } finally { + hkdfOutputs.fill(0) + } + } + + fun concat(ad: ByteArray, headers: ByteArray): ByteArray { + return ad + headers + } +} + diff --git a/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsHE.kt b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsHE.kt new file mode 100644 index 0000000..ce3380f --- /dev/null +++ b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsHE.kt @@ -0,0 +1,241 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import android.content.Context +import android.util.Pair +import androidx.core.util.component1 +import androidx.core.util.component2 +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.R +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.params.X25519PublicKeyParameters + +class RatchetPayload( + val header: ByteArray, + val cipherText: ByteArray, +) + +class RatchetsHE(context: Context) : Protocols(context){ + val MAX_SKIP = 255 + + /** + * @param state + * @param sk + * @param bobDhPublicKey + * @param sharedHka Alice's shared header key + * @param sharedNHka Alice's next shared header key + */ + fun ratchetInitAlice( + state: States, + sk: ByteArray, + bobDhPublicKey: ByteArray, + sharedHka: ByteArray, + sharedNHka: ByteArray, + ) { + state.DHRs = generateDH() + state.DHRr = bobDhPublicKey + + val keys = kdfRk( + rk = sk, dh( state.DHRs?.privateKey!!, state.DHRr!!) + ) + keys.use { k-> + state.RK = k.rk + state.CKs = k.hk + state.NHKs = k.nhk + } + + state.CKr = null + state.Ns = 0u + state.Nr = 0u + state.PN = 0u + state.MKSKIPPED = mutableMapOf() + state.HKs = sharedHka + state.HKr = null + state.NHKr = sharedNHka + } + + fun ratchetInitBob( + state: States, + sk: ByteArray, + bobKeypair: CloseableCurve15519KeyPair, + sharedHka: ByteArray, + sharedNHka: ByteArray, + ) { + state.DHRs = bobKeypair + state.DHRr = null + state.RK = sk + state.CKs = null + state.CKr = null + state.Ns = 0u + state.Nr = 0u + state.PN = 0u + state.MKSKIPPED = mutableMapOf() + state.HKs = null + state.NHKs = sharedNHka + state.HKr = null + state.NHKr = sharedHka + } + + fun ratchetEncrypt( + state: States, + plaintext: ByteArray, + ad: ByteArray, + ) : RatchetPayload { + val (ck, mk) = kdfCk(state.CKs) + try { + state.CKs = ck + val header = Headers(state.DHRs!!, state.PN, state.Ns) + val encHeader = hEncrypt(state.HKs!!, header.serialized) + state.Ns++ + return RatchetPayload( + header = encHeader, + cipherText = encrypt(mk, plaintext, concat(ad, encHeader)) + ) + } finally { + ck.fill(0) + mk.fill(0) + } + } + + fun ratchetDecrypt( + state: States, + encHeader: ByteArray, + cipherText: ByteArray, + ad: ByteArray, + ): ByteArray { + val plaintext = trySkippedMessageKeys(state, encHeader, cipherText, ad) + if(plaintext != null) + return plaintext + + val (header, dhRatchet) = decryptHeader(state, encHeader) + if(dhRatchet) { + skipMessageKeys(state, header.pn.toInt()) + dhRatchet(state, header) + } + + skipMessageKeys(state, header.n.toInt()) + + val (ck, mk) = kdfCk(state.CKr) + try { + state.CKr = ck + state.Nr++ + return decrypt(mk, cipherText, concat(ad, encHeader)) + } finally { + ck.fill(0) + mk.fill(0) + } + } + + private fun skipMessageKeys( + state: States, + until: Int, + ) { + if(state.Nr.toInt() + MAX_SKIP < until) + throw Exception("MAX SKIP Exceeded") + + state.CKr?.let{ + while(state.Nr.toInt() < until) { + val (ck, mk) = kdfCk(state.CKr) + try { + state.CKr = ck + val mk = mk + state.MKSKIPPED[MKSkippedPair(state.HKr, state.Nr.toInt())] = mk + state.Nr++ + } finally { + ck.fill(0) + mk.fill(0) + } + } + } + } + + private fun trySkippedMessageKeys( + state: States, + encHeader: ByteArray, + ciphertext: ByteArray, + ad: ByteArray + ) : ByteArray? { + state.MKSKIPPED.forEach { + val hk = it.key.key + val n = it.key.count + val mk = it.value + + try { + val header = hDecrypt(hk, encHeader)?.run { + Headers.deserialize(this) + } + if(header != null && header.n.toInt() == n) { + state.MKSKIPPED.remove(it.key) + return decrypt(mk, ciphertext, concat(ad, encHeader)) + } + } finally { + hk?.fill(0) + mk.fill(0) + } + } + + return null + } + + private fun decryptHeader( + state: States, + encHeader: ByteArray + ) : Pair { + var header: Headers? = null + try { + header = hDecrypt(state.HKr, encHeader)?.run { + Headers.deserialize(this) + } + } catch(e: Exception) { + e.printStackTrace() + } + + header?.let { + return Pair(header, false) + } + + val decryptedHeader = hDecrypt(state.NHKr!!, encHeader) + try { + if(decryptedHeader == null) throw Exception("Header is null") + header = Headers.deserialize(decryptedHeader) + } finally { + decryptedHeader?.fill(0) + } + + return Pair(header, true) + } + + private fun dhRatchet(state: States, header: Headers) { + state.PN = state.Ns + state.Ns = 0u + state.Nr = 0u + state.HKs = state.NHKs + state.HKr = state.NHKr + state.DHRr = header.dh.publicKey + + var keys = kdfRk(state.RK!!, + dh( + state.DHRs?.privateKey!!, + state.DHRr!!, + ) + ) + keys.use { k -> + state.RK = k.rk + state.CKr = k.hk + state.NHKr = k.nhk + } + + state.DHRs = generateDH() + + keys = kdfRk(state.RK!!, + dh( + state.DHRs?.privateKey!!, + state.DHRr!!, + ) + ) + keys.use { k-> + state.RK = k.rk + state.CKs = k.hk + state.NHKs = k.nhk + } + } +} \ No newline at end of file diff --git a/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.kt b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.kt new file mode 100644 index 0000000..5f0c780 --- /dev/null +++ b/double_ratchet/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.kt @@ -0,0 +1,84 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import android.R.id.input +import android.util.Pair +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import java.io.ByteArrayOutputStream +import java.lang.AutoCloseable +import java.security.KeyPair +import java.security.PrivateKey +import java.security.PublicKey + + +@Serializable +data class MKSkippedPair( + val key: ByteArray?, + val count: Int +) + +@Serializable +data class States( + var RK: ByteArray? = null, + var CKs: ByteArray? = null, + var CKr: ByteArray? = null, + var Ns: UByte = 0u, + var Nr: UByte = 0u, + var PN: UByte = 0u, + var DHRs: Protocols.CloseableCurve15519KeyPair? = null, + var DHRr: ByteArray? = null, + var HKs: ByteArray? = null, + var HKr: ByteArray? = null, + var NHKs: ByteArray? = null, + var NHKr: ByteArray? = null, + var MKSKIPPED: MutableMap = mutableMapOf() +) : AutoCloseable { + @OptIn(ExperimentalSerializationApi::class) + fun serialize(): ByteArray { + val outputBuffer = ByteArrayOutputStream() + Json.encodeToStream(this, outputBuffer) + return outputBuffer.toByteArray() + } + + private var isClosed = false + override fun close() { + if(isClosed) return + RK?.let { it.fill(0); RK = null } + CKs?.let { it.fill(0); CKs = null } + CKr?.let { it.fill(0); CKr = null } + HKs?.let { it.fill(0); HKs = null } + HKr?.let { it.fill(0); HKr = null } + NHKs?.let { it.fill(0); NHKs = null } + NHKr?.let { it.fill(0); NHKr = null } + + DHRr?.let { it.fill(0); DHRr = null } + + val iterator = MKSKIPPED.entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + entry.key.key?.fill(0) + entry.value.fill(0) + iterator.remove() + } + MKSKIPPED.clear() + + Ns = 0u + Nr = 0u + PN = 0u + isClosed = true + } + + companion object { + @OptIn(ExperimentalSerializationApi::class) + fun deserialize(data: ByteArray): States { + return Json.decodeFromStream(data.inputStream()) + } + } +} \ No newline at end of file diff --git a/double_ratchet/src/main/res/values/strings.xml b/double_ratchet/src/main/res/values/strings.xml new file mode 100644 index 0000000..cfccd9c --- /dev/null +++ b/double_ratchet/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + SMSWithoutBorders DoubleRatchet LibSignal + Missing public key + Ratchet states removed + RelaySMS v1 + RelaySMS C2S DR v1 + RelaySMS DRHE v2 + RelaySMS DR_ENCRYPTION v2 + RelaySMS_NK_handshake_v1 + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3deb2b0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +org.gradle.jvmargs=-Xmx4096M \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2a84e18 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..ef07e01 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6e8cd15 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,13 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { setUrl("https://jitpack.io") } + } +} +rootProject.name = "lib_signal_double_ratchet_java" +include ':double_ratchet' \ No newline at end of file diff --git a/src/androidTest/java/com/afkanerd/.DS_Store b/src/androidTest/java/com/afkanerd/.DS_Store new file mode 100644 index 0000000..e4cabc0 Binary files /dev/null and b/src/androidTest/java/com/afkanerd/.DS_Store differ diff --git a/src/androidTest/java/com/afkanerd/smswithoutborders/.DS_Store b/src/androidTest/java/com/afkanerd/smswithoutborders/.DS_Store new file mode 100644 index 0000000..68565d0 Binary files /dev/null and b/src/androidTest/java/com/afkanerd/smswithoutborders/.DS_Store differ diff --git a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityAESTest.kt b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityAESTest.kt deleted file mode 100644 index 82f93d4..0000000 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityAESTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet - -import androidx.test.filters.SmallTest -import org.junit.Assert.assertArrayEquals -import org.junit.Test -import javax.crypto.SecretKey - -@SmallTest -class SecurityAESTest { - - @Test - fun aesTest() { - val secretKey = SecurityAES.generateSecretKey(256) - - val input = CryptoHelpers.generateRandomBytes(277) - val cipher = SecurityAES.encryptAES256CBC(input, secretKey.encoded, null) - val output = SecurityAES.decryptAES256CBC(cipher, secretKey.encoded, null) - - assertArrayEquals(input, output) - } -} \ No newline at end of file diff --git a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSATest.java b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSATest.java deleted file mode 100644 index ec9ebd2..0000000 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSATest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet; - - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PublicKey; -import java.security.UnrecoverableEntryException; -import java.security.cert.CertificateException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; - -@RunWith(AndroidJUnit4.class) -public class SecurityRSATest { - - String keystoreAlias = "keystoreAlias"; - @Test - public void testCanStoreAndEncrypt() throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, UnrecoverableEntryException, CertificateException, KeyStoreException, IOException { -// KeyPairGenerator kpg = KeyPairGenerator.getInstance( -// KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); -// -// kpg.initialize(new KeyGenParameterSpec.Builder(keystoreAlias, -// KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) -// .setKeySize(2048) -// .setDigests(KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256, -// KeyProperties.DIGEST_SHA512) -// .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) -// .build()); -// -// KeyPair keyPair = kpg.generateKeyPair(); - PublicKey publicKey = SecurityRSA.generateKeyPair(keystoreAlias, 2048); - KeyPair keyPair = KeystoreHelpers.getKeyPairFromKeystore(keystoreAlias); - - SecretKey secretKey = SecurityAES.generateSecretKey(256); - byte[] cipherText = SecurityRSA.encrypt(keyPair.getPublic(), secretKey.getEncoded()); - byte[] plainText = SecurityRSA.decrypt(keyPair.getPrivate(), cipherText); - assertArrayEquals(secretKey.getEncoded(), plainText); - } -} diff --git a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityX25519Test.kt b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityX25519Test.kt deleted file mode 100644 index 52a1f89..0000000 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityX25519Test.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet - -import androidx.test.filters.SmallTest -import junit.framework.TestCase.assertEquals -import org.junit.Assert.assertArrayEquals -import org.junit.Test - -@SmallTest -class SecurityX25519Test { - - @Test - fun sharedSecret() { - val alice = SecurityCurve25519() - val bob = SecurityCurve25519() - - val alicePubKey = alice.generateKey() - val bobPubKey = bob.generateKey() - - val aliceSharedSecret = alice.calculateSharedSecret(bobPubKey) - val bobSharedSecret = bob.calculateSharedSecret(alicePubKey) - - assertArrayEquals(aliceSharedSecret, bobSharedSecret) - } -} \ No newline at end of file diff --git a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/HeadersTest.kt b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/HeadersTest.kt deleted file mode 100644 index e4876ae..0000000 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/HeadersTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal - -import androidx.test.filters.SmallTest -import junit.framework.TestCase.assertEquals -import org.junit.Test -import java.security.SecureRandom - -@SmallTest -class HeadersTest { - - @Test fun headersTest() { - val header = Headers(SecureRandom.getSeed(32), 0, 0) - val header1 = Headers.deSerializeHeader(header.serialized) - - assertEquals(header, header1) - } -} \ No newline at end of file diff --git a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsTest.kt b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsTest.kt deleted file mode 100644 index 63a8fc3..0000000 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal - -import android.content.Context -import androidx.core.util.component1 -import androidx.core.util.component2 -import androidx.test.filters.SmallTest -import androidx.test.platform.app.InstrumentationRegistry -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.SecurityCurve25519 -import org.junit.Assert.assertArrayEquals -import org.junit.Test -import java.security.SecureRandom - -@SmallTest -class RatchetsTest { - var context: Context = - InstrumentationRegistry.getInstrumentation().targetContext - - @Test - fun completeRatchetTest() { - val alice = SecurityCurve25519() - val bob = SecurityCurve25519() - - val SK = alice.calculateSharedSecret(bob.generateKey()) - val SK1 = bob.calculateSharedSecret(alice.generateKey()) - assertArrayEquals(SK, SK1) - - val aliceState = States() - Ratchets.ratchetInitAlice(aliceState, SK, bob.generateKey()) - - val bobState = States() - Ratchets.ratchetInitBob(bobState, SK, bob.getKeypair()) - - val originalText = SecureRandom.getSeed(32); - val (header, aliceCipherText) = Ratchets.ratchetEncrypt(aliceState, originalText, - bob.generateKey()) - - var header1: Headers? = null - var aliceCipherText1: ByteArray? = null - for(i in 1..10) { - val (header, aliceCipherText) = Ratchets.ratchetEncrypt(aliceState, originalText, - bob.generateKey()) - header1 = header - aliceCipherText1 = aliceCipherText - } - - val bobPlainText = Ratchets.ratchetDecrypt(bobState, header, aliceCipherText, - bob.generateKey()) - - val bobPlainText1 = Ratchets.ratchetDecrypt(bobState, header1, aliceCipherText1, - bob.generateKey()) - println(bobState.serializedStates) - - assertArrayEquals(originalText, bobPlainText) - assertArrayEquals(originalText, bobPlainText1) - } -} - diff --git a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/StateTest.kt b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/StateTest.kt deleted file mode 100644 index 42e3d84..0000000 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/StateTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal - -import androidx.test.filters.SmallTest -import junit.framework.TestCase.assertEquals -import org.junit.Test -import java.security.SecureRandom - -@SmallTest -class StateTest { - - @Test fun testStates() { - val state = States() - state.DHs = android.util.Pair(SecureRandom.getSeed(32), - SecureRandom.getSeed(32)) - val serializedStates = state.serializedStates - println("Encoded values: $serializedStates") - val state1 = States(serializedStates) - println(state1.serializedStates) - - assertEquals(state, state1) - } -} \ No newline at end of file diff --git a/src/main/java/com/afkanerd/.DS_Store b/src/main/java/com/afkanerd/.DS_Store new file mode 100644 index 0000000..fce4432 Binary files /dev/null and b/src/main/java/com/afkanerd/.DS_Store differ diff --git a/src/main/java/com/afkanerd/smswithoutborders/.DS_Store b/src/main/java/com/afkanerd/smswithoutborders/.DS_Store new file mode 100644 index 0000000..68565d0 Binary files /dev/null and b/src/main/java/com/afkanerd/smswithoutborders/.DS_Store differ diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/.DS_Store b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/.DS_Store new file mode 100644 index 0000000..d661d7f Binary files /dev/null and b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/.DS_Store differ diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java deleted file mode 100644 index df6ed86..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet; - -import android.util.Base64; - -import com.google.common.primitives.Bytes; - -import java.security.GeneralSecurityException; -import java.security.SecureRandom; -import java.util.Arrays; - -import javax.crypto.Mac; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; - -import at.favre.lib.hkdf.HKDF; - -public class CryptoHelpers { - - public final static String pemStartPrefix = "-----BEGIN PUBLIC KEY-----\n"; - public final static String pemEndPrefix = "\n-----END PUBLIC KEY-----"; - - public static byte[] getCipherMacParameters(String ALGO, byte[] mk) throws GeneralSecurityException { - int hashLen = 80; - byte[] info = "ENCRYPT".getBytes(); - byte[] salt = new byte[hashLen]; - Arrays.fill(salt, (byte) 0); - - return HKDF(ALGO, mk, salt, info, hashLen, 1)[0]; - } - - public static Mac buildVerificationHash(byte[] authKey, byte[] AD, byte[] cipherText) throws GeneralSecurityException { - Mac mac = CryptoHelpers.HMAC256(authKey); - byte[] updatedParams = Bytes.concat(AD, cipherText); - mac.update(updatedParams); - return mac; - } - - public static byte[] verifyCipherText(String ALGO, byte[] mk, byte[] cipherText, byte[] AD) throws Exception { - final int SHA256_DIGEST_LEN = 32; - - byte[] hkdfOutput = getCipherMacParameters(ALGO, mk); - byte[] key = new byte[32]; - byte[] authenticationKey = new byte[32]; - byte[] iv = new byte[16]; - - System.arraycopy(hkdfOutput, 32, authenticationKey, 0, 32); - - byte[] macValue = new byte[SHA256_DIGEST_LEN]; - System.arraycopy(cipherText, cipherText.length - SHA256_DIGEST_LEN, - macValue, 0, SHA256_DIGEST_LEN); - - byte[] extractedCipherText = new byte[cipherText.length - SHA256_DIGEST_LEN]; - System.arraycopy(cipherText, 0, extractedCipherText, - 0, extractedCipherText.length); - - byte[] reconstructedMac = - buildVerificationHash(authenticationKey, AD, extractedCipherText) - .doFinal(); - if(Arrays.equals(macValue, reconstructedMac)) { - return extractedCipherText; - } - throw new Exception("Cipher signature verification failed"); - } - - public static byte[][] HKDF(String algo, byte[] ikm, byte[] salt, byte[] info, int len, int num) throws GeneralSecurityException { - if (num < 1) - num = 1; - - HKDF hkdf = algo.equals("HMACSHA512") ? HKDF.fromHmacSha512() : HKDF.fromHmacSha256(); - byte[] output = hkdf.extractAndExpand(salt, ikm, info, len * num); - byte[][] outputs = new byte[num][len]; - for (int i = 0; i < num; ++i) { - System.arraycopy(output, i * len, outputs[i], 0, len); - } - return outputs; - } - - public static Mac HMAC256(byte[] data) throws GeneralSecurityException { - String algorithm = "HmacSHA256"; - Mac hmacOutput = Mac.getInstance(algorithm); - SecretKey key = new SecretKeySpec(data, algorithm); - hmacOutput.init(key); - return hmacOutput; - } - - public static byte[] generateRandomBytes(int length) { - SecureRandom random = new SecureRandom(); - byte[] bytes = new - - byte[length]; - random.nextBytes(bytes); - return bytes; - } -} diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/EncryptionController.kt b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/EncryptionController.kt deleted file mode 100644 index 548a0c3..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/EncryptionController.kt +++ /dev/null @@ -1,348 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet - -import android.content.Context -import android.util.Base64 -import android.widget.Toast -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.extensions.dataStore -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.extensions.getEncryptedBinaryData -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.extensions.getKeypairValues -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.extensions.saveBinaryDataEncrypted -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.extensions.setKeypairValues -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Headers -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Ratchets -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.States -import com.google.gson.Gson -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable - -object EncryptionController { - - @Serializable - enum class SecureRequestMode { - REQUEST_NONE, - REQUEST_REQUESTED, - REQUEST_RECEIVED, - REQUEST_ACCEPTED, - } - - enum class MessageRequestType(val code: Byte) { - TYPE_REQUEST(0x01.toByte()), - TYPE_ACCEPT(0x02.toByte()), - TYPE_MESSAGE(0x03.toByte()); - - companion object { - fun fromCode(code: Byte): MessageRequestType? = - entries.find { it.code == code } // Kotlin 1.9+, use values() before that - - fun fromMessage(message: ByteArray): MessageRequestType? = - entries.find { it.code == message[0] } // Kotlin 1.9+, use values() before that - } - } - - private fun extractRequestPublicKey( publicKey: ByteArray) : ByteArray { - val lenPubKey = publicKey[1].toInt() - return publicKey.drop(2).toByteArray() - } - - private fun extractMessage(data: ByteArray) : Pair { - val lenHeader = data[1].toInt() - val lenMessage = data[2].toInt() - val header = data.copyOfRange(3, 3 + lenHeader) - val message = data.copyOfRange(3 + lenHeader, (3 + lenHeader + lenMessage)) - return Pair(Headers.deSerializeHeader(header), message) - } - - @OptIn(ExperimentalUnsignedTypes::class) - private fun formatRequestPublicKey( - publicKey: ByteArray, - type: MessageRequestType - ) : ByteArray { - val mn = ubyteArrayOf(type.code.toUByte()) - val lenPubKey = ubyteArrayOf(publicKey.size.toUByte()) - - return (mn + lenPubKey).toByteArray() + publicKey - } - - @OptIn(ExperimentalUnsignedTypes::class) - private fun formatMessage( - header: Headers, - cipherText: ByteArray - ) : ByteArray { - val mn = ubyteArrayOf(MessageRequestType.TYPE_MESSAGE.code.toUByte()) - val lenHeader = ubyteArrayOf(header.serialized.size.toUByte()) - val lenMessage = ubyteArrayOf(cipherText.size.toUByte()) - - return (mn + lenHeader + lenMessage).toByteArray() + header.serialized + cipherText - } - - suspend fun sendRequest( - context: Context, - address: String, - mode: SecureRequestMode, - ): ByteArray { - try { - val publicKey = generateIdentityPublicKeys(context, address) - - var type: MessageRequestType? = null - val mode = when(mode) { - SecureRequestMode.REQUEST_RECEIVED -> { - type = MessageRequestType.TYPE_ACCEPT - SecureRequestMode.REQUEST_ACCEPTED - } - else -> { - type = MessageRequestType.TYPE_REQUEST - SecureRequestMode.REQUEST_REQUESTED - } - } - - context.setEncryptionModeStates(address, mode) - return formatRequestPublicKey(publicKey, type) - } catch (e: Exception) { - throw e - } - } - - suspend fun receiveRequest( - context: Context, - address: String, - publicKey: ByteArray, - ) : ByteArray? { - MessageRequestType.fromCode(publicKey[0])?.let { type -> - val publicKey = extractRequestPublicKey(publicKey) - try { - val mode = when(type) { - MessageRequestType.TYPE_REQUEST -> { - SecureRequestMode.REQUEST_RECEIVED - } - MessageRequestType.TYPE_ACCEPT -> { - context.removeEncryptionRatchetStates(address) - SecureRequestMode.REQUEST_ACCEPTED - } - else -> return null - } - context.setEncryptionModeStates( - address, - mode, - publicKey, - ) - } catch (e: Exception) { - throw e - } - return publicKey - } - - return null - } - - @Throws - private suspend fun generateIdentityPublicKeys( - context: Context, - address: String - ): ByteArray { - try { - val libSigCurve25519 = SecurityCurve25519() - val publicKey = libSigCurve25519.generateKey() - context.setKeypairValues(address, publicKey, libSigCurve25519.privateKey) - return publicKey - } catch (e: Exception) { - throw e - } - } - - @Throws - suspend fun decrypt( - context: Context, - address: String, - text: String - ): String? { - - val data = Base64.decode(text, Base64.DEFAULT) - if(MessageRequestType.fromCode(data[0]) != MessageRequestType.TYPE_MESSAGE) - return null - - val payload = try { extractMessage(data) } catch(e: Exception) { - throw e - } - - val modeStates = context.getEncryptionModeStatesSync(address) - val publicKey = Gson().fromJson(modeStates, - SavedEncryptedModes::class.java).publicKey - - if(publicKey == null) { - CoroutineScope(Dispatchers.Main).launch { - Toast.makeText( - context, - context.getString(R.string.missing_public_key), - Toast.LENGTH_LONG).show() - } - return null - } - - val publicKeyBytes = Base64.decode(publicKey, Base64.DEFAULT) - - val keystore = address + "_ratchet_state" - val currentState = context.getEncryptedBinaryData(keystore) - - var state: States? - if(currentState == null) { - state = States() - val sk = context.calculateSharedSecret(address, publicKeyBytes) - val keypair = context.getKeypairValues(address) //public private - - Ratchets.ratchetInitBob( - state, - sk, - android.util.Pair(keypair.second, keypair.first) - ) - } - else state = States(String(currentState)) - - val keypair = context.getKeypairValues(address) - var decryptedText: String? - try { - decryptedText = String(Ratchets.ratchetDecrypt( - state, - payload.first, - payload.second, - keypair.first - )) - context.saveBinaryDataEncrypted(keystore, - state.serializedStates.encodeToByteArray()) - } catch(e: Exception) { - throw e - } - return decryptedText - } - - @Throws - suspend fun encrypt( - context: Context, - address: String, - text: String - ) : String? { - val modeStates = context.getEncryptionModeStatesSync(address) - val publicKey = Gson().fromJson(modeStates, - SavedEncryptedModes::class.java).publicKey - - if(publicKey == null) { - CoroutineScope(Dispatchers.Main).launch { - Toast.makeText( - context, - context.getString(R.string.missing_public_key), - Toast.LENGTH_LONG).show() - } - return null - } - - val publicKeyBytes = Base64.decode(publicKey, Base64.DEFAULT) - - val keystore = address + "_ratchet_state" - val currentState = context.getEncryptedBinaryData(keystore) - - var state: States? - if(currentState == null) { - state = States() - val sk = context.calculateSharedSecret(address, publicKeyBytes) - Ratchets.ratchetInitAlice(state, sk, publicKeyBytes) - } - else state = States(String(currentState)) - - val ratchetOutput = Ratchets.ratchetEncrypt(state, - text.encodeToByteArray(), publicKeyBytes) - - return try { - val message = formatMessage( - ratchetOutput.first, - ratchetOutput.second - ) - context.saveBinaryDataEncrypted(keystore, - state.serializedStates.encodeToByteArray()) - Base64.encodeToString(message, Base64.DEFAULT) - } catch(e: Exception) { - throw e - } - } -} - -private suspend fun Context.calculateSharedSecret( - address: String, - publicKey: ByteArray -): ByteArray? { - val keypair = getKeypairValues(address) //public private - keypair.second?.let { privateKey -> - val libSigCurve25519 = SecurityCurve25519(privateKey) - return libSigCurve25519.calculateSharedSecret(publicKey) - } - return null -} - -data class SavedEncryptedModes( - var mode: EncryptionController.SecureRequestMode, - var publicKey: String? = null, -) - -private suspend fun Context.setEncryptionModeStates( - address: String, - mode: EncryptionController.SecureRequestMode, - publicKey: ByteArray? = null, -) { - val keyValue = stringPreferencesKey(address + "_mode_states") - dataStore.edit { secureComms -> - // Make a mutable copy of existing state - val currentState = secureComms[keyValue] ?: "" - val savedEncryptedModes = if(currentState.isNotEmpty()) Gson() - .fromJson(currentState, SavedEncryptedModes::class.java) - .apply { this.mode = mode } - else SavedEncryptedModes(mode = mode) - - publicKey?.let { publicKey -> - savedEncryptedModes.publicKey = - Base64.encodeToString(publicKey, Base64.DEFAULT) - } - - secureComms[keyValue] = Gson().toJson(savedEncryptedModes) - } -} - -suspend fun Context.removeEncryptionRatchetStates(address: String) { - val keyValue = stringPreferencesKey(address + "_ratchet_state") - dataStore.edit { secureComms -> - secureComms.remove(keyValue) - withContext(Dispatchers.Main) { - Toast.makeText( - this@removeEncryptionRatchetStates, - getString(R.string.ratchet_states_removed), - Toast.LENGTH_LONG).show() - } - } -} - -suspend fun Context.removeEncryptionModeStates(address: String) { - val keyValue = stringPreferencesKey(address + "_mode_states") - dataStore.edit { secureComms -> - secureComms.remove(keyValue) - } -} - -fun Context.getEncryptionRatchetStates(address: String): Flow { - val keyValue = stringPreferencesKey(address + "_ratchet_state") - return dataStore.data.map { it[keyValue] } -} - -suspend fun Context.getEncryptionModeStatesSync(address: String): String? { - val keyValue = stringPreferencesKey(address + "_mode_states") - return dataStore.data.first()[keyValue] -} - -fun Context.getEncryptionModeStates(address: String): Flow { - val keyValue = stringPreferencesKey(address + "_mode_states") - return dataStore.data.map { it[keyValue] } -} diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityAES.java b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityAES.java deleted file mode 100644 index 30e47a1..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityAES.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet; - -import android.security.keystore.KeyProperties; - -import com.google.common.primitives.Bytes; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.KeyGenerator; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -public class SecurityAES { - - public static final String DEFAULT_AES_ALGORITHM = "AES/CBC/PKCS5Padding"; - - public static final String ALGORITHM = "AES"; - - public static SecretKey generateSecretKey(int size) throws NoSuchAlgorithmException { - KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES); - keyGenerator.init(size); // Adjust key size as needed - return keyGenerator.generateKey(); - } - - public static byte[] encryptAESGCM(byte[] data, SecretKey secretKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding"); - aesCipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] cipherText = aesCipher.doFinal(data); - - final byte[] IV = aesCipher.getIV(); - byte[] cipherTextIv = new byte[IV.length + cipherText.length]; - System.arraycopy(IV, 0, cipherTextIv, 0, IV.length); - System.arraycopy(cipherText, 0, cipherTextIv, IV.length, cipherText.length); - return cipherTextIv; - } - - public static byte[] decryptAESGCM(byte[] data, SecretKey secretKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { - byte[] iv = new byte[12]; - System.arraycopy(data, 0, iv, 0, iv.length); - - byte[] _data = new byte[data.length - iv.length]; - System.arraycopy(data, iv.length, _data, 0, _data.length); - - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128,iv); - - Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding"); - aesCipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); - return aesCipher.doFinal(_data); - } - - public static byte[] encryptAES256CBC(byte[] input, byte[] secretKey, byte[] iv) throws Throwable { - SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, 0, secretKey.length, "AES"); - - Cipher cipher = Cipher.getInstance(DEFAULT_AES_ALGORITHM); - if(iv != null) { - IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); - return cipher.doFinal(input); - } - - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); - byte[] ciphertext = cipher.doFinal(input); - return Bytes.concat(cipher.getIV(), ciphertext); - } - - public static byte[] decryptAES256CBC(byte[] input, byte[] sharedKey, byte[] iv) throws Throwable { - SecretKeySpec secretKeySpec = new SecretKeySpec(sharedKey, ALGORITHM); - - Cipher cipher = Cipher.getInstance(DEFAULT_AES_ALGORITHM); - if(iv == null) { - iv = new byte[16]; - System.arraycopy(input, 0, iv, 0, 16); - - byte[] content = new byte[input.length - 16]; - System.arraycopy(input, 16, content, 0, content.length); - input = content; - } - - IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); - return cipher.doFinal(input); - } -} diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt deleted file mode 100644 index 9382dbb..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet - -import com.github.netricecake.ecdh.Curve25519 - -class SecurityCurve25519(val privateKey: ByteArray = Curve25519.generateRandomKey()) { - fun generateKey(): ByteArray { - return Curve25519.publicKey(this.privateKey) - } - - fun calculateSharedSecret(publicKey: ByteArray): ByteArray { - val sharedKey = Curve25519.sharedSecret(this.privateKey, publicKey) - return CryptoHelpers.HKDF("HMACSHA256", sharedKey, null, - "x25591_key_exchange".encodeToByteArray(), 32, 1)[0] - } - - fun getKeypair(): android.util.Pair { - return android.util.Pair(privateKey, generateKey()) - } -} \ No newline at end of file diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSA.kt b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSA.kt deleted file mode 100644 index fa8112d..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSA.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet - -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import java.security.InvalidAlgorithmParameterException -import java.security.InvalidKeyException -import java.security.KeyPairGenerator -import java.security.NoSuchAlgorithmException -import java.security.NoSuchProviderException -import java.security.PrivateKey -import java.security.PublicKey -import java.security.spec.MGF1ParameterSpec -import javax.crypto.BadPaddingException -import javax.crypto.Cipher -import javax.crypto.IllegalBlockSizeException -import javax.crypto.NoSuchPaddingException -import javax.crypto.spec.OAEPParameterSpec -import javax.crypto.spec.PSource - -object SecurityRSA { - var defaultEncryptionDigest: MGF1ParameterSpec? = MGF1ParameterSpec.SHA256 - var defaultDecryptionDigest: MGF1ParameterSpec? = MGF1ParameterSpec.SHA1 - - var encryptionDigestParam: OAEPParameterSpec = OAEPParameterSpec( - "SHA-256", "MGF1", defaultEncryptionDigest, - PSource.PSpecified.DEFAULT - ) - var decryptionDigestParam: OAEPParameterSpec = OAEPParameterSpec( - "SHA-256", "MGF1", defaultDecryptionDigest, - PSource.PSpecified.DEFAULT - ) - - @JvmStatic - @Throws( - NoSuchAlgorithmException::class, - NoSuchProviderException::class, - InvalidAlgorithmParameterException::class - ) - fun generateKeyPair(keystoreAlias: String, keySize: Int = 2048): PublicKey? { - val kpg = KeyPairGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore" - ) - kpg.initialize( - KeyGenParameterSpec.Builder( - keystoreAlias, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - ) - .setKeySize(keySize) - .setDigests( - KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256, - KeyProperties.DIGEST_SHA512 - ) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) - .build() - ) - return kpg.generateKeyPair().public - } - - @JvmStatic - @Throws( - NoSuchPaddingException::class, - NoSuchAlgorithmException::class, - IllegalBlockSizeException::class, - BadPaddingException::class, - InvalidKeyException::class, - InvalidAlgorithmParameterException::class - ) - fun decrypt(privateKey: PrivateKey?, data: ByteArray?): ByteArray? { - val cipher = Cipher.getInstance("RSA/ECB/" + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) - // cipher.init(Cipher.DECRYPT_MODE, privateKey, decryptionDigestParam); - cipher.init(Cipher.DECRYPT_MODE, privateKey) - return cipher.doFinal(data) - } - - @JvmStatic - @Throws( - NoSuchPaddingException::class, - NoSuchAlgorithmException::class, - IllegalBlockSizeException::class, - BadPaddingException::class, - InvalidKeyException::class, - InvalidAlgorithmParameterException::class - ) - fun encrypt(publicKey: PublicKey?, data: ByteArray?): ByteArray? { - val cipher = Cipher.getInstance("RSA/ECB/" + KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) - // cipher.init(Cipher.ENCRYPT_MODE, publicKey, encryptionDigestParam); - cipher.init(Cipher.ENCRYPT_MODE, publicKey) - return cipher.doFinal(data) - } -} diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/extensions/context.kt b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/extensions/context.kt deleted file mode 100644 index 7b2c70c..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/extensions/context.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet.extensions - -import android.content.Context -import android.util.Base64 -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.core.stringSetPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.SecurityAES -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.SecurityRSA -import com.google.gson.Gson -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import java.io.IOException -import java.security.KeyFactory -import java.security.KeyPair -import java.security.KeyStore -import java.security.KeyStoreException -import java.security.NoSuchAlgorithmException -import java.security.UnrecoverableEntryException -import java.security.cert.CertificateException -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import javax.crypto.SecretKey -import javax.crypto.spec.SecretKeySpec - -val Context.dataStore: DataStore by preferencesDataStore(name = "secure_comms") - -/** - * Pair - */ -suspend fun Context.getKeypairValues(address: String): Pair { - val keyValue = stringSetPreferencesKey(address + "_keypair") - val keypairSet = dataStore.data.first()[keyValue] - val encryptionPublicKey = getKeypairFromKeystore(address) - - val publicKey = SecurityRSA.decrypt( - encryptionPublicKey?.private, - Base64.decode(keypairSet?.elementAt(0), Base64.DEFAULT) - ) - val privateKey = SecurityRSA.decrypt( - encryptionPublicKey?.private, - Base64.decode(keypairSet?.elementAt(1), Base64.DEFAULT) - ) - return Pair(publicKey, privateKey) -} - -suspend fun Context.setKeypairValues( - address: String, - publicKey: ByteArray, - privateKey: ByteArray, -) { - val encryptionPublicKey = SecurityRSA.generateKeyPair(address) - - val keyValue = stringSetPreferencesKey(address + "_keypair") - dataStore.edit { secureComms-> - secureComms[keyValue] = setOf( - Base64.encodeToString(publicKey.run { - SecurityRSA.encrypt(encryptionPublicKey, this) - }, Base64.DEFAULT), - Base64.encodeToString(privateKey.run { - SecurityRSA.encrypt(encryptionPublicKey, this) - }, Base64.DEFAULT), - ) - } -} - -@Throws( - KeyStoreException::class, - CertificateException::class, - IOException::class, - NoSuchAlgorithmException::class, - UnrecoverableEntryException::class -) -fun Context.getKeypairFromKeystore(keystoreAlias: String): KeyPair? { - val keyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null) - - val entry = keyStore.getEntry(keystoreAlias, null) - if (entry is KeyStore.PrivateKeyEntry) { - val privateKey = entry.privateKey - val publicKey = keyStore.getCertificate(keystoreAlias).publicKey - return KeyPair(publicKey, privateKey) - } - return null -} - -data class SavedBinaryData( - val key: ByteArray, - val algorithm: String, - val data: ByteArray, -) - -/** - * Would overwrite anything with the same Keystore Alias - */ -@Throws -suspend fun Context.saveBinaryDataEncrypted( - keystoreAlias: String, - data: ByteArray, -) : Boolean { - val keyValue = stringPreferencesKey(keystoreAlias) - - val aesGcmKey = SecurityAES.generateSecretKey(256) - val data = SecurityAES.encryptAESGCM(data, aesGcmKey) - -// val encryptionPublicKey = getKeypairFromKeystore(keystoreAlias)?.public -// ?: SecurityRSA.generateKeyPair(keystoreAlias) - - var saved = false - dataStore.edit { secureComms-> - try { - val encryptionPublicKey = SecurityRSA.generateKeyPair(keystoreAlias) - SecurityRSA.encrypt(encryptionPublicKey, aesGcmKey.encoded)?.let { key -> - secureComms[keyValue] = Gson().toJson( - SavedBinaryData( - key = key, - algorithm = aesGcmKey.algorithm, - data = data - ) - ) - saved = true - } - } catch(e: Exception) { - throw e - } - } - return saved -} - -@Throws -suspend fun Context.getEncryptedBinaryData(keystoreAlias: String): ByteArray? { - val keyValue = stringPreferencesKey(keystoreAlias) - val data = dataStore.data.first()[keyValue] - if(data == null) return null - - val savedBinaryData = Gson().fromJson(data, SavedBinaryData::class.java) - - return try { - val encryptionPublicKey = getKeypairFromKeystore(keystoreAlias) - SecurityRSA.decrypt(encryptionPublicKey?.private, savedBinaryData.key) - ?.run { - SecurityAES.decryptAESGCM(savedBinaryData.data, - SecretKeySpec(this, savedBinaryData.algorithm) - ) - } - } catch(e: Exception) { - throw e - } -} diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Headers.java b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Headers.java deleted file mode 100644 index e238589..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Headers.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal; - -import android.util.Pair; - -import androidx.annotation.Nullable; - -import com.google.common.primitives.Bytes; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; - -public class Headers { - - public byte[] dh; - public int PN; - public int N; - - /** - * - * @param dhPair This is a public key - * @param PN - * @param N - */ - public Headers(Pair dhPair, int PN, int N) { - this.dh = dhPair.second; - this.PN = PN; - this.N = N; - } - - public Headers(byte[] dh, int PN, int N) { - this.dh = dh; - this.PN = PN; - this.N = N; - } - - public Headers() {} - - public static Headers deSerializeHeader(byte[] serializedHeader) throws NumberFormatException { - byte[] bytesPN = new byte[4]; - System.arraycopy(serializedHeader, 0, bytesPN, 0, 4); - int PN = ByteBuffer.wrap(bytesPN).order(ByteOrder.LITTLE_ENDIAN).getInt(); - - byte[] bytesN = new byte[4]; - System.arraycopy(serializedHeader, 4, bytesN, 0, 4); - int N = ByteBuffer.wrap(bytesN).order(ByteOrder.LITTLE_ENDIAN).getInt(); - - byte[] pubKey = new byte[serializedHeader.length - 8]; - System.arraycopy(serializedHeader, 8, pubKey, 0, pubKey.length); - - return new Headers(pubKey, PN, N); - } - - @Override - public boolean equals(@Nullable Object obj) { - if(obj instanceof Headers header) { - return Arrays.equals(header.dh, this.dh) && - header.PN == this.PN && - header.N == this.N; - } - return false; - } - - public byte[] getSerialized() throws IOException { - byte[] bytesPN = new byte[4]; - ByteBuffer.wrap(bytesPN).order(ByteOrder.LITTLE_ENDIAN).putInt(this.PN); - - byte[] bytesN = new byte[4]; - ByteBuffer.wrap(bytesN).order(ByteOrder.LITTLE_ENDIAN).putInt(this.N); - - return Bytes.concat(bytesPN, bytesN, this.dh); - } -} diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Protocols.java b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Protocols.java deleted file mode 100644 index 976ae94..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Protocols.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal; - -import static com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoHelpers.buildVerificationHash; -import static com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoHelpers.getCipherMacParameters; -import static com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoHelpers.verifyCipherText; - -import android.util.Pair; - -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.CryptoHelpers; -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.SecurityAES; -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.SecurityCurve25519; -import com.google.common.primitives.Bytes; - -import java.io.IOException; -import java.security.GeneralSecurityException; - -import javax.crypto.Mac; - -/** - * This implementations are based on the signal protocols specifications. - * - * This are based on the recommended algorithms and parameters for the encryption - * and decryption. - * - * The goal for this would be to transform it into library which can be used across - * other SMS projects. - * - * ... - */ -public class Protocols { - final static int HKDF_LEN = 32; - final static int HKDF_NUM_KEYS = 2; - final static String ALGO = "HMACSHA512"; - - public static Pair GENERATE_DH() { - SecurityCurve25519 securityCurve25519 = new SecurityCurve25519(); - return new Pair<>(securityCurve25519.getPrivateKey(), securityCurve25519.generateKey()); - } - - /** - * - * @param dhPair This private key (keypair required in Android if supported) - * @param peerPublicKey - * @return - * @throws GeneralSecurityException - * @throws IOException - * @throws InterruptedException - */ - public static byte[] DH(Pair dhPair, byte[] peerPublicKey) { - SecurityCurve25519 securityCurve25519 = new SecurityCurve25519(dhPair.first); - return securityCurve25519.calculateSharedSecret(peerPublicKey); - } - - public static Pair KDF_RK(byte[] rk, byte[] dhOut) throws GeneralSecurityException { - byte[] info = "KDF_RK".getBytes(); - byte[][] hkdfOutput = CryptoHelpers.HKDF(ALGO, dhOut, rk, info, HKDF_LEN, HKDF_NUM_KEYS); - return new Pair<>(hkdfOutput[0], hkdfOutput[1]); - } - - public static Pair KDF_CK(byte[] ck) throws GeneralSecurityException { -// Mac mac = CryptoHelpers.HMAC512(ck); - Mac mac = CryptoHelpers.HMAC256(ck); - byte[] _ck = mac.doFinal(new byte[]{0x01}); - byte[] mk = mac.doFinal(new byte[]{0x02}); - return new Pair<>(_ck, mk); - } - - public static byte[] ENCRYPT(byte[] mk, byte[] plainText, byte[] associated_data) throws Throwable { - byte[] hkdfOutput = getCipherMacParameters(ALGO, mk); - byte[] key = new byte[32]; - byte[] authenticationKey = new byte[32]; - byte[] iv = new byte[16]; - - System.arraycopy(hkdfOutput, 0, key, 0, 32); - System.arraycopy(hkdfOutput, 32, authenticationKey, 0, 32); - System.arraycopy(hkdfOutput, 64, iv, 0, 16); - - byte[] cipherText = SecurityAES.encryptAES256CBC(plainText, key, iv); - byte[] mac = buildVerificationHash(authenticationKey, associated_data, cipherText).doFinal(); - return Bytes.concat(cipherText, mac); - } - - public static byte[] DECRYPT(byte[] mk, byte[] cipherText, byte[] associated_data) throws Throwable { - cipherText = verifyCipherText(ALGO, mk, cipherText, associated_data); - - byte[] hkdfOutput = getCipherMacParameters(ALGO, mk); - byte[] key = new byte[32]; - byte[] iv = new byte[16]; - System.arraycopy(hkdfOutput, 0, key, 0, 32); - System.arraycopy(hkdfOutput, 64, iv, 0, 16); - - return SecurityAES.decryptAES256CBC(cipherText, key, iv); - } - - public static byte[] CONCAT(byte[] AD, Headers headers) throws IOException { - return Bytes.concat(AD, headers.getSerialized()); - } - -} - diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Ratchets.java b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Ratchets.java deleted file mode 100644 index 36e03cf..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Ratchets.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal; - -import android.util.Pair; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.util.Arrays; - -public class Ratchets { - public static final int MAX_SKIP = 100; - - /** - * - * @param state - * @param SK - * @param dhPublicKeyBob - * @throws GeneralSecurityException - * @throws IOException - * @throws InterruptedException - */ - public static void ratchetInitAlice(States state, - byte[] SK, - byte[] dhPublicKeyBob) throws GeneralSecurityException, IOException, InterruptedException { - state.DHs = Protocols.GENERATE_DH(); - state.DHr = dhPublicKeyBob; - byte[] dh_out = Protocols.DH(state.DHs, state.DHr); - Pair kdfRkOutput = Protocols.KDF_RK(SK, dh_out); - state.RK = kdfRkOutput.first; - state.CKs = kdfRkOutput.second; - } - - public static void ratchetInitBob(States state, byte[] SK, Pair dhKeyPairBob) { - state.DHs = dhKeyPairBob; - state.RK = SK; - } - - public static Pair ratchetEncrypt(States state, byte[] plainText, byte[] AD) throws Throwable { - Pair kdfCkOutput = Protocols.KDF_CK(state.CKs); - state.CKs = kdfCkOutput.first; - byte[] mk = kdfCkOutput.second; - Headers header = new Headers(state.DHs, state.PN, state.Ns); - state.Ns += 1; - - byte[] cipherText = Protocols.ENCRYPT(mk, plainText, Protocols.CONCAT(AD, header)); - return new Pair<>(header, cipherText); - } - - /** - * - * @param state - * @param header - * @param cipherText - * @param AD - * @return - * @throws Throwable - */ - public static byte[] ratchetDecrypt(States state, - Headers header, - byte[] cipherText, - byte[] AD) throws Throwable { - byte[] plainText = trySkippedMessageKeys(state, header, cipherText, AD); - if(plainText != null) - return plainText; - - if(state.DHr == null || !Arrays.equals(header.dh, state.DHr)) { - skipMessageKeys(state, header.PN); - DHRatchet(state, header); - } - skipMessageKeys(state, header.N); - Pair kdfCkOutput = Protocols.KDF_CK(state.CKr); - state.CKr = kdfCkOutput.first; - byte[] mk = kdfCkOutput.second; - state.Nr += 1; - return Protocols.DECRYPT(mk, cipherText, Protocols.CONCAT(AD, header)); - } - - private static void DHRatchet(States state, Headers header) throws GeneralSecurityException, IOException, InterruptedException { - state.PN = state.Ns; - state.Ns = 0; - state.Nr = 0; - state.DHr = header.dh; - byte[] dh_out = Protocols.DH(state.DHs, state.DHr); - Pair kdfRkOutput = Protocols.KDF_RK(state.RK, dh_out); - state.RK = kdfRkOutput.first; - state.CKr = kdfRkOutput.second; - - state.DHs = Protocols.GENERATE_DH(); - kdfRkOutput = Protocols.KDF_RK(state.RK, Protocols.DH(state.DHs, state.DHr)); - state.RK = kdfRkOutput.first; - state.CKs = kdfRkOutput.second; - } - - private static byte[] trySkippedMessageKeys(States state, Headers header, byte[] cipherText, byte[] AD) throws Throwable { - Pair mkSkippedKeys = new Pair<>(header.dh, header.N); - if(state.MKSKIPPED.containsKey(mkSkippedKeys)){ - byte[] mk = state.MKSKIPPED.get(mkSkippedKeys); - state.MKSKIPPED.remove(mkSkippedKeys); - return Protocols.DECRYPT(mk, cipherText, Protocols.CONCAT(AD, header)); - } - return null; - } - - private static void skipMessageKeys(States state, int until) throws Exception { - if((state.Nr + MAX_SKIP) < until) { - throw new Exception("MAX skip exceeded"); - } - - if(state.CKr != null) { - while(state.Nr < until) { - Pair kdfCkOutput = Protocols.KDF_CK(state.CKr); - state.CKr = kdfCkOutput.first; - byte[] mk = kdfCkOutput.second; - state.MKSKIPPED.put(new Pair<>(state.DHr, state.Nr), mk); - state.Nr +=1; - } - } - } - -} diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.java b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.java deleted file mode 100644 index 1625b57..0000000 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal; - -import android.util.Log; -import android.util.Pair; -import android.util.Base64; - -import androidx.annotation.Nullable; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; - -import com.google.gson.JsonSerializer; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.lang.reflect.Type; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -public class States { - public Pair DHs; - public byte[] DHr; - public byte[] RK; - public byte[] CKs; - public byte[] CKr; - - public int Ns = 0; - public int Nr = 0; - public int PN = 0; - - public Map, byte[]> MKSKIPPED = new HashMap<>(); - - public States(String states) throws JSONException { - if(states == null) - return; - - JSONObject jsonObject = new JSONObject(states); - if(jsonObject.has("DHs")) { - String[] encodedValues = jsonObject.getString("DHs").split(" "); - this.DHs = new Pair<>(android.util.Base64.decode(encodedValues[0], Base64.NO_WRAP), - android.util.Base64.decode(encodedValues[1], Base64.NO_WRAP)); - } - if(jsonObject.has("DHr")) - this.DHr = Base64.decode(jsonObject.getString("DHr"), Base64.NO_WRAP); - - if(jsonObject.has("RK")) - this.RK = Base64.decode(jsonObject.getString("RK"), Base64.NO_WRAP); - if(jsonObject.has("CKs")) - this.CKs = Base64.decode(jsonObject.get("CKs").toString(), Base64.NO_WRAP); - if(jsonObject.has("CKr")) - this.CKr = Base64.decode(jsonObject.getString("CKr"), Base64.NO_WRAP); - this.Ns = jsonObject.getInt("Ns"); - this.Nr = jsonObject.getInt("Nr"); - this.PN = jsonObject.getInt("PN"); - - JSONArray mkskipped = jsonObject.getJSONArray("MKSKIPPED"); - for(int i=0;i(pubkey, pair.getInt(StatesMKSKIPPED.N)), - Base64.decode(pair.getString(StatesMKSKIPPED.MK), Base64.NO_WRAP)); - } - } - - public static byte[] getADForHeaders(States states, Headers headers) { - for(Map.Entry, byte[]> entry : states.MKSKIPPED.entrySet()) { - if(entry.getKey().second == (headers.PN + headers.N)) - return entry.getKey().first; - } - - return null; - } - - public States() { - } - - public String getSerializedStates() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(KeyPair.class, new StatesKeyPairSerializer()); - gsonBuilder.registerTypeAdapter(PublicKey.class, new StatesPublicKeySerializer()); - gsonBuilder.registerTypeAdapter(byte[].class, new StatesBytesSerializer()); - gsonBuilder.registerTypeAdapter(Pair.class, new PairStatesBytesSerializer()); - gsonBuilder.registerTypeAdapter(Map.class, new StatesMKSKIPPED()); - gsonBuilder.setPrettyPrinting() - .disableHtmlEscaping(); - - Gson gson = gsonBuilder.create(); - return gson.toJson(this); - } - - @Override - public boolean equals(@Nullable Object obj) { - if(obj instanceof States state) { - return state.getSerializedStates().equals(this.getSerializedStates()); - } - return false; - } - - public static class StatesKeyPairSerializer implements JsonSerializer { - @Override - public JsonElement serialize(KeyPair src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive( - Base64.encodeToString(src.getPublic().getEncoded(), Base64.NO_WRAP)); - } - } - - public static class StatesPublicKeySerializer implements JsonSerializer { - @Override - public JsonElement serialize(PublicKey src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(Base64.encodeToString(src.getEncoded(), Base64.NO_WRAP)); - } - } - - public static class PairStatesBytesSerializer implements JsonSerializer> { - @Override - public JsonElement serialize(Pair src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive( Base64.encodeToString(src.first, Base64.NO_WRAP) + " " + - Base64.encodeToString(src.second, Base64.NO_WRAP)); - } - } - - public static class StatesBytesSerializer implements JsonSerializer { - @Override - public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive( Base64.encodeToString(src, Base64.NO_WRAP)); - } - } - - - public static class StatesMKSKIPPED implements JsonSerializer, byte[]>> { - public final static String PUBLIC_KEY = "PUBLIC_KEY"; - public final static String N = "N"; - public final static String MK = "MK"; - - @Override - public JsonElement serialize(Map, byte[]> src, Type typeOfSrc, JsonSerializationContext context) { - JsonArray jsonArray = new JsonArray(); - for(Map.Entry, byte[]> entry: src.entrySet()) { - String publicKey = Base64.encodeToString(entry.getKey().first, Base64.NO_WRAP); - Integer n = entry.getKey().second; - - JsonObject jsonObject1 = new JsonObject(); - jsonObject1.addProperty(PUBLIC_KEY, publicKey); - jsonObject1.addProperty(N, n); - jsonObject1.addProperty(MK, Base64.encodeToString(entry.getValue(), Base64.NO_WRAP)); - - jsonArray.add(jsonObject1); - } - return jsonArray; - } - } - -} diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml deleted file mode 100644 index e8057bf..0000000 --- a/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - SMSWithoutBorders DoubleRatchet LibSignal - Missing public key - Ratchet states removed - \ No newline at end of file