diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt index 6b0ee22072..6ca2757f59 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt @@ -1,4 +1,56 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving + +import org.session.libsession.messaging.Configuration +import org.session.libsignal.service.api.push.SignalServiceAddress +import org.session.libsignal.service.loki.crypto.LokiServiceCipher +import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error +import org.session.libsession.utilities.AESGCM +import org.whispersystems.curve25519.Curve25519 +import org.session.libsignal.libsignal.loki.ClosedGroupCiphertextMessage +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.protocol.closedgroups.SharedSenderKeysImplementation +import org.session.libsignal.service.loki.utilities.toHexString +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec object MessageReceiverDecryption { + + internal fun decryptWithSignalProtocol(envelope: SignalServiceEnvelope): Pair { + val storage = Configuration.shared.signalStorage + val certificateValidator = Configuration.shared.certificateValidator + val data = envelope.content + if (data.count() == 0) { throw Error.NoData } + val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey + val cipher = LokiServiceCipher(SignalServiceAddress(userPublicKey), storage, sskDatabase, Configuration.shared.sessionResetImp, certificateValidator) + val result = cipher.decrypt(envelope) + } + + internal fun decryptWithSharedSenderKeys(envelope: SignalServiceEnvelope): Pair { + // 1. ) Check preconditions + val groupPublicKey = envelope.source + if (!Configuration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey } + val data = envelope.content + if (data.count() == 0) { throw Error.NoData } + val groupPrivateKey = Configuration.shared.storage.getClosedGroupPrivateKey(groupPublicKey) ?: throw Error.NoGroupPrivateKey + // 2. ) Parse the wrapper + val wrapper = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data) + val ivAndCiphertext = wrapper.ciphertext.toByteArray() + val ephemeralPublicKey = wrapper.ephemeralPublicKey.toByteArray() + // 3. ) Decrypt the data inside + val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(ephemeralPublicKey, groupPrivateKey.serialize()) + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256")) + val symmetricKey = mac.doFinal(ephemeralSharedSecret) + val closedGroupCiphertextMessageAsData = AESGCM.decrypt(ivAndCiphertext, symmetricKey) + // 4. ) Parse the closed group ciphertext message + val closedGroupCiphertextMessage = ClosedGroupCiphertextMessage.from(closedGroupCiphertextMessageAsData) ?: throw Error.ParsingFailed + val senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString() + if (senderPublicKey == Configuration.shared.storage.getUserPublicKey()) { throw Error.SelfSend } + // 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content + val plaintext = SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, groupPublicKey, senderPublicKey, closedGroupCiphertextMessage.keyIndex) + // 6. ) Return + return Pair(plaintext, senderPublicKey) + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 87941f7aea..f4f1048ca1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -1,4 +1,198 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving + +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred + +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.jobs.NotifyPNServerJob +import org.session.libsession.messaging.opengroups.OpenGroupAPI +import org.session.libsession.messaging.opengroups.OpenGroupMessage +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.snode.RawResponsePromise +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeMessage + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.loki.api.crypto.ProofOfWork + object MessageSender { + + // Error + internal sealed class Error(val description: String) : Exception() { + object InvalidMessage : Error("Invalid message.") + object ProtoConversionFailed : Error("Couldn't convert message to proto.") + object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.") + object NoUserPublicKey : Error("Couldn't find user key pair.") + + // Closed groups + object NoThread : Error("Couldn't find a thread associated with the given group public key.") + object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.") + object InvalidClosedGroupUpdate : Error("Invalid group update.") + + internal val isRetryable: Boolean = when (this) { + is InvalidMessage -> false + is ProtoConversionFailed -> false + is ProofOfWorkCalculationFailed -> false + is InvalidClosedGroupUpdate -> false + else -> true + } + } + + // Convenience + fun send(message: Message, destination: Destination): Promise { + if (destination is Destination.OpenGroup) { + return sendToOpenGroupDestination(destination, message) + } + return sendToSnodeDestination(destination, message) + } + + // One-on-One Chats & Closed Groups + fun sendToSnodeDestination(destination: Destination, message: Message): Promise { + val deferred = deferred() + val promise = deferred.promise + val storage = Configuration.shared.storage + val preconditionFailure = Exception("Destination should not be open groups!") + var snodeMessage: SnodeMessage? = null + message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */ + message.sender = storage.getUserPublicKey() + try { + when (destination) { + is Destination.Contact -> message.recipient = destination.publicKey + is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey + is Destination.OpenGroup -> throw preconditionFailure + } + // Validate the message + message.isValid ?: throw Error.InvalidMessage + // Convert it to protobuf + val proto = message.toProto() ?: throw Error.ProtoConversionFailed + // Serialize the protobuf + val plaintext = proto.toByteArray() + // Encrypt the serialized protobuf + val ciphertext: ByteArray + when (destination) { + is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSignalProtocol(plaintext, message, destination.publicKey) + is Destination.ClosedGroup -> ciphertext = MessageSenderEncryption.encryptWithSharedSenderKeys(plaintext, destination.groupPublicKey) + is Destination.OpenGroup -> throw preconditionFailure + } + // Wrap the result + val kind: SignalServiceProtos.Envelope.Type + val senderPublicKey: String + when (destination) { + is Destination.Contact -> { + kind = SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER + senderPublicKey = "" + } + is Destination.ClosedGroup -> { + kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT + senderPublicKey = destination.groupPublicKey + } + is Destination.OpenGroup -> throw preconditionFailure + } + val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext) + // Calculate proof of work + val recipient = message.recipient!! + val base64EncodedData = Base64.encodeBytes(wrappedMessage) + val timestamp = System.currentTimeMillis() + val nonce = ProofOfWork.calculate(base64EncodedData, recipient, timestamp, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed + // Send the result + snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, timestamp, nonce) + SnodeAPI.sendMessage(snodeMessage).success { promises: Set -> + var isSuccess = false + val promiseCount = promises.size + var errorCount = 0 + promises.forEach { promise: RawResponsePromise -> + promise.success { + if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds + isSuccess = true + deferred.resolve(Unit) + } + promise.fail { + errorCount += 1 + if (errorCount != promiseCount) { return@fail } // Only error out if all promises failed + deferred.reject(it) + } + } + }.fail { + Log.d("Loki", "Couldn't send message due to error: $it.") + deferred.reject(it) + } + } catch (exception: Exception) { + deferred.reject(exception) + } + // Handle completion + promise.success { + handleSuccessfulMessageSend(message) + if (message is VisibleMessage && snodeMessage != null) { + val notifyPNServerJob = NotifyPNServerJob(snodeMessage) + JobQueue.shared.add(notifyPNServerJob) + } + } + promise.fail { + handleFailedMessageSend(message, it) + } + + return promise + } + + // Open Groups + fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { + val deferred = deferred() + val promise = deferred.promise + val storage = Configuration.shared.storage + val preconditionFailure = Exception("Destination should not be contacts or closed groups!") + message.sentTimestamp = System.currentTimeMillis() + message.sender = storage.getUserPublicKey() + try { + val server: String + val channel: Long + when (destination) { + is Destination.Contact -> throw preconditionFailure + is Destination.ClosedGroup -> throw preconditionFailure + is Destination.OpenGroup -> { + message.recipient = "${destination.server}.${destination.channel}" + server = destination.server + channel = destination.channel + } + } + // Validate the message + if (message !is VisibleMessage || !message.isValid) { + throw Error.InvalidMessage + } + // Convert the message to an open group message + val openGroupMessage = OpenGroupMessage.from(message, server) ?: throw Error.InvalidMessage + // Send the result + OpenGroupAPI.sendMessage(openGroupMessage, channel, server).success { + message.openGroupServerMessageID = it.serverID + deferred.resolve(Unit) + }.fail { + deferred.reject(it) + } + } catch (exception: Exception) { + deferred.reject(exception) + } + // Handle completion + promise.success { + handleSuccessfulMessageSend(message) + } + promise.fail { + handleFailedMessageSend(message, it) + } + return deferred.promise + } + + // Result Handling + fun handleSuccessfulMessageSend(message: Message) { + + } + + fun handleFailedMessageSend(message: Message, error: Exception) { + + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt index 65e9828fbb..ef1a5ba8b9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt @@ -1,4 +1,49 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving + +import com.google.protobuf.ByteString +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.sending_receiving.MessageSender.Error +import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil +import org.session.libsession.utilities.AESGCM +import org.session.libsignal.libsignal.SignalProtocolAddress +import org.session.libsignal.libsignal.loki.ClosedGroupCiphertextMessage +import org.session.libsignal.libsignal.util.Hex +import org.session.libsignal.service.api.crypto.SignalServiceCipher +import org.session.libsignal.service.api.push.SignalServiceAddress +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded object MessageSenderEncryption { + + internal fun encryptWithSignalProtocol(plaintext: ByteArray, message: Message, recipientPublicKey: String): ByteArray{ + val storage = Configuration.shared.signalStorage + val sskDatabase = Configuration.shared.sskDatabase + val sessionResetImp = Configuration.shared.sessionResetImp + val localAddress = SignalServiceAddress(recipientPublicKey) + val certificateValidator = Configuration.shared.certificateValidator + val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator) + val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1) + val unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) + val encryptedMessage = cipher.encrypt(signalProtocolAddress, unidentifiedAccess,plaintext) + return Base64.decode(encryptedMessage.content) + } + + internal fun encryptWithSharedSenderKeys(plaintext: ByteArray, groupPublicKey: String): ByteArray { + // 1. ) Encrypt the data with the user's sender key + val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey + val ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(plaintext, groupPublicKey, userPublicKey) + val ivAndCiphertext = ciphertextAndKeyIndex.first + val keyIndex = ciphertextAndKeyIndex.second + val encryptedMessage = 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 intermediate = AESGCM.encrypt(encryptedMessage.serialize(), groupPublicKey.removing05PrefixIfNeeded()) + // 3. ) Wrap the result + return SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.newBuilder() + .setCiphertext(ByteString.copyFrom(intermediate.ciphertext)) + .setEphemeralPublicKey(ByteString.copyFrom(intermediate.ephemeralPublicKey)) + .build().toByteArray() + } } \ No newline at end of file