mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 14:47:44 +00:00
remove shared sender keys
This commit is contained in:
@@ -75,7 +75,6 @@ import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol
|
||||
import org.session.libsignal.service.loki.database.LokiPreKeyBundleDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities;
|
||||
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol;
|
||||
import org.session.libsignal.service.loki.utilities.Broadcaster;
|
||||
@@ -121,7 +120,6 @@ public class SignalServiceMessageSender {
|
||||
// Loki
|
||||
private final String userPublicKey;
|
||||
private final LokiAPIDatabaseProtocol apiDatabase;
|
||||
private final SharedSenderKeysDatabaseProtocol sskDatabase;
|
||||
private final LokiThreadDatabaseProtocol threadDatabase;
|
||||
private final LokiMessageDatabaseProtocol messageDatabase;
|
||||
private final LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase;
|
||||
@@ -151,7 +149,6 @@ public class SignalServiceMessageSender {
|
||||
Optional<EventListener> eventListener,
|
||||
String userPublicKey,
|
||||
LokiAPIDatabaseProtocol apiDatabase,
|
||||
SharedSenderKeysDatabaseProtocol sskDatabase,
|
||||
LokiThreadDatabaseProtocol threadDatabase,
|
||||
LokiMessageDatabaseProtocol messageDatabase,
|
||||
LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase,
|
||||
@@ -161,7 +158,7 @@ public class SignalServiceMessageSender {
|
||||
LokiOpenGroupDatabaseProtocol openGroupDatabase,
|
||||
Broadcaster 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);
|
||||
this(urls, new StaticCredentialsProvider(user, password, null), store, userAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, userPublicKey, apiDatabase, threadDatabase, messageDatabase, preKeyBundleDatabase, sessionProtocolImpl, sessionResetImpl, userDatabase, openGroupDatabase, broadcaster);
|
||||
}
|
||||
|
||||
public SignalServiceMessageSender(SignalServiceConfiguration urls,
|
||||
@@ -174,7 +171,6 @@ public class SignalServiceMessageSender {
|
||||
Optional<EventListener> eventListener,
|
||||
String userPublicKey,
|
||||
LokiAPIDatabaseProtocol apiDatabase,
|
||||
SharedSenderKeysDatabaseProtocol sskDatabase,
|
||||
LokiThreadDatabaseProtocol threadDatabase,
|
||||
LokiMessageDatabaseProtocol messageDatabase,
|
||||
LokiPreKeyBundleDatabaseProtocol preKeyBundleDatabase,
|
||||
@@ -193,7 +189,6 @@ public class SignalServiceMessageSender {
|
||||
this.eventListener = eventListener;
|
||||
this.userPublicKey = userPublicKey;
|
||||
this.apiDatabase = apiDatabase;
|
||||
this.sskDatabase = sskDatabase;
|
||||
this.threadDatabase = threadDatabase;
|
||||
this.messageDatabase = messageDatabase;
|
||||
this.preKeyBundleDatabase = preKeyBundleDatabase;
|
||||
@@ -1181,9 +1176,9 @@ public class SignalServiceMessageSender {
|
||||
|
||||
PushTransportDetails transportDetails = new PushTransportDetails(3);
|
||||
String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group
|
||||
boolean isSSKBasedClosedGroup = sskDatabase.isSSKBasedClosedGroup(publicKey);
|
||||
boolean isClosedGroup = apiDatabase.isClosedGroup(publicKey);
|
||||
String encryptionPublicKey;
|
||||
if (isSSKBasedClosedGroup) {
|
||||
if (isClosedGroup) {
|
||||
ECKeyPair encryptionKeyPair = apiDatabase.getLatestClosedGroupEncryptionKeyPair(publicKey);
|
||||
encryptionPublicKey = HexEncodingKt.getHexEncodedPublicKey(encryptionKeyPair);
|
||||
} else {
|
||||
@@ -1191,7 +1186,7 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
byte[] ciphertext = sessionProtocolImpl.encrypt(transportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey);
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int type = isSSKBasedClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
|
||||
int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
|
||||
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(type, 1, 0, body);
|
||||
messages.add(message);
|
||||
|
@@ -88,8 +88,6 @@ import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocolUtilities;
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
||||
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.sessionmanagement.PreKeyBundleMessage;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -115,7 +113,6 @@ public class SignalServiceCipher {
|
||||
|
||||
private final SignalProtocolStore signalProtocolStore;
|
||||
private final SessionResetProtocol sessionResetProtocol;
|
||||
private final SharedSenderKeysDatabaseProtocol sskDatabase;
|
||||
private final SignalServiceAddress localAddress;
|
||||
private final SessionProtocol sessionProtocolImpl;
|
||||
private final LokiAPIDatabaseProtocol apiDB;
|
||||
@@ -123,7 +120,6 @@ public class SignalServiceCipher {
|
||||
|
||||
public SignalServiceCipher(SignalServiceAddress localAddress,
|
||||
SignalProtocolStore signalProtocolStore,
|
||||
SharedSenderKeysDatabaseProtocol sskDatabase,
|
||||
SessionResetProtocol sessionResetProtocol,
|
||||
SessionProtocol sessionProtocolImpl,
|
||||
LokiAPIDatabaseProtocol apiDB,
|
||||
@@ -131,56 +127,55 @@ public class SignalServiceCipher {
|
||||
{
|
||||
this.signalProtocolStore = signalProtocolStore;
|
||||
this.sessionResetProtocol = sessionResetProtocol;
|
||||
this.sskDatabase = sskDatabase;
|
||||
this.localAddress = localAddress;
|
||||
this.sessionProtocolImpl = sessionProtocolImpl;
|
||||
this.apiDB = apiDB;
|
||||
this.certificateValidator = certificateValidator;
|
||||
}
|
||||
|
||||
public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
byte[] unpaddedMessage)
|
||||
throws UntrustedIdentityException, InvalidKeyException, IOException
|
||||
{
|
||||
if (unidentifiedAccess.isPresent() && sskDatabase.isSSKBasedClosedGroup(destination.getName())) {
|
||||
String userPublicKey = localAddress.getNumber();
|
||||
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(userPublicKey, 1);
|
||||
SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, sessionResetProtocol, signalProtocolAddress);
|
||||
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
|
||||
byte[] plaintext = transportDetails.getPaddedMessageBody(unpaddedMessage);
|
||||
byte[] ciphertext = ClosedGroupUtilities.encrypt(plaintext, destination.getName(), userPublicKey);
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
|
||||
return new OutgoingPushMessage(Type.CLOSED_GROUP_CIPHERTEXT_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
} else if (unidentifiedAccess.isPresent()) {
|
||||
SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, sessionResetProtocol, new SignalProtocolAddress(localAddress.getNumber(), 1));
|
||||
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
|
||||
byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
|
||||
|
||||
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
} else {
|
||||
SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, destination);
|
||||
PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
|
||||
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
|
||||
String body = Base64.encodeBytes(message.serialize());
|
||||
|
||||
int type;
|
||||
|
||||
switch (message.getType()) {
|
||||
case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
|
||||
case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
|
||||
case CiphertextMessage.FALLBACK_MESSAGE_TYPE: type = Type.FALLBACK_MESSAGE_VALUE; break;
|
||||
case CiphertextMessage.CLOSED_GROUP_CIPHERTEXT: type = Type.CLOSED_GROUP_CIPHERTEXT_VALUE; break;
|
||||
default: throw new AssertionError("Bad type: " + message.getType());
|
||||
}
|
||||
|
||||
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
}
|
||||
}
|
||||
// public OutgoingPushMessage encrypt(SignalProtocolAddress destination,
|
||||
// Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
// byte[] unpaddedMessage)
|
||||
// throws UntrustedIdentityException, InvalidKeyException, IOException
|
||||
// {
|
||||
// if (unidentifiedAccess.isPresent() && sskDatabase.isSSKBasedClosedGroup(destination.getName())) {
|
||||
// String userPublicKey = localAddress.getNumber();
|
||||
// SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(userPublicKey, 1);
|
||||
// SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, sessionResetProtocol, signalProtocolAddress);
|
||||
// PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
|
||||
// byte[] plaintext = transportDetails.getPaddedMessageBody(unpaddedMessage);
|
||||
// byte[] ciphertext = ClosedGroupUtilities.encrypt(plaintext, destination.getName(), userPublicKey);
|
||||
// String body = Base64.encodeBytes(ciphertext);
|
||||
// int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
|
||||
// return new OutgoingPushMessage(Type.CLOSED_GROUP_CIPHERTEXT_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
// } else if (unidentifiedAccess.isPresent()) {
|
||||
// SealedSessionCipher sessionCipher = new SealedSessionCipher(signalProtocolStore, sessionResetProtocol, new SignalProtocolAddress(localAddress.getNumber(), 1));
|
||||
// PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion(destination));
|
||||
// byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
// String body = Base64.encodeBytes(ciphertext);
|
||||
// int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination);
|
||||
//
|
||||
// return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
// } else {
|
||||
// SessionCipher sessionCipher = new SessionCipher(signalProtocolStore, destination);
|
||||
// PushTransportDetails transportDetails = new PushTransportDetails(sessionCipher.getSessionVersion());
|
||||
// CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
|
||||
// int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
|
||||
// String body = Base64.encodeBytes(message.serialize());
|
||||
//
|
||||
// int type;
|
||||
//
|
||||
// switch (message.getType()) {
|
||||
// case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
|
||||
// case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
|
||||
// case CiphertextMessage.FALLBACK_MESSAGE_TYPE: type = Type.FALLBACK_MESSAGE_VALUE; break;
|
||||
// case CiphertextMessage.CLOSED_GROUP_CIPHERTEXT: type = Type.CLOSED_GROUP_CIPHERTEXT_VALUE; break;
|
||||
// default: throw new AssertionError("Bad type: " + message.getType());
|
||||
// }
|
||||
//
|
||||
// return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Decrypt a received {@link SignalServiceEnvelope}
|
||||
|
@@ -11,9 +11,8 @@ 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.database.LokiAPIDatabaseProtocol
|
||||
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, apiDB: LokiAPIDatabaseProtocol, certificateValidator: CertificateValidator?) : SignalServiceCipher(localAddress, signalProtocolStore, sskDatabase, sessionResetProtocol, sessionProtocolImpl, apiDB, certificateValidator) {
|
||||
class LokiServiceCipher(localAddress: SignalServiceAddress, private val signalProtocolStore: SignalProtocolStore, sessionProtocolImpl: SessionProtocol, sessionResetProtocol: SessionResetProtocol, apiDB: LokiAPIDatabaseProtocol, certificateValidator: CertificateValidator?) : SignalServiceCipher(localAddress, signalProtocolStore, sessionResetProtocol, sessionProtocolImpl, apiDB, certificateValidator) {
|
||||
|
||||
|
||||
private val userPrivateKey get() = signalProtocolStore.identityKeyPair.privateKey.serialize()
|
||||
|
@@ -37,4 +37,5 @@ interface LokiAPIDatabaseProtocol {
|
||||
fun getUserX25519KeyPair(): ECKeyPair
|
||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): List<ECKeyPair>
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
fun isClosedGroup(groupPublicKey: String): Boolean
|
||||
}
|
||||
|
@@ -1,22 +0,0 @@
|
||||
package org.session.libsignal.service.loki.protocol.closedgroups
|
||||
|
||||
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
||||
|
||||
public class ClosedGroupRatchet(public val chainKey: String, public val keyIndex: Int, public val messageKeys: List<String>) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is ClosedGroupRatchet) {
|
||||
chainKey == other.chainKey && keyIndex == other.keyIndex && messageKeys == other.messageKeys
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return chainKey.hashCode() xor keyIndex.hashCode() xor messageKeys.hashCode()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "[ chainKey : $chainKey, keyIndex : $keyIndex, messageKeys : ${messageKeys.prettifiedDescription()} ]"
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
package org.session.libsignal.service.loki.protocol.closedgroups
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
|
||||
public class ClosedGroupSenderKey(public val chainKey: ByteArray, public val keyIndex: Int, public val publicKey: ByteArray) {
|
||||
|
||||
companion object {
|
||||
|
||||
public fun fromJSON(jsonAsString: String): ClosedGroupSenderKey? {
|
||||
try {
|
||||
val json = JsonUtil.fromJson(jsonAsString, Map::class.java)
|
||||
val chainKey = Hex.fromStringCondensed(json["chainKey"] as String)
|
||||
val keyIndex = json["keyIndex"] as Int
|
||||
val publicKey = Hex.fromStringCondensed(json["publicKey"] as String)
|
||||
return ClosedGroupSenderKey(chainKey, keyIndex, publicKey)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse closed group sender key from: $jsonAsString.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun toJSON(): String {
|
||||
val json = mapOf( "chainKey" to chainKey.toHexString(), "keyIndex" to keyIndex, "publicKey" to publicKey.toHexString() )
|
||||
return JsonUtil.toJson(json)
|
||||
}
|
||||
|
||||
public fun toProto(): SignalServiceProtos.ClosedGroupUpdate.SenderKey {
|
||||
val builder = SignalServiceProtos.ClosedGroupUpdate.SenderKey.newBuilder()
|
||||
builder.chainKey = ByteString.copyFrom(chainKey)
|
||||
builder.keyIndex = keyIndex
|
||||
builder.publicKey = ByteString.copyFrom(publicKey)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is ClosedGroupSenderKey) {
|
||||
chainKey.contentEquals(other.chainKey) && keyIndex == other.keyIndex && publicKey.contentEquals(other.publicKey)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return chainKey.hashCode() xor keyIndex.hashCode() xor publicKey.hashCode()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "[ chainKey : ${chainKey.toHexString()}, keyIndex : $keyIndex, messageKeys : ${publicKey.toHexString()} ]"
|
||||
}
|
||||
}
|
@@ -1,78 +0,0 @@
|
||||
package org.session.libsignal.service.loki.protocol.closedgroups
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.session.libsignal.libsignal.loki.ClosedGroupCiphertextMessage
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.libsignal.util.Pair
|
||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.api.utilities.DecryptionUtilities
|
||||
import org.session.libsignal.service.loki.api.utilities.EncryptionUtilities
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
public object ClosedGroupUtilities {
|
||||
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
object InvalidGroupPublicKey : Error("Invalid group public key.")
|
||||
object NoData : Error("Received an empty envelope.")
|
||||
object NoGroupPrivateKey : Error("Missing group private key.")
|
||||
object ParsingFailed : Error("Couldn't parse closed group ciphertext message.")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun encrypt(data: ByteArray, groupPublicKey: String, userPublicKey: String): ByteArray {
|
||||
// 1. ) Encrypt the data with the user's sender key
|
||||
val ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(data, groupPublicKey, userPublicKey)
|
||||
val ivAndCiphertext = ciphertextAndKeyIndex.first
|
||||
val keyIndex = ciphertextAndKeyIndex.second
|
||||
val x0 = ClosedGroupCiphertextMessage(ivAndCiphertext, Hex.fromStringCondensed(userPublicKey), keyIndex);
|
||||
// 2. ) Encrypt the result for the group's public key to hide the sender public key and key index
|
||||
val x1 = EncryptionUtilities.encryptForX25519PublicKey(x0.serialize(), groupPublicKey.removing05PrefixIfNeeded())
|
||||
// 3. ) Wrap the result
|
||||
return SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.newBuilder()
|
||||
.setCiphertext(ByteString.copyFrom(x1.ciphertext))
|
||||
.setEphemeralPublicKey(ByteString.copyFrom(x1.ephemeralPublicKey))
|
||||
.build().toByteArray()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun decrypt(envelope: SignalServiceEnvelope): Pair<ByteArray, String> {
|
||||
// 1. ) Check preconditions
|
||||
val groupPublicKey = envelope.source
|
||||
if (groupPublicKey == null || !SharedSenderKeysImplementation.shared.isClosedGroup(groupPublicKey)) {
|
||||
throw Error.InvalidGroupPublicKey
|
||||
}
|
||||
val data = envelope.content
|
||||
if (data.count() == 0) {
|
||||
throw Error.NoData
|
||||
}
|
||||
val groupPrivateKey = SharedSenderKeysImplementation.shared.getKeyPair(groupPublicKey)?.privateKey?.serialize()
|
||||
if (groupPrivateKey == null) {
|
||||
throw Error.NoGroupPrivateKey
|
||||
}
|
||||
// 2. ) Parse the wrapper
|
||||
val x0 = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data)
|
||||
val ivAndCiphertext = x0.ciphertext.toByteArray()
|
||||
val ephemeralPublicKey = x0.ephemeralPublicKey.toByteArray()
|
||||
// 3. ) Decrypt the data inside
|
||||
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(ephemeralPublicKey, groupPrivateKey)
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
|
||||
val symmetricKey = mac.doFinal(ephemeralSharedSecret)
|
||||
val x1 = DecryptionUtilities.decryptUsingAESGCM(ivAndCiphertext, symmetricKey)
|
||||
// 4. ) Parse the closed group ciphertext message
|
||||
val x2 = ClosedGroupCiphertextMessage.from(x1)
|
||||
if (x2 == null) {
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
val senderPublicKey = x2.senderPublicKey.toHexString()
|
||||
// 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content
|
||||
val plaintext = SharedSenderKeysImplementation.shared.decrypt(x2.ivAndCiphertext, groupPublicKey, senderPublicKey, x2.keyIndex)
|
||||
// 6. ) Return
|
||||
return Pair(plaintext, senderPublicKey)
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
package org.session.libsignal.service.loki.protocol.closedgroups
|
||||
|
||||
enum class ClosedGroupRatchetCollectionType { Old, Current }
|
||||
|
||||
interface SharedSenderKeysDatabaseProtocol {
|
||||
|
||||
// region Ratchets & Sender Keys
|
||||
fun getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, collection: ClosedGroupRatchetCollectionType): ClosedGroupRatchet?
|
||||
fun setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, collection: ClosedGroupRatchetCollectionType)
|
||||
fun removeAllClosedGroupRatchets(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType)
|
||||
fun getAllClosedGroupRatchets(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType): Set<Pair<String, ClosedGroupRatchet>>
|
||||
fun getAllClosedGroupSenderKeys(groupPublicKey: String, collection: ClosedGroupRatchetCollectionType): Set<ClosedGroupSenderKey>
|
||||
// endregion
|
||||
|
||||
// region Private & Public Keys
|
||||
fun getClosedGroupPrivateKey(groupPublicKey: String): String?
|
||||
fun setClosedGroupPrivateKey(groupPublicKey: String, groupPrivateKey: String)
|
||||
fun removeClosedGroupPrivateKey(groupPublicKey: String)
|
||||
fun getAllClosedGroupPublicKeys(): Set<String>
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
fun isSSKBasedClosedGroup(groupPublicKey: String): Boolean
|
||||
// endregion
|
||||
}
|
@@ -1,217 +0,0 @@
|
||||
package org.session.libsignal.service.loki.protocol.closedgroups
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.libsignal.util.ByteUtil
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import org.session.libsignal.service.loki.api.utilities.EncryptionUtilities
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
public final class SharedSenderKeysImplementation(private val database: SharedSenderKeysDatabaseProtocol, private val delegate: SharedSenderKeysImplementationDelegate) {
|
||||
private val gcmTagSize = 128
|
||||
private val ivSize = 12
|
||||
|
||||
// A quick overview of how shared sender key based closed groups work:
|
||||
//
|
||||
// • When a user creates a group, they generate a key pair for the group along with a ratchet for
|
||||
// every member of the group. They bundle this together with some other group info such as the group
|
||||
// name in a `ClosedGroupUpdateMessage` and send that using established channels to every member of
|
||||
// the group. Note that because a user can only pick from their existing contacts when selecting
|
||||
// the group members they shouldn't need to establish sessions before being able to send the
|
||||
// `ClosedGroupUpdateMessage`.
|
||||
// • After the group is created, every user polls for the public key associated with the group.
|
||||
// • Upon receiving a `ClosedGroupUpdateMessage` of type `.new`, a user sends session requests to all
|
||||
// other members of the group they don't yet have a session with for reasons outlined below.
|
||||
// • When a user sends a message they step their ratchet and use the resulting message key to encrypt
|
||||
// the message.
|
||||
// • When another user receives that message, they step the ratchet associated with the sender and
|
||||
// use the resulting message key to decrypt the message.
|
||||
// • When a user leaves or is kicked from a group, all members must generate new ratchets to ensure that
|
||||
// removed users can't decrypt messages going forward. To this end every user deletes all ratchets
|
||||
// associated with the group in question upon receiving a group update message that indicates that
|
||||
// a user left. They then generate a new ratchet for themselves and send it out to all members of
|
||||
// the group. The user should already have established sessions with all other members at this point
|
||||
// because of the behavior outlined a few points above.
|
||||
// • When a user adds a new member to the group, they generate a ratchet for that new member and
|
||||
// send that bundled in a `ClosedGroupUpdateMessage` to the group. They send a
|
||||
// `ClosedGroupUpdateMessage` with the newly generated ratchet but also the existing ratchets of
|
||||
// every other member of the group to the user that joined.
|
||||
|
||||
// region Initialization
|
||||
companion object {
|
||||
|
||||
public lateinit var shared: SharedSenderKeysImplementation
|
||||
|
||||
public fun configureIfNeeded(database: SharedSenderKeysDatabaseProtocol, delegate: SharedSenderKeysImplementationDelegate) {
|
||||
if (::shared.isInitialized) { return; }
|
||||
shared = SharedSenderKeysImplementation(database, delegate)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Error
|
||||
public class LoadingFailed(val groupPublicKey: String, val senderPublicKey: String)
|
||||
: Exception("Couldn't get ratchet for closed group with public key: $groupPublicKey, sender public key: $senderPublicKey.")
|
||||
public class MessageKeyMissing(val targetKeyIndex: Int, val groupPublicKey: String, val senderPublicKey: String)
|
||||
: Exception("Couldn't find message key for old key index: $targetKeyIndex, public key: $groupPublicKey, sender public key: $senderPublicKey.")
|
||||
public class GenericRatchetingException : Exception("An error occurred.")
|
||||
// endregion
|
||||
|
||||
// region Private API
|
||||
private fun hmac(key: ByteArray, input: ByteArray): ByteArray {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(key, "HmacSHA256"))
|
||||
return mac.doFinal(input)
|
||||
}
|
||||
|
||||
private fun step(ratchet: ClosedGroupRatchet): ClosedGroupRatchet {
|
||||
val nextMessageKey = hmac(Hex.fromStringCondensed(ratchet.chainKey), ByteArray(1) { 1.toByte() })
|
||||
val nextChainKey = hmac(Hex.fromStringCondensed(ratchet.chainKey), ByteArray(1) { 2.toByte() })
|
||||
val nextKeyIndex = ratchet.keyIndex + 1
|
||||
val messageKeys = ratchet.messageKeys + listOf( nextMessageKey.toHexString() )
|
||||
return ClosedGroupRatchet(nextChainKey.toHexString(), nextKeyIndex, messageKeys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync. Don't call from the main thread.
|
||||
*/
|
||||
private fun stepRatchetOnce(groupPublicKey: String, senderPublicKey: String): ClosedGroupRatchet {
|
||||
val ratchet = database.getClosedGroupRatchet(groupPublicKey, senderPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||
if (ratchet == null) {
|
||||
val exception = LoadingFailed(groupPublicKey, senderPublicKey)
|
||||
Log.d("Loki", exception.message ?: "An error occurred.")
|
||||
throw exception
|
||||
}
|
||||
try {
|
||||
val result = step(ratchet)
|
||||
database.setClosedGroupRatchet(groupPublicKey, senderPublicKey, result, ClosedGroupRatchetCollectionType.Current)
|
||||
return result
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't step ratchet due to error: $exception.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private fun stepRatchet(groupPublicKey: String, senderPublicKey: String, targetKeyIndex: Int, isRetry: Boolean = false): ClosedGroupRatchet {
|
||||
val collection = if (isRetry) ClosedGroupRatchetCollectionType.Old else ClosedGroupRatchetCollectionType.Current
|
||||
val ratchet = database.getClosedGroupRatchet(groupPublicKey, senderPublicKey, collection)
|
||||
if (ratchet == null) {
|
||||
val exception = LoadingFailed(groupPublicKey, senderPublicKey)
|
||||
Log.d("Loki", exception.message ?: "An error occurred.")
|
||||
throw exception
|
||||
}
|
||||
if (targetKeyIndex < ratchet.keyIndex) {
|
||||
// There's no need to advance the ratchet if this is invoked for an old key index
|
||||
if (ratchet.messageKeys.count() <= targetKeyIndex) {
|
||||
val exception = MessageKeyMissing(targetKeyIndex, groupPublicKey, senderPublicKey)
|
||||
Log.d("Loki", exception.message ?: "An error occurred.")
|
||||
throw exception
|
||||
}
|
||||
return ratchet
|
||||
} else {
|
||||
var currentKeyIndex = ratchet.keyIndex
|
||||
var result: ClosedGroupRatchet = ratchet // Explicitly typed because otherwise the compiler has trouble inferring that this can't be null
|
||||
while (currentKeyIndex < targetKeyIndex) {
|
||||
try {
|
||||
result = step(result)
|
||||
currentKeyIndex = result.keyIndex
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't step ratchet due to error: $exception.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
val collection = if (isRetry) ClosedGroupRatchetCollectionType.Old else ClosedGroupRatchetCollectionType.Current
|
||||
database.setClosedGroupRatchet(groupPublicKey, senderPublicKey, result, collection)
|
||||
return result
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Public API
|
||||
public fun generateRatchet(groupPublicKey: String, senderPublicKey: String): ClosedGroupRatchet {
|
||||
val rootChainKey = Util.getSecretBytes(32).toHexString()
|
||||
val ratchet = ClosedGroupRatchet(rootChainKey, 0, listOf())
|
||||
database.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet, ClosedGroupRatchetCollectionType.Current)
|
||||
return ratchet
|
||||
}
|
||||
|
||||
public fun encrypt(plaintext: ByteArray, groupPublicKey: String, senderPublicKey: String): Pair<ByteArray, Int> {
|
||||
val ratchet: ClosedGroupRatchet
|
||||
try {
|
||||
ratchet = stepRatchetOnce(groupPublicKey, senderPublicKey)
|
||||
} catch (exception: Exception) {
|
||||
if (exception is LoadingFailed) {
|
||||
delegate.requestSenderKey(groupPublicKey, senderPublicKey)
|
||||
}
|
||||
throw exception
|
||||
}
|
||||
val iv = Util.getSecretBytes(ivSize)
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
val messageKey = ratchet.messageKeys.last()
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(Hex.fromStringCondensed(messageKey), "AES"), GCMParameterSpec(gcmTagSize, iv))
|
||||
return Pair(ByteUtil.combine(iv, cipher.doFinal(plaintext)), ratchet.keyIndex)
|
||||
}
|
||||
|
||||
public fun decrypt(ivAndCiphertext: ByteArray, groupPublicKey: String, senderPublicKey: String, keyIndex: Int, isRetry: Boolean = false): ByteArray {
|
||||
val ratchet: ClosedGroupRatchet
|
||||
try {
|
||||
ratchet = stepRatchet(groupPublicKey, senderPublicKey, keyIndex, isRetry)
|
||||
} catch (exception: Exception) {
|
||||
if (!isRetry) {
|
||||
return decrypt(ivAndCiphertext, groupPublicKey, senderPublicKey, keyIndex, true)
|
||||
} else {
|
||||
if (exception is LoadingFailed) {
|
||||
delegate.requestSenderKey(groupPublicKey, senderPublicKey)
|
||||
}
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
val iv = ivAndCiphertext.sliceArray(0 until ivSize)
|
||||
val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.count())
|
||||
val messageKeys = ratchet.messageKeys
|
||||
val lastNMessageKeys: List<String>
|
||||
if (messageKeys.count() > 16) { // Pick an arbitrary number of message keys to try; this helps resolve issues caused by messages arriving out of order
|
||||
lastNMessageKeys = messageKeys.subList(messageKeys.lastIndex - 16, messageKeys.lastIndex)
|
||||
} else {
|
||||
lastNMessageKeys = messageKeys
|
||||
}
|
||||
if (lastNMessageKeys.isEmpty()) {
|
||||
throw MessageKeyMissing(keyIndex, groupPublicKey, senderPublicKey)
|
||||
}
|
||||
var exception: Exception? = null
|
||||
for (messageKey in lastNMessageKeys.reversed()) { // Reversed because most likely the last one is the one we need
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(Hex.fromStringCondensed(messageKey), "AES"), GCMParameterSpec(EncryptionUtilities.gcmTagSize, iv))
|
||||
try {
|
||||
return cipher.doFinal(ciphertext)
|
||||
} catch (e: Exception) {
|
||||
exception = e
|
||||
}
|
||||
}
|
||||
if (!isRetry) {
|
||||
return decrypt(ivAndCiphertext, groupPublicKey, senderPublicKey, keyIndex, true)
|
||||
} else {
|
||||
delegate.requestSenderKey(groupPublicKey, senderPublicKey)
|
||||
throw exception ?: GenericRatchetingException()
|
||||
}
|
||||
}
|
||||
|
||||
public fun isClosedGroup(publicKey: String): Boolean {
|
||||
return database.getAllClosedGroupPublicKeys().contains(publicKey)
|
||||
}
|
||||
|
||||
public fun getKeyPair(groupPublicKey: String): ECKeyPair? {
|
||||
val privateKey = database.getClosedGroupPrivateKey(groupPublicKey) ?: return null
|
||||
return ECKeyPair(DjbECPublicKey(Hex.fromStringCondensed(groupPublicKey.removing05PrefixIfNeeded())),
|
||||
DjbECPrivateKey(Hex.fromStringCondensed(privateKey)))
|
||||
}
|
||||
// endregion
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
package org.session.libsignal.service.loki.protocol.closedgroups
|
||||
|
||||
public interface SharedSenderKeysImplementationDelegate {
|
||||
|
||||
public fun requestSenderKey(groupPublicKey: String, senderPublicKey: String)
|
||||
}
|
@@ -3,23 +3,20 @@ package org.session.libsignal.service.loki.protocol.sessionmanagement
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.libsignal.loki.SessionResetProtocol
|
||||
import org.session.libsignal.libsignal.loki.SessionResetStatus
|
||||
import org.session.libsignal.libsignal.state.SignalProtocolStore
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
|
||||
|
||||
public class SessionManagementProtocol(private val sessionResetImpl: SessionResetProtocol, private val sskDatabase: SharedSenderKeysDatabaseProtocol,
|
||||
private val delegate: SessionManagementProtocolDelegate) {
|
||||
public class SessionManagementProtocol(private val sessionResetImpl: SessionResetProtocol, private val delegate: SessionManagementProtocolDelegate) {
|
||||
|
||||
// region Initialization
|
||||
companion object {
|
||||
|
||||
public lateinit var shared: SessionManagementProtocol
|
||||
|
||||
public fun configureIfNeeded(sessionResetImpl: SessionResetProtocol, sskDatabase: SharedSenderKeysDatabaseProtocol, delegate: SessionManagementProtocolDelegate) {
|
||||
public fun configureIfNeeded(sessionResetImpl: SessionResetProtocol, delegate: SessionManagementProtocolDelegate) {
|
||||
if (::shared.isInitialized) { return; }
|
||||
shared = SessionManagementProtocol(sessionResetImpl, sskDatabase, delegate)
|
||||
shared = SessionManagementProtocol(sessionResetImpl, delegate)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
Reference in New Issue
Block a user