From c2bba7bcf713fabc3124633d67c59f3509e33a22 Mon Sep 17 00:00:00 2001 From: sherlockwisdom Date: Fri, 9 Jan 2026 21:06:46 +0100 Subject: [PATCH 1/3] update: added new method for agreeing with nonce --- .../CryptoHelpers.java | 4 +- .../SecurityCurve25519.kt | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java index df6ed86..cdea976 100644 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java +++ b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java @@ -85,9 +85,7 @@ public static Mac HMAC256(byte[] data) throws GeneralSecurityException { public static byte[] generateRandomBytes(int length) { SecureRandom random = new SecureRandom(); - byte[] bytes = new - - byte[length]; + byte[] bytes = new byte[length]; random.nextBytes(bytes); return bytes; } diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt index 9382dbb..dd62d59 100644 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt +++ b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt @@ -7,6 +7,43 @@ class SecurityCurve25519(val privateKey: ByteArray = Curve25519.generateRandomKe return Curve25519.publicKey(this.privateKey) } + fun agreeWithAuthAndNonce( + authenticationPublicKey: ByteArray, + publicKey: ByteArray, + salt: ByteArray, + nonce1: ByteArray, + nonce2: ByteArray, + info: ByteArray, + ): ByteArray { + val handshakeSalt = nonce1 + nonce2 + val dh1 = Curve25519.sharedSecret(this.privateKey, authenticationPublicKey) + val dh2 = Curve25519.sharedSecret(this.privateKey, publicKey) + var ck = CryptoHelpers.HKDF( + "HMACSHA256", + handshakeSalt, + salt, + info, + 32, + 1 + )[0] + ck = CryptoHelpers.HKDF( + "HMACSHA256", + dh1, + ck, + info, + 32, + 1 + )[0] + return CryptoHelpers.HKDF( + "HMACSHA256", + dh2, + ck, + info, + 32, + 1 + )[0] + } + fun calculateSharedSecret(publicKey: ByteArray): ByteArray { val sharedKey = Curve25519.sharedSecret(this.privateKey, publicKey) return CryptoHelpers.HKDF("HMACSHA256", sharedKey, null, From 433c1fde62f7ab3654106dc3c1ef6d6367b012b1 Mon Sep 17 00:00:00 2001 From: sherlockwisdom Date: Sun, 11 Jan 2026 18:48:12 +0100 Subject: [PATCH 2/3] update: removed python files --- .../CryptoHelpers.java | 4 +- .../SecurityCurve25519.kt | 37 ------------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java index cdea976..df6ed86 100644 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java +++ b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/CryptoHelpers.java @@ -85,7 +85,9 @@ public static Mac HMAC256(byte[] data) throws GeneralSecurityException { public static byte[] generateRandomBytes(int length) { SecureRandom random = new SecureRandom(); - byte[] bytes = new byte[length]; + byte[] bytes = new + + byte[length]; random.nextBytes(bytes); return bytes; } diff --git a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt index dd62d59..9382dbb 100644 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt +++ b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityCurve25519.kt @@ -7,43 +7,6 @@ class SecurityCurve25519(val privateKey: ByteArray = Curve25519.generateRandomKe return Curve25519.publicKey(this.privateKey) } - fun agreeWithAuthAndNonce( - authenticationPublicKey: ByteArray, - publicKey: ByteArray, - salt: ByteArray, - nonce1: ByteArray, - nonce2: ByteArray, - info: ByteArray, - ): ByteArray { - val handshakeSalt = nonce1 + nonce2 - val dh1 = Curve25519.sharedSecret(this.privateKey, authenticationPublicKey) - val dh2 = Curve25519.sharedSecret(this.privateKey, publicKey) - var ck = CryptoHelpers.HKDF( - "HMACSHA256", - handshakeSalt, - salt, - info, - 32, - 1 - )[0] - ck = CryptoHelpers.HKDF( - "HMACSHA256", - dh1, - ck, - info, - 32, - 1 - )[0] - return CryptoHelpers.HKDF( - "HMACSHA256", - dh2, - ck, - info, - 32, - 1 - )[0] - } - fun calculateSharedSecret(publicKey: ByteArray): ByteArray { val sharedKey = Curve25519.sharedSecret(this.privateKey, publicKey) return CryptoHelpers.HKDF("HMACSHA256", sharedKey, null, From 1d4745f02cff089ee1ec87b4a95f849c692e5ce9 Mon Sep 17 00:00:00 2001 From: sherlockwisdom Date: Mon, 12 Jan 2026 11:55:30 +0100 Subject: [PATCH 3/3] update: brought back the files --- src/androidTest/java/com/afkanerd/.DS_Store | Bin 0 -> 8196 bytes .../com/afkanerd/smswithoutborders/.DS_Store | Bin 0 -> 6148 bytes .../SecurityRSATest.java | 14 +- .../SecurityX25519Test.kt | 40 +++- .../libsignal/RatchetsTest.kt | 105 ++++++++- .../libsignal/StateTest.kt | 10 +- src/main/java/com/afkanerd/.DS_Store | Bin 0 -> 8196 bytes .../com/afkanerd/smswithoutborders/.DS_Store | Bin 0 -> 6148 bytes .../libsignal_doubleratchet/.DS_Store | Bin 0 -> 8196 bytes .../CryptoHelpers.java | 8 +- .../EncryptionController.kt | 8 +- .../SecurityCurve25519.kt | 105 ++++++++- .../extensions/context.kt | 9 +- .../libsignal/Protocols.java | 79 ++++++- .../libsignal/RatchetsHE.kt | 213 ++++++++++++++++++ .../libsignal/States.java | 165 -------------- .../libsignal/States.kt | 61 +++++ 17 files changed, 613 insertions(+), 204 deletions(-) create mode 100644 src/androidTest/java/com/afkanerd/.DS_Store create mode 100644 src/androidTest/java/com/afkanerd/smswithoutborders/.DS_Store create mode 100644 src/main/java/com/afkanerd/.DS_Store create mode 100644 src/main/java/com/afkanerd/smswithoutborders/.DS_Store create mode 100644 src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/.DS_Store create mode 100644 src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsHE.kt delete mode 100644 src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.java create mode 100644 src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.kt diff --git a/src/androidTest/java/com/afkanerd/.DS_Store b/src/androidTest/java/com/afkanerd/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e4cabc01e52098fdca113b7b1e2b256e9e9e627e GIT binary patch literal 8196 zcmeHMU2GIp6u#fI&{;arDYQ~*2R2=ZfNfb?pye;w{we=Owx!!rSax?tIxss^c4l|M zQq!3DBB1d}<6rd2A0iJbYIxB^QS?#K1Y?ZBX#9E5L|;@MJTrF|Y=MLa0~+Th_nv$1 zx#!+{&YbVwo-JbxU3qmSV|9!%k*Z7ON@}iCT)A7XDS|&zP86iin9dwF$}E;A8kE2T zfd>K)1Re-H5P0C<=mC1O-6G!L-RIJv4LlHd;D6}>_kIXdb!p6}lbm{22Q^*_K-5hE zyh44-13aCuPh&ov`Ulo3e0j^H+usfY-%%_u_a&-o-&fxD1KSP1HJL!eJ z=?pPWgEsI$;DKvB;KHYx*(^)7I3<5iQy!mVD$}U7(?xqMZCGiVeKtL2=(ZD$eh#6m ze8$WQu|ljA_r^!9V%$kNdAlo>+v9TmmSv<%{Zx9lt`(DVO}Aw_Dc#g^o`SAwG-YH^ zH&v_HW#vuP_H^T1Acn+HQm!8#Z(iBDrYXF#ZB2BdDLlTmHQE$j)wX70A|%#ER;=HZ zJY-}|>jXCs{uRMyPc!%A&Dm<$g>G(ZS-fQH7bROmvnr+8%3xw>SQ<&na|>it-4T{1iTvaGdP2|)1;k@n~l_R3)?GaWQr4f}5x7||RT~tv%L+jA)OO!@s zk1OfUY1FE|!ZPJoNVfS%o3vUPFj8sF2+1nnr$bt&3>LWm zL-JNWxKXNA5*aI-B@^usAL~@Qm3{8E+fugEH>R2Hk zdY_cWU8P=CcPvjx-q_KoC`0aYovP~DP+zy8N{K52qWp1H-_(V8LotwCD2!#HHBb>fI2pjI*w0VoP?LPy;i*r2K)zscG zKfGl5id7vOJFkQ%kLOYd{~Z_}8m3^#LxUGB(<1qThfitYq7oplLX<;xfMBE@5nVLP z75?nVVv%4Z&+yr$%ZS}7&*W@FbBjojl?9KL8d}>z1WdWiXPeuJ1!tu78$tw8x!h+L z#)xGe)6me#eXXPFS1MQd-e;vOz}Sl^H7Jo5T&T^!Y1tS;y#W6>_Za!k%o#a zOjtNf2snlbJcvm=MDc%u5bzit$5VJ3&kzb;#7lS?r|>Fa;4Qq3vv?2h;~YNp(eNc< z;Ya*5Exxx;jc@#RbMak>=S;&g576uqQl>6+3%D}AN1dDh_ulyT|66d9V2i*5fd_7v z2TQ(c{yPbWF05h}O* Whk)DlM|kl52k-yYg!;C>|9=7YAf>7R literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..68565d064e7e8f286bb29c4a9f83e012e5578add GIT binary patch literal 6148 zcmeHKyKVw85S&dYkkZgW>0ihnSO~r#9}ohE5~Nd33eaDd-_Gmm`Uq1H>BGJ2FBOrxKki(PD_x884An1A9lOL!$YRxLKk@ z5xbq|7fXj!#~f3ERA8vUqc3M#|JU>@{r`}Zt5hHrxGM!@vU*u9`J||=qsM8jE%Xij qV$8L44pxeZR*JdMQoQ(;S9H$(8rVA;opGZR^G85+NlOL(LxCSaks2KU literal 0 HcmV?d00001 diff --git a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSATest.java b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSATest.java index ec9ebd2..98487f6 100644 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSATest.java +++ b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityRSATest.java @@ -43,12 +43,12 @@ public void testCanStoreAndEncrypt() throws NoSuchAlgorithmException, NoSuchProv // .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); +// 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 index 52a1f89..b8fa7c2 100644 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityX25519Test.kt +++ b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/SecurityX25519Test.kt @@ -1,12 +1,50 @@ package com.afkanerd.smswithoutborders.libsignal_doubleratchet +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties import androidx.test.filters.SmallTest -import junit.framework.TestCase.assertEquals import org.junit.Assert.assertArrayEquals import org.junit.Test +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.Signature @SmallTest class SecurityX25519Test { + @Test + fun keystoreEd25519() { + val keystoreAlias = "keystoreAlias" + val kpg: KeyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, + "AndroidKeyStore" + ) + val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( + keystoreAlias, + KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY + ).run { + setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) + build() + } + + kpg.initialize(parameterSpec) + val kp = kpg.generateKeyPair() + + val ks: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { + load(null) + } + val entry: KeyStore.Entry = ks.getEntry(keystoreAlias, null) + if (entry !is KeyStore.PrivateKeyEntry) { + throw Exception("No instance of keystore") + } + + val data = "Hello world".encodeToByteArray() + val signature: ByteArray = Signature.getInstance("SHA256withECDSA").run { + initSign(entry.privateKey) + update(data) + sign() + } + + } @Test fun sharedSecret() { 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 index 63a8fc3..63c31af 100644 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsTest.kt +++ b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsTest.kt @@ -5,6 +5,7 @@ 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.CryptoHelpers import com.afkanerd.smswithoutborders.libsignal_doubleratchet.SecurityCurve25519 import org.junit.Assert.assertArrayEquals import org.junit.Test @@ -14,6 +15,108 @@ import java.security.SecureRandom class RatchetsTest { var context: Context = InstrumentationRegistry.getInstrumentation().targetContext + @Test + fun completeRatchetHETest() { + val aliceEphemeralKeyPair = SecurityCurve25519() + val aliceEphemeralHeaderKeyPair = SecurityCurve25519() + val aliceEphemeralNextHeaderKeyPair = SecurityCurve25519() + + val bobStaticKeyPair = SecurityCurve25519() + val bobEphemeralKeyPair = SecurityCurve25519() + val bobEphemeralHeaderKeyPair = SecurityCurve25519() + val bobEphemeralNextHeaderKeyPair = SecurityCurve25519() + + val aliceNonce = CryptoHelpers.generateRandomBytes(16) + val bobNonce = CryptoHelpers.generateRandomBytes(16) + + val (aliceSk, aliceSkH, aliceSkNh) = SecurityCurve25519(aliceEphemeralKeyPair.privateKey) + .agreeWithAuthAndNonce( + authenticationPublicKey = bobStaticKeyPair.generateKey(), + authenticationPrivateKey = null, + headerPrivateKey = aliceEphemeralHeaderKeyPair.privateKey, + nextHeaderPrivateKey = aliceEphemeralNextHeaderKeyPair.privateKey, + publicKey = bobEphemeralKeyPair.generateKey(), + headerPublicKey = bobEphemeralHeaderKeyPair.generateKey(), + nextHeaderPublicKey = bobEphemeralNextHeaderKeyPair.generateKey(), + salt = "RelaySMS v1".encodeToByteArray(), + nonce1 = aliceNonce, + nonce2 = bobNonce, + info = "RelaySMS C2S DR v1".encodeToByteArray() + ) + + val (bobSk, bobSkH, bobSkNh) = SecurityCurve25519(bobEphemeralKeyPair.privateKey) + .agreeWithAuthAndNonce( + authenticationPublicKey = null, + authenticationPrivateKey = bobStaticKeyPair.privateKey, + headerPrivateKey = bobEphemeralHeaderKeyPair.privateKey, + nextHeaderPrivateKey = bobEphemeralNextHeaderKeyPair.privateKey, + publicKey = aliceEphemeralKeyPair.generateKey(), + headerPublicKey = aliceEphemeralHeaderKeyPair.generateKey(), + nextHeaderPublicKey = aliceEphemeralNextHeaderKeyPair.generateKey(), + salt = "RelaySMS v1".encodeToByteArray(), + nonce1 = aliceNonce, + nonce2 = bobNonce, + info = "RelaySMS C2S DR v1".encodeToByteArray() + ) + + assertArrayEquals(aliceSk, bobSk) + assertArrayEquals(aliceSkH, bobSkH) + assertArrayEquals(aliceSkNh, bobSkNh) + + val aliceState = States() + RatchetsHE.ratchetInitAlice( + state = aliceState, + SK = aliceSk, + bobDhPublicKey = bobEphemeralKeyPair.generateKey(), + sharedHka = aliceSkH, + sharedNhkb = aliceSkNh + ) + + val bobState = States() + RatchetsHE.ratchetInitBob( + state = bobState, + SK = bobSk, + bobDhPublicKeypair = bobEphemeralKeyPair.getKeypair(), + sharedHka = bobSkH, + sharedNhkb = bobSkNh + ) + + val originalText = SecureRandom.getSeed(32); + val (encHeader, aliceCipherText) = RatchetsHE.ratchetEncrypt( + aliceState, + originalText, + bobStaticKeyPair.generateKey() + ) + + var encHeader1: ByteArray? = null + var aliceCipherText1: ByteArray? = null + for(i in 1..10) { + val (encHeader2, aliceCipherText2) = RatchetsHE.ratchetEncrypt( + aliceState, + originalText, + bobStaticKeyPair.generateKey() + ) + encHeader1 = encHeader2 + aliceCipherText1 = aliceCipherText2 + } + + val bobPlainText = RatchetsHE.ratchetDecrypt( + state = bobState, + encHeader = encHeader, + cipherText = aliceCipherText, + AD = bobStaticKeyPair.generateKey() + ) + + val bobPlainText1 = RatchetsHE.ratchetDecrypt( + state = bobState, + encHeader = encHeader1!!, + cipherText = aliceCipherText1!!, + AD = bobStaticKeyPair.generateKey() + ) + + assertArrayEquals(originalText, bobPlainText) + assertArrayEquals(originalText, bobPlainText1) + } @Test fun completeRatchetTest() { @@ -48,7 +151,7 @@ class RatchetsTest { val bobPlainText1 = Ratchets.ratchetDecrypt(bobState, header1, aliceCipherText1, bob.generateKey()) - println(bobState.serializedStates) + println(bobState.serialize()) 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 index 42e3d84..42c254e 100644 --- a/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/StateTest.kt +++ b/src/androidTest/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/StateTest.kt @@ -2,6 +2,7 @@ package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal import androidx.test.filters.SmallTest import junit.framework.TestCase.assertEquals +import kotlinx.serialization.json.Json import org.junit.Test import java.security.SecureRandom @@ -12,11 +13,8 @@ class StateTest { 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) + val serializedStates = Json.encodeToString(state) + val deserializedStates = Json.decodeFromString(serializedStates) + assertEquals(state, deserializedStates) } } \ 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 0000000000000000000000000000000000000000..fce4432f14a4875ae269202cf8c22cbcca51223b GIT binary patch literal 8196 zcmeHMU2GIp6u#fI&>1?=DYQ~*M>buEfGxD;r{yo%{we=Owx!!rSax?tIxss^c4l|M zQq!3DqM-3f<6rd2A0iJbYIxB^QS?#K1Y?ZBX#9E5L|;@MJTrF|Y=MLa0~+Th_nv$1 zx#!+{&YbVwo-JbxU3smQu{y?>NY$fCB{kP5p4_Wf6(JZYCkoPM%wR4XWj4zb4NK^O z&;y|dLJx!<2tDv`^Z>otUJH09Pk@#G6hu7SKsfxjF+^X9#vikfFfeo%AC9 zbcPtGVH7p~1HtjUcK9e3Z4aaS1`5Z!7 z`HYzrVue^K?v0Py#kiYt^G;VPx5wl9ZQD$j`l2Q--DId<8?(Y0Ai; zVQF@;%g$SxR@7MSRP3#a|>it-it$Kt4q~r zlFnDvEvUb}K~?#LD+;BliG12PoHyK~N>mj6JtAtOJfhLzw%eMwizX_kX&u^qvD&Ea z@g&_jomwqWSgPJ7_d8^RsHFJB<#LPMe~7|EQXD?AQeLeNn5ndGN{Yt!>5$i{g9Yw? zN!iK=H_EkYB4cN>WTGSSu}-yH-REt)E#quX)VMkzDxZk-i43b?5G3fca%70n(x-{W zmho=%-2-3yJ?`jetc8!$bEvX5*2A{3{RF^qc7i?0PO>-HS@r?@jD5v^V86280n9`N zW}y-_sK+8SVKv&(fwkyDA9i9F;uygw3><`mBRGn29K%C+7?0ot9>o)Q7SG{%ynt8m z8eYd~yoq;k4jFCoTyyg!w|9us~=OnuKOyxzH|j2+Pp>H_MZXag*iU#YHDwr zA6dL?#j1{totMLt$8#x!{|*cv4O1}Wqrs1sX_0*1$ESS9!V(~_K$JsvfMBE_5j`|3 z6~XN2B9UOE%m~;eONrg8%;aoC^Ky|Os|Y?TH?*yk2$)J)z&5uN3(m;vH%J6gr95Eo zh!M+trlFyymzFuFi2qdlUtky6_v{yTiQ<0_=AjOEqJ^Tq3!AXRkNY?Vun$S>M;aQk zuwdgbA>bG$@Bk+9AjSW2Lcn8q98ckCJVPjW5ij9ooW!eyfw%BB&fq<~kF)qNK*N`W zg&*A@xn>Wd+uR@KVjuZ9MaiW+1VMzT5P4#qQ0iEQOMyTBM W9|CUGAK~HqAHM%r66#z2{{IDVccrob literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..68565d064e7e8f286bb29c4a9f83e012e5578add GIT binary patch literal 6148 zcmeHKyKVw85S&dYkkZgW>0ihnSO~r#9}ohE5~Nd33eaDd-_Gmm`Uq1H>BGJ2FBOrxKki(PD_x884An1A9lOL!$YRxLKk@ z5xbq|7fXj!#~f3ERA8vUqc3M#|JU>@{r`}Zt5hHrxGM!@vU*u9`J||=qsM8jE%Xij qV$8L44pxeZR*JdMQoQ(;S9H$(8rVA;opGZR^G85+NlOL(LxCSaks2KU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d661d7f9ee81c45fc4021d238049ba5b09d10fbe GIT binary patch literal 8196 zcmeHMU2GIp6u#fIrL%OP1GLg&M>buEfGsTLr{yo%{we=Owx!!rSY~&|c3?VFc4oI= zscB4n5zzRg@h|%150M8I|6Vjv6n#`Q!5CvO8h;)%(HE5m&&-|Wr!8+{YMh(QJ@=e* z&%O7Y`Oe%uEn^IAd2KagHHqQ)Y*P zFalu&!U%*B2qO?iU?xO>?rg7#H@Ww@HtfR)gc0~(M!@?%MCtKpBA^qT`kxN!{1Sj@ zDT)0?W2ysun`l520iEE~H>EXs^?<+>L5TrwPWq@fooFJU6P$8$25!y}?2Mp7fxkQ1 zMg8dv2~NX4j6fKHsS)sM_Xsnoa?dd5`ug1&$(VMAv~OodOv7>G@h>2hl$OmZ7t6&8 z@j!CO9!tJ-bLRSiqe`38oj)0BZ;!_w^0Hal-= zj&Gaf3Na!^(sJ$CSmTD(4bj!>;_Js7qGKDH;tkQYYu1mCN5tybs!e;-N6cZ%KFPg7 z@I_#ACV6*ide<~3Yi8)Ep4l{>n@*vfdtQ<(dj4h6^T_N9X^zsH>g$&V((?QQx!5)0 z8n)%^({m2dv|Q;>D7Iy7Yfn!1^u1Zz%nw^mTAr6x&1~LOUA@&blg9lz$ts;}&Nj_G zwqtn9^rc+=nCt1Ls8aooopam$#nQ5zcl5nFtz{JS4qDMk8MLKmST!jMh@73faPg95 zjZMv)6P??3-FvZ8o;QDiR4w&W%q`cr&(L#ys*}|%&9DZ0Mh(p!>Ma@xYT`wz!H%iE?Fb3Q+iA_qni;~0P4)*5E|j zN$S5+l6yv-n&H~MlA^J@UQznIb^|Pp~uWb#|V8$UbLZvme=S><<95 zP>$KCKou5Y2^z2t&1k^}w4n=ouop=TU%TdKHcwzeUJip9ntsZ^#)R{XRM-A@`!6kJ25BiwSrwC3%YWP}=CnL@#^g z@?dssiAZT9mj&#y<;3ogXK}W!aivHpDhoa<)ite&P$J4D0o&M2EI1==+8m*jluHBl zjs&sHXX@%YI%$~;viMKo{Sv#(eqg_{D}?vCSb!Sbi8!IW4O_9>$9WPxIEXY3Ap;G= zuwdgTA^rr$@c<_9AmRNKA^uT3h9~h9o+i}4fEV!+&fpco{F`_S@8ErWfb;k$K>t^S z{h#pjBs}lB5zqL2dOR1BIm@)ILDCj8;YPz5!Ikh+)OhuO=gt5AKO@c>9wLlD7=c?8 z0hD$mJKAXNn|-UiT02V5A$q*#&70uVH=)ii$BFvoIMJ*BFr;ymrg}DsfKG5q5^DeY W9|ETQvc4rhy2JZFy#HtP{r@) { + val handshakeSalt = nonce1 + nonce2 + val headerInfo = "RelaySMS C2S DRHE v1".encodeToByteArray() + + val rootKey = agreeWithAuthAndNonceImpl( + authenticationPublicKey = authenticationPublicKey, + authenticationPrivateKey = authenticationPrivateKey, + publicKey = publicKey, + salt = salt, + info = info, + handshakeSalt = handshakeSalt, + ) + + val headerKey = agreeWithAuthAndNonceImpl( + authenticationPublicKey = authenticationPublicKey, + authenticationPrivateKey = authenticationPrivateKey, + publicKey = headerPublicKey, + salt = salt, + info = headerInfo, + handshakeSalt = handshakeSalt, + privateKey = headerPrivateKey + ) + + val nextHeaderKey = agreeWithAuthAndNonceImpl( + authenticationPublicKey = authenticationPublicKey, + authenticationPrivateKey = authenticationPrivateKey, + publicKey = nextHeaderPublicKey, + salt = salt, + info = headerInfo, + handshakeSalt = handshakeSalt, + privateKey = nextHeaderPrivateKey + ) + + return Triple(rootKey, headerKey, nextHeaderKey) + } + + fun calculateSharedSecret( + publicKey: ByteArray, + salt: ByteArray? = null, + info: ByteArray? = "x25591_key_exchange".encodeToByteArray(), + ): ByteArray { val sharedKey = Curve25519.sharedSecret(this.privateKey, publicKey) - return CryptoHelpers.HKDF("HMACSHA256", sharedKey, null, - "x25591_key_exchange".encodeToByteArray(), 32, 1)[0] + return CryptoHelpers.HKDF( + "HMACSHA256", + sharedKey, + salt, + info, + 32, + 1 + )[0] } fun getKeypair(): android.util.Pair { 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 index 7b2c70c..fbc7020 100644 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/extensions/context.kt +++ b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/extensions/context.kt @@ -11,20 +11,14 @@ 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") @@ -134,8 +128,7 @@ suspend fun Context.saveBinaryDataEncrypted( @Throws suspend fun Context.getEncryptedBinaryData(keystoreAlias: String): ByteArray? { val keyValue = stringPreferencesKey(keystoreAlias) - val data = dataStore.data.first()[keyValue] - if(data == null) return null + val data = dataStore.data.first()[keyValue] ?: return null val savedBinaryData = Gson().fromJson(data, SavedBinaryData::class.java) 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 index 976ae94..3776adc 100644 --- a/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Protocols.java +++ b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/Protocols.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.security.GeneralSecurityException; +import java.util.ArrayList; import javax.crypto.Mac; @@ -29,14 +30,36 @@ */ public class Protocols { final static int HKDF_LEN = 32; - final static int HKDF_NUM_KEYS = 2; final static String ALGO = "HMACSHA512"; + final static byte[] KDF_RK_HE_INFO = "RelaySMS C2S DR Ratchet v1".getBytes(); 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_HE( + Pair dhPair, + byte[] peerPublicKey, + byte[] info + ) { + SecurityCurve25519 securityCurve25519 = new SecurityCurve25519(dhPair.first); + return securityCurve25519.calculateSharedSecret( + peerPublicKey, + null, + info + ); + } + /** * * @param dhPair This private key (keypair required in Android if supported) @@ -48,12 +71,26 @@ public static Pair GENERATE_DH() { */ public static byte[] DH(Pair dhPair, byte[] peerPublicKey) { SecurityCurve25519 securityCurve25519 = new SecurityCurve25519(dhPair.first); - return securityCurve25519.calculateSharedSecret(peerPublicKey); + return securityCurve25519.calculateSharedSecret( + peerPublicKey, + null, + "x25591_key_exchange".getBytes() + ); + } + + public static byte[][] KDF_RK_HE( + byte[] rk, + byte[] dhOut + ) throws GeneralSecurityException { + int numKeys = 3; + byte[] info = "SMSWithoutBorders DRHE v2".getBytes(); + return CryptoHelpers.HKDF(ALGO, dhOut, rk, info, HKDF_LEN, numKeys); } public static Pair KDF_RK(byte[] rk, byte[] dhOut) throws GeneralSecurityException { + int numKeys = 2; byte[] info = "KDF_RK".getBytes(); - byte[][] hkdfOutput = CryptoHelpers.HKDF(ALGO, dhOut, rk, info, HKDF_LEN, HKDF_NUM_KEYS); + byte[][] hkdfOutput = CryptoHelpers.HKDF(ALGO, dhOut, rk, info, HKDF_LEN, numKeys); return new Pair<>(hkdfOutput[0], hkdfOutput[1]); } @@ -65,6 +102,24 @@ public static Pair KDF_CK(byte[] ck) throws GeneralSecurityExcep return new Pair<>(_ck, mk); } + public static byte[] HENCRYPT( + byte[] mk, + byte[] plainText + ) 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, null, cipherText).doFinal(); + return Bytes.concat(cipherText, mac); + } + public static byte[] ENCRYPT(byte[] mk, byte[] plainText, byte[] associated_data) throws Throwable { byte[] hkdfOutput = getCipherMacParameters(ALGO, mk); byte[] key = new byte[32]; @@ -80,6 +135,20 @@ public static byte[] ENCRYPT(byte[] mk, byte[] plainText, byte[] associated_data return Bytes.concat(cipherText, mac); } + public static byte[] HDECRYPT( + byte[] mk, + byte[] cipherText + ) throws Throwable { + cipherText = verifyCipherText(ALGO, mk, cipherText, null); + + 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[] DECRYPT(byte[] mk, byte[] cipherText, byte[] associated_data) throws Throwable { cipherText = verifyCipherText(ALGO, mk, cipherText, associated_data); @@ -92,6 +161,10 @@ public static byte[] DECRYPT(byte[] mk, byte[] cipherText, byte[] associated_dat return SecurityAES.decryptAES256CBC(cipherText, key, iv); } + public static byte[] CONCAT_HE(byte[] AD, byte[] headers) throws IOException { + return Bytes.concat(AD, headers); + } + 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/RatchetsHE.kt b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsHE.kt new file mode 100644 index 0000000..8d93d95 --- /dev/null +++ b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/RatchetsHE.kt @@ -0,0 +1,213 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import android.util.Pair +import androidx.core.util.component1 +import androidx.core.util.component2 +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols.CONCAT_HE +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols.DECRYPT +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols.ENCRYPT +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols.GENERATE_DH +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols.HDECRYPT +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols.HENCRYPT +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols.KDF_CK +import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Protocols.KDF_RK_HE + +object RatchetsHE { + + const val MAX_SKIP: Int = 100 + + fun ratchetInitAlice( + state: States, + SK: ByteArray, + bobDhPublicKey: ByteArray, + sharedHka: ByteArray, + sharedNhkb: ByteArray, + ) { + state.DHRs = GENERATE_DH() + state.DHRr = bobDhPublicKey + + val kdfRkHEOutputs = KDF_RK_HE(SK, + Protocols.DH_HE( + state.DHRs, + state.DHRr, + Protocols.KDF_RK_HE_INFO + ) + ) + state.RK = kdfRkHEOutputs[0] + state.CKs = kdfRkHEOutputs[1] + state.NHKs = kdfRkHEOutputs[2] + + state.CKr = null + state.Ns = 0 + state.Nr = 0 + state.PN = 0 + state.MKSKIPPED = mutableMapOf() + state.HKs = sharedHka + state.HKr = null + state.NHKr = sharedNhkb + } + + fun ratchetInitBob( + state: States, + SK: ByteArray, + bobDhPublicKeypair: Pair, + sharedHka: ByteArray, + sharedNhkb: ByteArray, + ) { + state.DHRs = bobDhPublicKeypair + state.DHRr = null + state.RK = SK + state.CKs = null + state.CKr = null + state.Ns = 0 + state.Nr = 0 + state.PN = 0 + state.MKSKIPPED = mutableMapOf() + state.HKs = null + state.NHKs = sharedNhkb + state.HKr = null + state.NHKr = sharedHka + } + + fun ratchetEncrypt( + state: States, + plaintext: ByteArray, + AD: ByteArray, + ) : Pair { + val kdfCk = KDF_CK(state.CKs) + state.CKs = kdfCk.first + val mk = kdfCk.second + val header = Headers(state.DHRs, state.PN, state.Ns) + val encHeader = HENCRYPT(state.HKs, header.serialized) + state.Ns += 1 + return Pair(encHeader, + ENCRYPT(mk, plaintext, CONCAT_HE(AD, encHeader))) + } + + 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) + DHRatchetHE(state, header) + } + + skipMessageKeys(state, header.N) + val kdfCk = KDF_CK(state.CKr) + state.CKr = kdfCk.first + val mk = kdfCk.second + state.Nr += 1 + return DECRYPT(mk, cipherText, CONCAT_HE(AD, encHeader)) + } + + private fun skipMessageKeys( + state: States, + until: Int, + ) { + if(state.Nr + MAX_SKIP < until) + throw Exception("MAX_SKIP Exceeded") + + state.CKr?.let{ + while(state.Nr < until) { + val kdfCk = KDF_CK(state.CKr) + state.CKr = kdfCk.first + val mk = kdfCk.second + state.MKSKIPPED[Pair(state.HKr, state.Nr)] = mk + state.Nr += 1 + } + } + } + + private fun trySkippedMessageKeys( + state: States, + encHeader: ByteArray, + ciphertext: ByteArray, + AD: ByteArray + ) : ByteArray? { + state.MKSKIPPED.forEach { + val hk = it.key.first + val n = it.key.second + val mk = it.value + + val header = HDECRYPT(hk, encHeader).run { + Headers.deSerializeHeader(this) + } + if(header != null && header.N == n) { + state.MKSKIPPED.remove(it.key) + return DECRYPT(mk, ciphertext, CONCAT_HE(AD, encHeader)) + } + } + + return null + } + + private fun decryptHeader( + state: States, + encHeader: ByteArray + ) : Pair { + var header: Headers? = null + try { + header = HDECRYPT(state.HKr, encHeader).run { + Headers.deSerializeHeader(this) + } + } catch(e: Exception) { + e.printStackTrace() + } + + header?.let { + return Pair(header, false) + } + + header = HDECRYPT(state.NHKr, encHeader).run { + Headers.deSerializeHeader(this) + } + header?.let { + return Pair(header, true) + } + throw Exception("Generic error decrypting header...") + } + + private fun DHRatchetHE( + state: States, + header: Headers + ) { + state.PN = state.Ns + state.Ns = 0 + state.Nr = 0 + state.HKs = state.NHKs + state.HKr = state.NHKr + state.DHRr = header.dh + + var kdfRkHEOutputs = KDF_RK_HE(state.RK, + Protocols.DH_HE( + state.DHRs, + state.DHRr, + Protocols.KDF_RK_HE_INFO + ) + ) + state.RK = kdfRkHEOutputs[0] + state.CKr = kdfRkHEOutputs[1] + state.NHKr = kdfRkHEOutputs[2] + + state.DHRs = GENERATE_DH() + + kdfRkHEOutputs = KDF_RK_HE(state.RK, + Protocols.DH_HE( + state.DHRs, + state.DHRr, + Protocols.KDF_RK_HE_INFO + ) + ) + state.RK = kdfRkHEOutputs[0] + state.CKs = kdfRkHEOutputs[1] + state.NHKs = kdfRkHEOutputs[2] + } +} \ No newline at end of file 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/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.kt b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.kt new file mode 100644 index 0000000..79817ed --- /dev/null +++ b/src/main/java/com/afkanerd/smswithoutborders/libsignal_doubleratchet/libsignal/States.kt @@ -0,0 +1,61 @@ +package com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal + +import android.util.Pair +import kotlinx.serialization.json.Json + +data class States( + @JvmField + var DHs: Pair? = null, + + @JvmField + var DHr: ByteArray? = null, + + @JvmField + var RK: ByteArray? = null, + + @JvmField + var CKs: ByteArray? = null, + + @JvmField + var CKr: ByteArray? = null, + + @JvmField + var Ns: Int = 0, + + @JvmField + var Nr: Int = 0, + + @JvmField + var PN: Int = 0, + + @JvmField + var DHRs: Pair? = null, + + @JvmField + var DHRr: ByteArray? = null, + + @JvmField + var HKs: ByteArray? = null, + + @JvmField + var HKr: ByteArray? = null, + + @JvmField + var NHKs: ByteArray? = null, + + @JvmField + var NHKr: ByteArray? = null, + + @JvmField + var MKSKIPPED: MutableMap, ByteArray> = mutableMapOf() +) { + fun serialize(): String { + return Json.encodeToString(this) + } + + companion object { + fun deserialize(input: String): States { + return Json.decodeFromString(input) + } + } +} \ No newline at end of file