From 48477a72ba0301478ea070d6e47b0d8870ffe5ed Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 14 Dec 2020 13:06:46 +1100 Subject: [PATCH 1/3] Implement Session protocol --- .../SignalCommunicationModule.java | 2 + .../securesms/jobs/PushDecryptJob.java | 3 +- .../securesms/loki/api/SessionProtocol.kt | 101 ++++++++++++++++++ .../loki/database/LokiAPIDatabase.kt | 5 + .../loki/utilities/KeyPairUtilities.kt | 11 ++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/org/thoughtcrime/securesms/loki/api/SessionProtocol.kt diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index a7449ca58d..b9dd7e675f 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; @@ -157,6 +158,7 @@ public class SignalCommunicationModule { DatabaseFactory.getLokiThreadDatabase(context), DatabaseFactory.getLokiMessageDatabase(context), DatabaseFactory.getLokiPreKeyBundleDatabase(context), + new SessionProtocolImpl(context), new SessionResetImplementation(context), DatabaseFactory.getLokiUserDatabase(context), DatabaseFactory.getGroupDatabase(context), diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 900ba496af..56bf2f2f16 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.activities.HomeActivity; +import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl; import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; @@ -259,7 +260,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context); SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context)); - LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator()); + LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), new SessionProtocolImpl(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator()); SignalServiceContent content = cipher.decrypt(envelope); diff --git a/src/org/thoughtcrime/securesms/loki/api/SessionProtocol.kt b/src/org/thoughtcrime/securesms/loki/api/SessionProtocol.kt new file mode 100644 index 0000000000..bc6ff3b029 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/api/SessionProtocol.kt @@ -0,0 +1,101 @@ +package org.thoughtcrime.securesms.loki.api + +import android.content.Context +import android.util.Log +import com.goterl.lazycode.lazysodium.LazySodiumAndroid +import com.goterl.lazycode.lazysodium.SodiumAndroid +import com.goterl.lazycode.lazysodium.interfaces.Box +import com.goterl.lazycode.lazysodium.interfaces.Sign +import com.goterl.lazycode.lazysodium.utils.KeyPair +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.libsignal.util.Hex +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE +import org.whispersystems.signalservice.loki.api.SnodeAPI +import org.whispersystems.signalservice.loki.api.crypto.SessionProtocol +import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded +import org.whispersystems.signalservice.loki.utilities.toHexString + +class SessionProtocolImpl(private val context: Context) : SessionProtocol { + + override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray { + val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair + val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded()) + val sodium = LazySodiumAndroid(SodiumAndroid()) + + val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey + val signature = ByteArray(Sign.BYTES) + try { + sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) + } catch (exception: Exception) { + Log.d("Loki", "Couldn't sign message due to error: $exception.") + throw SessionProtocol.Exception.SigningFailed + } + val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature + val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES) + try { + sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey) + } catch (exception: Exception) { + Log.d("Loki", "Couldn't encrypt message due to error: $exception.") + throw SessionProtocol.Exception.EncryptionFailed + } + + return ciphertext + } + + override fun decrypt(envelope: SignalServiceEnvelope): Pair { + val ciphertext = envelope.content ?: throw SessionProtocol.Exception.NoData + val recipientX25519PrivateKey: ByteArray + val recipientX25519PublicKey: ByteArray + when (envelope.type) { + UNIDENTIFIED_SENDER_VALUE -> { + recipientX25519PrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() + recipientX25519PublicKey = Hex.fromStringCondensed(TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()) + } + CLOSED_GROUP_CIPHERTEXT_VALUE -> { + val hexEncodedGroupPublicKey = envelope.source + val sskDB = DatabaseFactory.getSSKDatabase(context) + if (!sskDB.isSSKBasedClosedGroup(hexEncodedGroupPublicKey)) { throw SessionProtocol.Exception.InvalidGroupPublicKey } + val hexEncodedGroupPrivateKey = sskDB.getClosedGroupPrivateKey(hexEncodedGroupPublicKey) ?: throw SessionProtocol.Exception.NoGroupPrivateKey + recipientX25519PrivateKey = Hex.fromStringCondensed(hexEncodedGroupPrivateKey) + recipientX25519PublicKey = Hex.fromStringCondensed(hexEncodedGroupPublicKey.removing05PrefixIfNeeded()) + } + else -> throw AssertionError() + } + val sodium = LazySodiumAndroid(SodiumAndroid()) + val signatureSize = Sign.BYTES + val ed25519PublicKeySize = Sign.PUBLICKEYBYTES + + // 1. ) Decrypt the message + val plaintextWithMetadata = ByteArray(ciphertext.size - Box.SEALBYTES) + try { + sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey) + } catch (exception: Exception) { + Log.d("Loki", "Couldn't decrypt message due to error: $exception.") + throw SessionProtocol.Exception.DecryptionFailed + } + if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw SessionProtocol.Exception.DecryptionFailed } + // 2. ) Get the message parts + val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size) + val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize) + val plaintext = plaintextWithMetadata.sliceArray(0 until plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize)) + // 3. ) Verify the signature + val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey) + try { + val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey) + if (!isValid) { throw SessionProtocol.Exception.InvalidSignature } + } catch (exception: Exception) { + Log.d("Loki", "Couldn't verify message signature due to error: $exception.") + throw SessionProtocol.Exception.InvalidSignature + } + // 4. ) Get the sender's X25519 public key + val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES) + sodium.convertPublicKeyEd25519ToCurve25519(senderX25519PublicKey, senderED25519PublicKey) + + return Pair(plaintext, "05" + senderX25519PublicKey.toHexString()) + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index 0b7cb3228d..706a5d45ba 100644 --- a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -3,13 +3,18 @@ package org.thoughtcrime.securesms.loki.database import android.content.ContentValues import android.content.Context import android.util.Log +import com.goterl.lazycode.lazysodium.utils.Key +import com.goterl.lazycode.lazysodium.utils.KeyPair +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope import org.whispersystems.signalservice.loki.api.Snode import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.DeviceLink +import org.whispersystems.signalservice.loki.utilities.toHexString import java.util.* class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol { diff --git a/src/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt b/src/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt index 31038c251f..ff7f0fd37b 100644 --- a/src/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.loki.utilities import android.content.Context +import android.util.Log import com.goterl.lazycode.lazysodium.LazySodiumAndroid import com.goterl.lazycode.lazysodium.SodiumAndroid +import com.goterl.lazycode.lazysodium.utils.Key import com.goterl.lazycode.lazysodium.utils.KeyPair import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.util.Base64 @@ -11,6 +13,7 @@ import org.whispersystems.curve25519.Curve25519 import org.whispersystems.libsignal.ecc.DjbECPrivateKey import org.whispersystems.libsignal.ecc.DjbECPublicKey import org.whispersystems.libsignal.ecc.ECKeyPair +import org.whispersystems.signalservice.loki.utilities.toHexString object KeyPairUtilities { @@ -49,4 +52,12 @@ object KeyPairUtilities { fun hasV2KeyPair(context: Context): Boolean { return (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) != null) } + + fun getUserED25519KeyPair(context: Context): KeyPair? { + val hexEncodedED25519PublicKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_PUBLIC_KEY) ?: return null + val hexEncodedED25519SecretKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) ?: return null + val ed25519PublicKey = Key.fromBase64String(hexEncodedED25519PublicKey) + val ed25519SecretKey = Key.fromBase64String(hexEncodedED25519SecretKey) + return KeyPair(ed25519PublicKey, ed25519SecretKey) + } } \ No newline at end of file From 3f6daf22265528d833b8ca7294572045b438fdc3 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 16 Dec 2020 13:55:05 +1100 Subject: [PATCH 2/3] Recommend that users migrate --- ...agment_key_pair_migration_bottom_sheet.xml | 59 +++++++++++++++++++ .../securesms/ApplicationContext.java | 2 +- .../securesms/loki/activities/HomeActivity.kt | 51 ++++------------ ...sionProtocol.kt => SessionProtocolImpl.kt} | 0 .../loki/dialogs/ClearAllDataDialog.kt | 4 +- .../dialogs/KeyPairMigrationBottomSheet.kt | 53 +++++++++++++++++ .../protocol/shelved/MultiDeviceProtocol.kt | 2 +- .../securesms/util/TextSecurePreferences.java | 8 +++ 8 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 res/layout/fragment_key_pair_migration_bottom_sheet.xml rename src/org/thoughtcrime/securesms/loki/api/{SessionProtocol.kt => SessionProtocolImpl.kt} (100%) create mode 100644 src/org/thoughtcrime/securesms/loki/dialogs/KeyPairMigrationBottomSheet.kt diff --git a/res/layout/fragment_key_pair_migration_bottom_sheet.xml b/res/layout/fragment_key_pair_migration_bottom_sheet.xml new file mode 100644 index 0000000000..4ff5e57327 --- /dev/null +++ b/res/layout/fragment_key_pair_migration_bottom_sheet.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + +