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

@@ -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> 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) {
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();

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.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<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();
try {
// Try the Session protocol
kotlin.Pair<byte[], String> 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<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()) {
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<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()));
try {
// Try the Session protocol
kotlin.Pair<byte[], String> 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<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 {
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.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()