remove shared sender keys

This commit is contained in:
Ryan ZHAO
2021-02-18 14:14:05 +11:00
parent 568fddf91d
commit 9d0831b874
29 changed files with 120 additions and 678 deletions

View File

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

View File

@@ -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}

View File

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

View File

@@ -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
}

View File

@@ -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()} ]"
}
}

View File

@@ -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()} ]"
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,6 +0,0 @@
package org.session.libsignal.service.loki.protocol.closedgroups
public interface SharedSenderKeysImplementationDelegate {
public fun requestSenderKey(groupPublicKey: String, senderPublicKey: String)
}

View File

@@ -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