diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 2d48567afc..b8d8b0e850 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -562,7 +562,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc }); } - public void clearData() { + //FIXME AC: Using this method to cleanup app data is unsafe due to potential concurrent + // activity that still might be using the data that is being deleted here. + // The most reliable and safe way to do this is to use official API call: + // https://developer.android.com/reference/android/app/ActivityManager.html#clearApplicationUserData() + // The downside is it kills the app in the process and there's no any conventional way to start + // another activity when the task is done. + // Dev community is in demand for such a feature, so check on it some time in the feature + // and replace our implementation with the API call when it's safe to do so. + // Here's a feature request related https://issuetracker.google.com/issues/174903931 + public void clearAllData() { String token = TextSecurePreferences.getFCMToken(this); if (token != null && !token.isEmpty()) { LokiPushNotificationManager.unregister(token, this); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index e4d58c761d..2760fdfc9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -31,6 +31,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.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.push.MessageSenderEventListener; @@ -100,8 +101,8 @@ public class SignalCommunicationModule { DatabaseFactory.getSSKDatabase(context), DatabaseFactory.getLokiThreadDatabase(context), DatabaseFactory.getLokiMessageDatabase(context), -// DatabaseFactory.getLokiPreKeyBundleDatabase(context), - null, + null, // DatabaseFactory.getLokiPreKeyBundleDatabase(context) + new SessionProtocolImpl(context), new SessionResetImplementation(context), DatabaseFactory.getLokiUserDatabase(context), DatabaseFactory.getGroupDatabase(context), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 486b05df87..0d997a82e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -63,6 +63,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; @@ -244,7 +245,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/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 4d3fc81959..350208232c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -204,7 +204,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe // Clear all data if this is a secondary device if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) != null) { TextSecurePreferences.setWasUnlinked(this, true) - ApplicationContext.getInstance(this).clearData() + ApplicationContext.getInstance(this).clearAllData() } // Perform chat sessions reset if requested (usually happens after backup restoration). @@ -220,36 +220,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe if (hasViewedSeed || !isMasterDevice) { seedReminderView.visibility = View.GONE } - - // Multi device removal sheet - if (!TextSecurePreferences.getHasSeenMultiDeviceRemovalSheet(this)) { - TextSecurePreferences.setHasSeenMultiDeviceRemovalSheet(this) - val userPublicKey = TextSecurePreferences.getLocalNumber(this) - val deviceLinks = DatabaseFactory.getLokiAPIDatabase(this).getDeviceLinks(userPublicKey) - if (deviceLinks.isNotEmpty()) { - val bottomSheet = MultiDeviceRemovalBottomSheet() - bottomSheet.onOKTapped = { - bottomSheet.dismiss() - } - bottomSheet.onLinkTapped = { - bottomSheet.dismiss() - val url = "https://getsession.org/faq" - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(intent) - } - bottomSheet.show(supportFragmentManager, bottomSheet.tag) - return - } - } - - // Light theme introduction sheet - if (!TextSecurePreferences.hasSeenLightThemeIntroSheet(this) && - UiModeUtilities.isDayUiMode(this)) { - TextSecurePreferences.setHasSeenLightThemeIntroSheet(this) - val bottomSheet = LightThemeFeatureIntroBottomSheet() - bottomSheet.show(supportFragmentManager, bottomSheet.tag) - return - } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt new file mode 100644 index 0000000000..2009132adf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt @@ -0,0 +1,99 @@ +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 org.session.libsignal.libsignal.util.Hex +import org.session.libsignal.service.api.messages.SignalServiceEnvelope +import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE +import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE +import org.session.libsignal.service.loki.api.crypto.SessionProtocol +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded +import org.session.libsignal.service.loki.utilities.toHexString +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 + +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/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt index 50e54567b9..fb0d4ad151 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt @@ -27,13 +27,13 @@ class ClearAllDataDialog : DialogFragment() { private fun clearAllData() { if (KeyPairUtilities.hasV2KeyPair(requireContext())) { - ApplicationContext.getInstance(context).clearData() + ApplicationContext.getInstance(context).clearAllData() } else { val dialog = AlertDialog.Builder(requireContext()) val message = "We’ve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID." dialog.setMessage(message) dialog.setPositiveButton("Yes") { _, _ -> - ApplicationContext.getInstance(context).clearData() + ApplicationContext.getInstance(context).clearAllData() } dialog.setNegativeButton("Cancel") { _, _ -> // Do nothing diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt index de310dbd6b..17cddd6ae5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.utilities import android.content.Context 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 @@ -44,6 +45,13 @@ object KeyPairUtilities { 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) + } data class KeyPairGenerationResult( val seed: ByteArray, diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java index dc15a313d0..cfa1c9bc31 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java @@ -77,6 +77,7 @@ import org.session.libsignal.service.loki.api.LokiDotNetAPI; import org.session.libsignal.service.loki.api.PushNotificationAPI; import org.session.libsignal.service.loki.api.SignalMessageInfo; import org.session.libsignal.service.loki.api.SnodeAPI; +import org.session.libsignal.service.loki.api.crypto.SessionProtocol; import org.session.libsignal.service.loki.api.fileserver.FileServerAPI; import org.session.libsignal.service.loki.api.opengroups.PublicChat; import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI; @@ -142,6 +143,7 @@ public class SignalServiceMessageSender { private final LokiThreadDatabaseProtocol threadDatabase; private final LokiMessageDatabaseProtocol messageDatabase; private final LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase; + private final SessionProtocol sessionProtocolImpl; private final SessionResetProtocol sessionResetImpl; private final LokiUserDatabaseProtocol userDatabase; private final LokiOpenGroupDatabaseProtocol openGroupDatabase; @@ -171,12 +173,13 @@ public class SignalServiceMessageSender { LokiThreadDatabaseProtocol threadDatabase, LokiMessageDatabaseProtocol messageDatabase, LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase, + SessionProtocol sessionProtocolImpl, SessionResetProtocol sessionResetImpl, LokiUserDatabaseProtocol userDatabase, LokiOpenGroupDatabaseProtocol openGroupDatabase, Broadcaster broadcaster) { - this(urls, new StaticCredentialsProvider(user, password, null), store, userAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, userPublicKey, apiDatabase, sskDatabase, threadDatabase, messageDatabase, preKeyBundleDatabase, sessionResetImpl, userDatabase, openGroupDatabase, broadcaster); + this(urls, new StaticCredentialsProvider(user, password, null), store, userAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, userPublicKey, apiDatabase, sskDatabase, threadDatabase, messageDatabase, preKeyBundleDatabase, sessionProtocolImpl, sessionResetImpl, userDatabase, openGroupDatabase, broadcaster); } public SignalServiceMessageSender(SignalServiceConfiguration urls, @@ -193,6 +196,7 @@ public class SignalServiceMessageSender { LokiThreadDatabaseProtocol threadDatabase, LokiMessageDatabaseProtocol messageDatabase, LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase, + SessionProtocol sessionProtocolImpl, SessionResetProtocol sessionResetImpl, LokiUserDatabaseProtocol userDatabase, LokiOpenGroupDatabaseProtocol openGroupDatabase, @@ -211,6 +215,7 @@ public class SignalServiceMessageSender { this.threadDatabase = threadDatabase; this.messageDatabase = messageDatabase; this.preKeyBundleDatabase = preKeyBundleDatabase; + this.sessionProtocolImpl = sessionProtocolImpl; this.sessionResetImpl = sessionResetImpl; this.userDatabase = userDatabase; this.openGroupDatabase = openGroupDatabase; @@ -1383,6 +1388,30 @@ public class SignalServiceMessageSender { return new OutgoingPushMessageList(recipient.getNumber(), timestamp, messages, online); } + private OutgoingPushMessageList getSessionProtocolEncryptedMessages(PushServiceSocket socket, + SignalServiceAddress recipient, + Optional unidentifiedAccess, + long timestamp, + byte[] plaintext, + boolean online, + boolean useFallbackEncryption, + boolean isClosedGroup) + { + List messages = new LinkedList<>(); + + PushTransportDetails transportDetails = new PushTransportDetails(3); + String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group + byte[] ciphertext = sessionProtocolImpl.encrypt(transportDetails.getPaddedMessageBody(plaintext), publicKey); + String body = Base64.encodeBytes(ciphertext); + boolean isSSKBasedClosedGroup = sskDatabase.isSSKBasedClosedGroup(publicKey); + int type = isSSKBasedClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE : + SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE; + OutgoingPushMessage message = new OutgoingPushMessage(type, 1, 0, body); + messages.add(message); + + return new OutgoingPushMessageList(publicKey, timestamp, messages, online); + } + private OutgoingPushMessage getUnencryptedMessage(byte[] plaintext) { Log.d("Loki", "Bypassing cipher and preparing a plaintext message."); int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID; @@ -1417,7 +1446,7 @@ public class SignalServiceMessageSender { { int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID; SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(groupPublicKey, deviceID); - SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sskDatabase, sessionResetImpl, null); + SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sskDatabase, sessionResetImpl, sessionProtocolImpl, null); try { return cipher.encrypt(signalProtocolAddress, Optional.of(unidentifiedAccess), plaintext); } catch (org.session.libsignal.libsignal.UntrustedIdentityException e) { @@ -1435,7 +1464,7 @@ public class SignalServiceMessageSender { Log.d("Loki", "Using Signal cipher."); int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID; SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getNumber(), deviceID); - SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sskDatabase, sessionResetImpl, null); + SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sskDatabase, sessionResetImpl, sessionProtocolImpl, null); try { String contactPublicKey = recipient.getNumber(); diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java index 06da05fd33..0850f5dd42 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java @@ -83,6 +83,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos.SyncMessa import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.Verified; import org.session.libsignal.service.internal.util.Base64; +import org.session.libsignal.service.loki.api.crypto.SessionProtocol; import org.session.libsignal.service.loki.api.opengroups.PublicChat; import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupUtilities; import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol; @@ -114,18 +115,21 @@ public class SignalServiceCipher { private final SessionResetProtocol sessionResetProtocol; private final SharedSenderKeysDatabaseProtocol sskDatabase; private final SignalServiceAddress localAddress; + private final SessionProtocol sessionProtocolImpl; private final CertificateValidator certificateValidator; public SignalServiceCipher(SignalServiceAddress localAddress, SignalProtocolStore signalProtocolStore, SharedSenderKeysDatabaseProtocol sskDatabase, SessionResetProtocol sessionResetProtocol, + SessionProtocol sessionProtocolImpl, CertificateValidator certificateValidator) { this.signalProtocolStore = signalProtocolStore; this.sessionResetProtocol = sessionResetProtocol; this.sskDatabase = sskDatabase; this.localAddress = localAddress; + this.sessionProtocolImpl = sessionProtocolImpl; this.certificateValidator = certificateValidator; } @@ -317,12 +321,23 @@ public class SignalServiceCipher { int sessionVersion; if (envelope.isClosedGroupCiphertext()) { - Pair plaintextAndSenderPublicKey = ClosedGroupUtilities.decrypt(envelope); - String senderPublicKey = plaintextAndSenderPublicKey.second(); - if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob - paddedMessage = plaintextAndSenderPublicKey.first(); - metadata = new Metadata(senderPublicKey, envelope.getSourceDevice(), envelope.getTimestamp(), false); - sessionVersion = sessionCipher.getSessionVersion(); + try { + // Try the Session protocol + kotlin.Pair plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(envelope); + paddedMessage = plaintextAndSenderPublicKey.getFirst(); + String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); + if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob + metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); + sessionVersion = sessionCipher.getSessionVersion(); + } catch (Exception exception) { + // Fall back on shared sender keys + Pair plaintextAndSenderPublicKey = ClosedGroupUtilities.decrypt(envelope); + String senderPublicKey = plaintextAndSenderPublicKey.second(); + if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob + paddedMessage = plaintextAndSenderPublicKey.first(); + metadata = new Metadata(senderPublicKey, envelope.getSourceDevice(), envelope.getTimestamp(), false); + sessionVersion = sessionCipher.getSessionVersion(); + } } else if (envelope.isPreKeySignalMessage()) { paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext)); metadata = new Metadata(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), false); @@ -332,11 +347,21 @@ public class SignalServiceCipher { metadata = new Metadata(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), false); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isUnidentifiedSender()) { - Pair> results = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp(), envelope.getSource()); - Pair data = results.second(); - paddedMessage = data.second(); - metadata = new Metadata(results.first().getName(), results.first().getDeviceId(), envelope.getTimestamp(), false); - sessionVersion = sealedSessionCipher.getSessionVersion(new SignalProtocolAddress(metadata.getSender(), metadata.getSenderDevice())); + try { + // Try the Session protocol + kotlin.Pair plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(envelope); + paddedMessage = plaintextAndSenderPublicKey.getFirst(); + String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); + metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); + sessionVersion = sealedSessionCipher.getSessionVersion(new SignalProtocolAddress(metadata.getSender(), metadata.getSenderDevice())); + } catch (Exception exception) { + // Fall back on the Signal protocol + Pair> results = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp(), envelope.getSource()); + Pair data = results.second(); + paddedMessage = data.second(); + metadata = new Metadata(results.first().getName(), results.first().getDeviceId(), envelope.getTimestamp(), false); + sessionVersion = sealedSessionCipher.getSessionVersion(new SignalProtocolAddress(metadata.getSender(), metadata.getSenderDevice())); + } } else { throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); } diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/SessionProtocol.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/SessionProtocol.kt new file mode 100644 index 0000000000..21a6cd26e4 --- /dev/null +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/SessionProtocol.kt @@ -0,0 +1,40 @@ +package org.session.libsignal.service.loki.api.crypto + +import org.session.libsignal.service.api.messages.SignalServiceEnvelope + +interface SessionProtocol { + + sealed class Exception(val description: String) : kotlin.Exception(description) { + // Encryption + object NoUserED25519KeyPair : Exception("Couldn't find user ED25519 key pair.") + object SigningFailed : Exception("Couldn't sign message.") + object EncryptionFailed : Exception("Couldn't encrypt message.") + // Decryption + object NoData : Exception("Received an empty envelope.") + object InvalidGroupPublicKey : Exception("Invalid group public key.") + object NoGroupPrivateKey : Exception("Missing group private key.") + object DecryptionFailed : Exception("Couldn't decrypt message.") + object InvalidSignature : Exception("Invalid message signature.") + } + + /** + * Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`. + * + * @param plaintext the plaintext to encrypt. Must already be padded. + * @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group. + * + * @return the encrypted message. + */ + fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray + + /** + * Decrypts `envelope.content` using the Session protocol. If the envelope type is `UNIDENTIFIED_SENDER` the message is assumed to be a one-to-one + * message. If the envelope type is `CLOSED_GROUP_CIPHERTEXT` the message is assumed to be a closed group message. In the latter case `envelope.source` + * must be set to the closed group's public key. + * + * @param envelope the envelope for which to decrypt the content. + * + * @return the padded plaintext. + */ + fun decrypt(envelope: SignalServiceEnvelope): Pair +} \ No newline at end of file diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/crypto/LokiServiceCipher.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/crypto/LokiServiceCipher.kt index e48d04da6b..f6e63127d8 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/crypto/LokiServiceCipher.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/crypto/LokiServiceCipher.kt @@ -9,15 +9,23 @@ import org.session.libsignal.service.api.crypto.SignalServiceCipher import org.session.libsignal.service.api.messages.SignalServiceEnvelope import org.session.libsignal.service.api.push.SignalServiceAddress import org.session.libsignal.service.internal.push.PushTransportDetails +import org.session.libsignal.service.loki.api.crypto.SessionProtocol import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol class LokiServiceCipher( localAddress: SignalServiceAddress, private val signalProtocolStore: SignalProtocolStore, private val sskDatabase: SharedSenderKeysDatabaseProtocol, + sessionProtocolImpl: SessionProtocol, sessionResetProtocol: SessionResetProtocol, certificateValidator: CertificateValidator?) - : SignalServiceCipher(localAddress, signalProtocolStore, sskDatabase, sessionResetProtocol, certificateValidator) { + : SignalServiceCipher( + localAddress, + signalProtocolStore, + sskDatabase, + sessionResetProtocol, + sessionProtocolImpl, + certificateValidator) { private val userPrivateKey get() = signalProtocolStore.identityKeyPair.privateKey.serialize()