Session encryption protocol.

Changes from #390 and #101 (service repo)
This commit is contained in:
Anton Chekulaev 2020-12-18 13:02:33 +11:00
parent 9002606917
commit e33b8a5fa2
11 changed files with 242 additions and 52 deletions

View File

@ -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); String token = TextSecurePreferences.getFCMToken(this);
if (token != null && !token.isEmpty()) { if (token != null && !token.isEmpty()) {
LokiPushNotificationManager.unregister(token, this); LokiPushNotificationManager.unregister(token, this);

View File

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.logging.Log; 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.SessionResetImplementation;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.push.MessageSenderEventListener; import org.thoughtcrime.securesms.push.MessageSenderEventListener;
@ -100,8 +101,8 @@ public class SignalCommunicationModule {
DatabaseFactory.getSSKDatabase(context), DatabaseFactory.getSSKDatabase(context),
DatabaseFactory.getLokiThreadDatabase(context), DatabaseFactory.getLokiThreadDatabase(context),
DatabaseFactory.getLokiMessageDatabase(context), DatabaseFactory.getLokiMessageDatabase(context),
// DatabaseFactory.getLokiPreKeyBundleDatabase(context), null, // DatabaseFactory.getLokiPreKeyBundleDatabase(context)
null, new SessionProtocolImpl(context),
new SessionResetImplementation(context), new SessionResetImplementation(context),
DatabaseFactory.getLokiUserDatabase(context), DatabaseFactory.getLokiUserDatabase(context),
DatabaseFactory.getGroupDatabase(context), DatabaseFactory.getGroupDatabase(context),

View File

@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.activities.HomeActivity; 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.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
@ -244,7 +245,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context); SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context);
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(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); SignalServiceContent content = cipher.decrypt(envelope);

View File

@ -204,7 +204,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
// Clear all data if this is a secondary device // Clear all data if this is a secondary device
if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) != null) { if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) != null) {
TextSecurePreferences.setWasUnlinked(this, true) TextSecurePreferences.setWasUnlinked(this, true)
ApplicationContext.getInstance(this).clearData() ApplicationContext.getInstance(this).clearAllData()
} }
// Perform chat sessions reset if requested (usually happens after backup restoration). // Perform chat sessions reset if requested (usually happens after backup restoration).
@ -220,36 +220,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
if (hasViewedSeed || !isMasterDevice) { if (hasViewedSeed || !isMasterDevice) {
seedReminderView.visibility = View.GONE 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View File

@ -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<ByteArray, String> {
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())
}
}

View File

@ -27,13 +27,13 @@ class ClearAllDataDialog : DialogFragment() {
private fun clearAllData() { private fun clearAllData() {
if (KeyPairUtilities.hasV2KeyPair(requireContext())) { if (KeyPairUtilities.hasV2KeyPair(requireContext())) {
ApplicationContext.getInstance(context).clearData() ApplicationContext.getInstance(context).clearAllData()
} else { } else {
val dialog = AlertDialog.Builder(requireContext()) val dialog = AlertDialog.Builder(requireContext())
val message = "Weve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID." val message = "Weve upgraded the way Session IDs are generated, so you will be unable to restore your current Session ID."
dialog.setMessage(message) dialog.setMessage(message)
dialog.setPositiveButton("Yes") { _, _ -> dialog.setPositiveButton("Yes") { _, _ ->
ApplicationContext.getInstance(context).clearData() ApplicationContext.getInstance(context).clearAllData()
} }
dialog.setNegativeButton("Cancel") { _, _ -> dialog.setNegativeButton("Cancel") { _, _ ->
// Do nothing // Do nothing

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.utilities
import android.content.Context import android.content.Context
import com.goterl.lazycode.lazysodium.LazySodiumAndroid import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid import com.goterl.lazycode.lazysodium.SodiumAndroid
import com.goterl.lazycode.lazysodium.utils.Key
import com.goterl.lazycode.lazysodium.utils.KeyPair import com.goterl.lazycode.lazysodium.utils.KeyPair
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.Base64
@ -44,6 +45,13 @@ object KeyPairUtilities {
return (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) != null) 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( data class KeyPairGenerationResult(
val seed: ByteArray, val seed: ByteArray,

View File

@ -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.PushNotificationAPI;
import org.session.libsignal.service.loki.api.SignalMessageInfo; import org.session.libsignal.service.loki.api.SignalMessageInfo;
import org.session.libsignal.service.loki.api.SnodeAPI; 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.fileserver.FileServerAPI;
import org.session.libsignal.service.loki.api.opengroups.PublicChat; import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI; import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
@ -142,6 +143,7 @@ public class SignalServiceMessageSender {
private final LokiThreadDatabaseProtocol threadDatabase; private final LokiThreadDatabaseProtocol threadDatabase;
private final LokiMessageDatabaseProtocol messageDatabase; private final LokiMessageDatabaseProtocol messageDatabase;
private final LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase; private final LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase;
private final SessionProtocol sessionProtocolImpl;
private final SessionResetProtocol sessionResetImpl; private final SessionResetProtocol sessionResetImpl;
private final LokiUserDatabaseProtocol userDatabase; private final LokiUserDatabaseProtocol userDatabase;
private final LokiOpenGroupDatabaseProtocol openGroupDatabase; private final LokiOpenGroupDatabaseProtocol openGroupDatabase;
@ -171,12 +173,13 @@ public class SignalServiceMessageSender {
LokiThreadDatabaseProtocol threadDatabase, LokiThreadDatabaseProtocol threadDatabase,
LokiMessageDatabaseProtocol messageDatabase, LokiMessageDatabaseProtocol messageDatabase,
LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase, LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase,
SessionProtocol sessionProtocolImpl,
SessionResetProtocol sessionResetImpl, SessionResetProtocol sessionResetImpl,
LokiUserDatabaseProtocol userDatabase, LokiUserDatabaseProtocol userDatabase,
LokiOpenGroupDatabaseProtocol openGroupDatabase, LokiOpenGroupDatabaseProtocol openGroupDatabase,
Broadcaster broadcaster) 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, public SignalServiceMessageSender(SignalServiceConfiguration urls,
@ -193,6 +196,7 @@ public class SignalServiceMessageSender {
LokiThreadDatabaseProtocol threadDatabase, LokiThreadDatabaseProtocol threadDatabase,
LokiMessageDatabaseProtocol messageDatabase, LokiMessageDatabaseProtocol messageDatabase,
LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase, LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase,
SessionProtocol sessionProtocolImpl,
SessionResetProtocol sessionResetImpl, SessionResetProtocol sessionResetImpl,
LokiUserDatabaseProtocol userDatabase, LokiUserDatabaseProtocol userDatabase,
LokiOpenGroupDatabaseProtocol openGroupDatabase, LokiOpenGroupDatabaseProtocol openGroupDatabase,
@ -211,6 +215,7 @@ public class SignalServiceMessageSender {
this.threadDatabase = threadDatabase; this.threadDatabase = threadDatabase;
this.messageDatabase = messageDatabase; this.messageDatabase = messageDatabase;
this.preKeyBundleDatabase = preKeyBundleDatabase; this.preKeyBundleDatabase = preKeyBundleDatabase;
this.sessionProtocolImpl = sessionProtocolImpl;
this.sessionResetImpl = sessionResetImpl; this.sessionResetImpl = sessionResetImpl;
this.userDatabase = userDatabase; this.userDatabase = userDatabase;
this.openGroupDatabase = openGroupDatabase; this.openGroupDatabase = openGroupDatabase;
@ -1383,6 +1388,30 @@ public class SignalServiceMessageSender {
return new OutgoingPushMessageList(recipient.getNumber(), timestamp, messages, online); return new OutgoingPushMessageList(recipient.getNumber(), timestamp, messages, online);
} }
private OutgoingPushMessageList getSessionProtocolEncryptedMessages(PushServiceSocket socket,
SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp,
byte[] plaintext,
boolean online,
boolean useFallbackEncryption,
boolean isClosedGroup)
{
List<OutgoingPushMessage> 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) { private OutgoingPushMessage getUnencryptedMessage(byte[] plaintext) {
Log.d("Loki", "Bypassing cipher and preparing a plaintext message."); Log.d("Loki", "Bypassing cipher and preparing a plaintext message.");
int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID; int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID;
@ -1417,7 +1446,7 @@ public class SignalServiceMessageSender {
{ {
int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID; int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID;
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(groupPublicKey, deviceID); 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 { try {
return cipher.encrypt(signalProtocolAddress, Optional.of(unidentifiedAccess), plaintext); return cipher.encrypt(signalProtocolAddress, Optional.of(unidentifiedAccess), plaintext);
} catch (org.session.libsignal.libsignal.UntrustedIdentityException e) { } catch (org.session.libsignal.libsignal.UntrustedIdentityException e) {
@ -1435,7 +1464,7 @@ public class SignalServiceMessageSender {
Log.d("Loki", "Using Signal cipher."); Log.d("Loki", "Using Signal cipher.");
int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID; int deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID;
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getNumber(), deviceID); 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 { try {
String contactPublicKey = recipient.getNumber(); String contactPublicKey = recipient.getNumber();

View File

@ -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.TypingMessage;
import org.session.libsignal.service.internal.push.SignalServiceProtos.Verified; import org.session.libsignal.service.internal.push.SignalServiceProtos.Verified;
import org.session.libsignal.service.internal.util.Base64; 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.api.opengroups.PublicChat;
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupUtilities; import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupUtilities;
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol; import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol;
@ -114,18 +115,21 @@ public class SignalServiceCipher {
private final SessionResetProtocol sessionResetProtocol; private final SessionResetProtocol sessionResetProtocol;
private final SharedSenderKeysDatabaseProtocol sskDatabase; private final SharedSenderKeysDatabaseProtocol sskDatabase;
private final SignalServiceAddress localAddress; private final SignalServiceAddress localAddress;
private final SessionProtocol sessionProtocolImpl;
private final CertificateValidator certificateValidator; private final CertificateValidator certificateValidator;
public SignalServiceCipher(SignalServiceAddress localAddress, public SignalServiceCipher(SignalServiceAddress localAddress,
SignalProtocolStore signalProtocolStore, SignalProtocolStore signalProtocolStore,
SharedSenderKeysDatabaseProtocol sskDatabase, SharedSenderKeysDatabaseProtocol sskDatabase,
SessionResetProtocol sessionResetProtocol, SessionResetProtocol sessionResetProtocol,
SessionProtocol sessionProtocolImpl,
CertificateValidator certificateValidator) CertificateValidator certificateValidator)
{ {
this.signalProtocolStore = signalProtocolStore; this.signalProtocolStore = signalProtocolStore;
this.sessionResetProtocol = sessionResetProtocol; this.sessionResetProtocol = sessionResetProtocol;
this.sskDatabase = sskDatabase; this.sskDatabase = sskDatabase;
this.localAddress = localAddress; this.localAddress = localAddress;
this.sessionProtocolImpl = sessionProtocolImpl;
this.certificateValidator = certificateValidator; this.certificateValidator = certificateValidator;
} }
@ -317,12 +321,23 @@ public class SignalServiceCipher {
int sessionVersion; int sessionVersion;
if (envelope.isClosedGroupCiphertext()) { if (envelope.isClosedGroupCiphertext()) {
Pair<byte[], String> plaintextAndSenderPublicKey = ClosedGroupUtilities.decrypt(envelope); try {
String senderPublicKey = plaintextAndSenderPublicKey.second(); // Try the Session protocol
if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(envelope);
paddedMessage = plaintextAndSenderPublicKey.first(); paddedMessage = plaintextAndSenderPublicKey.getFirst();
metadata = new Metadata(senderPublicKey, envelope.getSourceDevice(), envelope.getTimestamp(), false); String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
sessionVersion = sessionCipher.getSessionVersion(); 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<byte[], String> 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()) { } else if (envelope.isPreKeySignalMessage()) {
paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext)); paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext));
metadata = new Metadata(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), false); 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); metadata = new Metadata(envelope.getSource(), envelope.getSourceDevice(), envelope.getTimestamp(), false);
sessionVersion = sessionCipher.getSessionVersion(); sessionVersion = sessionCipher.getSessionVersion();
} else if (envelope.isUnidentifiedSender()) { } else if (envelope.isUnidentifiedSender()) {
Pair<SignalProtocolAddress, Pair<Integer, byte[]>> results = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp(), envelope.getSource()); try {
Pair<Integer, byte[]> data = results.second(); // Try the Session protocol
paddedMessage = data.second(); kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(envelope);
metadata = new Metadata(results.first().getName(), results.first().getDeviceId(), envelope.getTimestamp(), false); paddedMessage = plaintextAndSenderPublicKey.getFirst();
sessionVersion = sealedSessionCipher.getSessionVersion(new SignalProtocolAddress(metadata.getSender(), metadata.getSenderDevice())); 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<SignalProtocolAddress, Pair<Integer, byte[]>> results = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerTimestamp(), envelope.getSource());
Pair<Integer, byte[]> 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 { } else {
throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType());
} }

View File

@ -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<ByteArray, String>
}

View File

@ -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.messages.SignalServiceEnvelope
import org.session.libsignal.service.api.push.SignalServiceAddress import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.PushTransportDetails 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 import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
class LokiServiceCipher( class LokiServiceCipher(
localAddress: SignalServiceAddress, localAddress: SignalServiceAddress,
private val signalProtocolStore: SignalProtocolStore, private val signalProtocolStore: SignalProtocolStore,
private val sskDatabase: SharedSenderKeysDatabaseProtocol, private val sskDatabase: SharedSenderKeysDatabaseProtocol,
sessionProtocolImpl: SessionProtocol,
sessionResetProtocol: SessionResetProtocol, sessionResetProtocol: SessionResetProtocol,
certificateValidator: CertificateValidator?) certificateValidator: CertificateValidator?)
: SignalServiceCipher(localAddress, signalProtocolStore, sskDatabase, sessionResetProtocol, certificateValidator) { : SignalServiceCipher(
localAddress,
signalProtocolStore,
sskDatabase,
sessionResetProtocol,
sessionProtocolImpl,
certificateValidator) {
private val userPrivateKey get() = signalProtocolStore.identityKeyPair.privateKey.serialize() private val userPrivateKey get() = signalProtocolStore.identityKeyPair.privateKey.serialize()