From 979c21ccbf229f0fc8d68b83368373182f6b9abf Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 23 Apr 2021 16:09:47 +1000 Subject: [PATCH] Fix duplicated API --- .../securesms/ApplicationContext.java | 28 +- .../securesms/loki/activities/PathActivity.kt | 2 +- .../loki/activities/SettingsActivity.kt | 2 +- .../loki/api/BackgroundPollWorker.kt | 12 +- .../loki/api/LokiPushNotificationManager.kt | 8 +- .../securesms/loki/utilities/IP2Country.kt | 2 +- .../securesms/loki/views/PathStatusView.kt | 2 +- .../securesms/mms/PushMediaConstraints.java | 2 +- .../messaging/fileserver/FileServerAPI.kt | 2 +- .../messaging/opengroups/OpenGroupAPI.kt | 4 +- .../sending_receiving/MessageSender.kt | 7 +- .../notifications/PushNotificationAPI.kt | 2 +- .../libsession/snode/OnionRequestAPI.kt | 2 +- .../session/libsession/snode/SnodeMessage.kt | 6 +- .../api/SignalServiceMessageReceiver.java | 13 +- .../libsignal/service/loki/api/DotNetAPI.kt | 252 ---------- .../libsignal/service/loki/api/LokiMessage.kt | 86 ---- .../service/loki/api/PushNotificationAPI.kt | 42 -- .../libsignal/service/loki/api/SnodeAPI.kt | 280 ----------- .../libsignal/service/loki/api/SwarmAPI.kt | 185 ------- .../service/loki/api/crypto/ProofOfWork.kt | 64 --- .../loki/api/fileserver/FileServerAPI.kt | 77 --- .../loki/api/onionrequests/OnionRequestAPI.kt | 460 ------------------ .../onionrequests/OnionRequestEncryption.kt | 95 ---- .../loki/utilities/DownloadUtilities.kt | 88 ---- 25 files changed, 38 insertions(+), 1685 deletions(-) delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/DotNetAPI.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/LokiMessage.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/PushNotificationAPI.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/SnodeAPI.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/SwarmAPI.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/ProofOfWork.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/fileserver/FileServerAPI.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/onionrequests/OnionRequestAPI.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/api/onionrequests/OnionRequestEncryption.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 0ebcdc36a7..63215e5294 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -32,12 +32,15 @@ import androidx.multidex.MultiDexApplication; import org.conscrypt.Conscrypt; import org.session.libsession.messaging.MessagingConfiguration; import org.session.libsession.messaging.avatars.AvatarHelper; +import org.session.libsession.messaging.fileserver.FileServerAPI; import org.session.libsession.messaging.jobs.JobQueue; import org.session.libsession.messaging.opengroups.OpenGroupAPI; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI; import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller; import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.messaging.threads.Address; +import org.session.libsession.snode.SnodeAPI; import org.session.libsession.snode.SnodeConfiguration; import org.session.libsession.utilities.IdentityKeyUtil; import org.session.libsession.utilities.SSKEnvironment; @@ -47,10 +50,6 @@ import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWr import org.session.libsession.utilities.dynamiclanguage.LocaleParser; import org.session.libsession.utilities.preferences.ProfileKeyUtil; import org.session.libsignal.service.api.util.StreamDetails; -import org.session.libsignal.service.loki.api.PushNotificationAPI; -import org.session.libsignal.service.loki.api.SnodeAPI; -import org.session.libsignal.service.loki.api.SwarmAPI; -import org.session.libsignal.service.loki.api.fileserver.FileServerAPI; import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol; import org.session.libsignal.service.loki.utilities.mentions.MentionsManager; import org.session.libsignal.utilities.logging.Log; @@ -96,6 +95,7 @@ import org.webrtc.voiceengine.WebRtcAudioUtils; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.security.SecureRandom; import java.security.Security; import java.util.Date; @@ -179,11 +179,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc new SessionProtocolImpl(this)); SnodeConfiguration.Companion.configure(apiDB, broadcaster); if (userPublicKey != null) { - SwarmAPI.Companion.configureIfNeeded(apiDB); - SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster); MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB); } - PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG); setUpStorageAPIIfNeeded(); resubmitProfilePictureIfNeeded(); publicChatManager = new PublicChatManager(this); @@ -427,7 +424,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize(); LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this); - FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB); org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB); return true; } @@ -458,13 +454,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc String userPublicKey = TextSecurePreferences.getLocalNumber(this); if (userPublicKey == null) return; if (poller != null) { - SnodeAPI.shared.setUserPublicKey(userPublicKey); poller.setUserPublicKey(userPublicKey); return; } LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this); - SwarmAPI.Companion.configureIfNeeded(apiDB); - SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster); poller = new Poller(); closedGroupPoller = new ClosedGroupPoller(); } @@ -503,12 +496,13 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc try { File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey)); StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length()); - FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> { - TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime()); - TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt()); - ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey); - return Unit.INSTANCE; - }); + throw new IOException(); +// FileServerAPI.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> { +// TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime()); +// TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt()); +// ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey); +// return Unit.INSTANCE; +// }); } catch (Exception exception) { // Do nothing } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PathActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PathActivity.kt index 0fc4c3d363..482a276328 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PathActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PathActivity.kt @@ -19,12 +19,12 @@ import android.widget.Toast import androidx.annotation.ColorRes import kotlinx.android.synthetic.main.activity_path.* import network.loki.messenger.R +import org.session.libsession.snode.OnionRequestAPI import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.views.GlowViewUtilities import org.thoughtcrime.securesms.loki.views.PathDotView import org.session.libsignal.service.loki.api.Snode -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI class PathActivity : PassphraseRequiredActionBarActivity() { private val broadcastReceivers = mutableListOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt index 6bd6bb85ac..95d1f446c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt @@ -28,13 +28,13 @@ import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.alwaysUi import org.session.libsession.messaging.avatars.AvatarHelper +import org.session.libsession.messaging.fileserver.FileServerAPI import org.session.libsession.messaging.opengroups.OpenGroupAPI import org.session.libsession.messaging.threads.Address import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.preferences.ProfileKeyUtil import org.session.libsignal.service.api.util.StreamDetails -import org.session.libsignal.service.loki.api.fileserver.FileServerAPI import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt index 8476fae3e1..d4bd5ed8d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt @@ -10,11 +10,12 @@ import nl.komponents.kovenant.functional.map import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.opengroups.OpenGroup import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller +import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.service.loki.api.SnodeAPI import org.session.libsignal.utilities.logging.Log import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseFactory +import java.io.IOException import java.util.concurrent.TimeUnit class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { @@ -70,10 +71,11 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor // Private chats val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val privateChatsPromise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes -> - envelopes.map { envelope -> - MessageReceiveJob(envelope.toByteArray(), false).executeAsync() - } + val privateChatsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes -> + throw IOException() +// envelopes.map { envelope -> +// MessageReceiveJob(envelope.toByteArray(), false).executeAsync() +// } } promises.addAll(privateChatsPromise.get()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/LokiPushNotificationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/LokiPushNotificationManager.kt index db2789985f..905a58461b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/LokiPushNotificationManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/LokiPushNotificationManager.kt @@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.loki.api import android.content.Context import nl.komponents.kovenant.functional.map import okhttp3.* +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.snode.OnionRequestAPI import org.thoughtcrime.securesms.database.DatabaseFactory import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.service.loki.api.PushNotificationAPI -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI import org.session.libsignal.service.loki.utilities.retryIfNeeded object LokiPushNotificationManager { @@ -16,10 +16,10 @@ object LokiPushNotificationManager { private val tokenExpirationInterval = 12 * 60 * 60 * 1000 private val server by lazy { - PushNotificationAPI.shared.server + PushNotificationAPI.server } private val pnServerPublicKey by lazy { - PushNotificationAPI.pnServerPublicKey + PushNotificationAPI.serverPublicKey } enum class ClosedGroupOperation { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/IP2Country.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/IP2Country.kt index 7ebc9e2cb7..855ca52240 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/IP2Country.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/IP2Country.kt @@ -7,7 +7,7 @@ import android.content.IntentFilter import androidx.localbroadcastmanager.content.LocalBroadcastManager import org.session.libsignal.utilities.logging.Log import com.opencsv.CSVReader -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI +import org.session.libsession.snode.OnionRequestAPI import org.session.libsignal.utilities.ThreadUtils import java.io.File import java.io.FileOutputStream diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/PathStatusView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/PathStatusView.kt index 7f8611458e..6990849306 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/PathStatusView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/PathStatusView.kt @@ -11,9 +11,9 @@ import android.util.AttributeSet import android.view.View import androidx.annotation.ColorInt import network.loki.messenger.R +import org.session.libsession.snode.OnionRequestAPI import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.toPx -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI class PathStatusView : View { private val broadcastReceivers = mutableListOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java index 3d5ef96399..4bfcbfd24d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; -import org.session.libsignal.service.loki.api.fileserver.FileServerAPI; +import org.session.libsession.messaging.fileserver.FileServerAPI; public class PushMediaConstraints extends MediaConstraints { diff --git a/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt index 37230d79d8..c0ae724d44 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt @@ -4,10 +4,10 @@ import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.map import okhttp3.Request import org.session.libsession.messaging.utilities.DotNetAPI +import org.session.libsession.snode.OnionRequestAPI import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.utilities.* import java.net.URL diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt index 6c35888f7b..f7b1fd56b2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt @@ -8,12 +8,12 @@ import nl.komponents.kovenant.then import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.fileserver.FileServerAPI import org.session.libsession.messaging.utilities.DotNetAPI -import org.session.libsignal.service.loki.utilities.DownloadUtilities import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.utilities.* import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.logging.Log import java.io.ByteArrayOutputStream +import java.io.IOException import java.text.SimpleDateFormat import java.util.* @@ -316,7 +316,7 @@ object OpenGroupAPI: DotNetAPI() { Log.d("Loki", "Downloading open group profile picture from \"$url\".") val outputStream = ByteArrayOutputStream() try { - DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null) + throw IOException(); Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"") return outputStream.toByteArray() } catch (e: Exception) { 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 225c38b7da..b6b4d6788c 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 @@ -23,7 +23,6 @@ import org.session.libsession.snode.SnodeMessage import org.session.libsession.utilities.SSKEnvironment import org.session.libsignal.service.internal.push.PushTransportDetails import org.session.libsignal.service.internal.push.SignalServiceProtos -import org.session.libsignal.service.loki.api.crypto.ProofOfWork import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.logging.Log @@ -151,11 +150,9 @@ object MessageSender { if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { SnodeConfiguration.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!) } - val recipient = message.recipient!! - val base64EncodedData = Base64.encodeBytes(wrappedMessage) - val nonce = ProofOfWork.calculate(base64EncodedData, recipient, message.sentTimestamp!!, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed // Send the result - val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, message.sentTimestamp!!, nonce) + val base64EncodedData = Base64.encodeBytes(wrappedMessage) + val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, message.sentTimestamp!!) if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt index d1b2310207..ed0f771498 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -6,8 +6,8 @@ import okhttp3.MediaType import okhttp3.Request import okhttp3.RequestBody import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.logging.Log diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 20f1fc8d04..3c4b3e9711 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -6,12 +6,12 @@ import nl.komponents.kovenant.deferred import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import okhttp3.Request +import org.session.libsession.messaging.fileserver.FileServerAPI import org.session.libsession.utilities.AESGCM import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.* import org.session.libsignal.service.loki.api.Snode -import org.session.libsignal.service.loki.api.fileserver.FileServerAPI import org.session.libsignal.service.loki.api.utilities.* import org.session.libsession.utilities.AESGCM.EncryptionResult import org.session.libsignal.utilities.ThreadUtils diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt index 558447c549..5d9c49a8d5 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt @@ -8,9 +8,7 @@ data class SnodeMessage( // The time to live for the message in milliseconds. val ttl: Long, // When the proof of work was calculated. - val timestamp: Long, - // The base 64 encoded proof of work. - val nonce: String + val timestamp: Long ) { internal fun toJSON(): Map { return mutableMapOf( @@ -18,6 +16,6 @@ data class SnodeMessage( "data" to data, "ttl" to ttl.toString(), "timestamp" to timestamp.toString(), - "nonce" to nonce) + "nonce" to "") } } diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageReceiver.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageReceiver.java index 8effb24b34..250d9730a9 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageReceiver.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageReceiver.java @@ -12,7 +12,6 @@ import org.session.libsignal.service.api.crypto.ProfileCipherInputStream; import org.session.libsignal.service.api.messages.SignalServiceAttachment.ProgressListener; import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer; import org.session.libsignal.service.api.messages.SignalServiceDataMessage; -import org.session.libsignal.service.loki.utilities.DownloadUtilities; import java.io.File; import java.io.FileInputStream; @@ -46,8 +45,7 @@ public class SignalServiceMessageReceiver { public InputStream retrieveProfileAvatar(String path, File destination, byte[] profileKey, int maxSizeBytes) throws IOException { - DownloadUtilities.downloadFile(destination, path, maxSizeBytes, null); - return new ProfileCipherInputStream(new FileInputStream(destination), profileKey); + throw new IOException(); } /** @@ -65,13 +63,6 @@ public class SignalServiceMessageReceiver { public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener) throws IOException, InvalidMessageException { - // Loki - Fetch attachment - if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL."); - DownloadUtilities.downloadFile(destination, pointer.getUrl(), maxSizeBytes, listener); - - // Loki - Assume we're retrieving an attachment for an open group server if the digest is not set - if (!pointer.getDigest().isPresent()) { return new FileInputStream(destination); } - - return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get()); + throw new IOException(); } } diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/DotNetAPI.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/DotNetAPI.kt deleted file mode 100644 index 044afa6301..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/DotNetAPI.kt +++ /dev/null @@ -1,252 +0,0 @@ -package org.session.libsignal.service.loki.api - -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.functional.map -import nl.komponents.kovenant.then -import okhttp3.MediaType -import okhttp3.MultipartBody -import okhttp3.Request -import okhttp3.RequestBody -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.utilities.DiffieHellman -import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream -import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException -import org.session.libsignal.service.api.push.exceptions.PushNetworkException -import org.session.libsignal.service.api.util.StreamDetails -import org.session.libsignal.service.internal.push.ProfileAvatarData -import org.session.libsignal.service.internal.push.PushAttachmentData -import org.session.libsignal.service.internal.push.http.DigestingRequestBody -import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.service.loki.api.fileserver.FileServerAPI -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI -import org.session.libsignal.service.loki.api.utilities.HTTP -import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol -import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded -import org.session.libsignal.service.loki.utilities.retryIfNeeded -import org.session.libsignal.utilities.recover -import java.util.* - -/** - * Base class that provides utilities for .NET based APIs. - */ -open class LokiDotNetAPI(internal val userPublicKey: String, private val userPrivateKey: ByteArray, private val apiDatabase: LokiAPIDatabaseProtocol) { - - internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH } - - companion object { - private val authTokenRequestCache = hashMapOf>() - } - - public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?) - - public fun getAuthToken(server: String): Promise { - val token = apiDatabase.getAuthToken(server) - if (token != null) { return Promise.of(token) } - // Avoid multiple token requests to the server by caching - var promise = authTokenRequestCache[server] - if (promise == null) { - promise = requestNewAuthToken(server).bind { submitAuthToken(it, server) }.then { newToken -> - apiDatabase.setAuthToken(server, newToken) - newToken - }.always { - authTokenRequestCache.remove(server) - } - authTokenRequestCache[server] = promise - } - return promise - } - - private fun requestNewAuthToken(server: String): Promise { - Log.d("Loki", "Requesting auth token for server: $server.") - val parameters: Map = mapOf( "pubKey" to userPublicKey ) - return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map(SnodeAPI.sharedContext) { json -> - try { - val base64EncodedChallenge = json["cipherText64"] as String - val challenge = Base64.decode(base64EncodedChallenge) - val base64EncodedServerPublicKey = json["serverPubKey64"] as String - var serverPublicKey = Base64.decode(base64EncodedServerPublicKey) - // Discard the "05" prefix if needed - if (serverPublicKey.count() == 33) { - val hexEncodedServerPublicKey = Hex.toStringCondensed(serverPublicKey) - serverPublicKey = Hex.fromStringCondensed(hexEncodedServerPublicKey.removing05PrefixIfNeeded()) - } - // The challenge is prefixed by the 16 bit IV - val tokenAsData = DiffieHellman.decrypt(challenge, serverPublicKey, userPrivateKey) - val token = tokenAsData.toString(Charsets.UTF_8) - token - } catch (exception: Exception) { - Log.d("Loki", "Couldn't parse auth token for server: $server.") - throw exception - } - } - } - - private fun submitAuthToken(token: String, server: String): Promise { - Log.d("Loki", "Submitting auth token for server: $server.") - val parameters = mapOf( "pubKey" to userPublicKey, "token" to token ) - return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { token } - } - - internal fun execute(verb: HTTPVerb, server: String, endpoint: String, isAuthRequired: Boolean = true, parameters: Map = mapOf(), isJSONRequired: Boolean = true): Promise, Exception> { - fun execute(token: String?): Promise, Exception> { - val sanitizedEndpoint = endpoint.removePrefix("/") - var url = "$server/$sanitizedEndpoint" - if (verb == HTTPVerb.GET || verb == HTTPVerb.DELETE) { - val queryParameters = parameters.map { "${it.key}=${it.value}" }.joinToString("&") - if (queryParameters.isNotEmpty()) { url += "?$queryParameters" } - } - var request = Request.Builder().url(url) - if (isAuthRequired) { - if (token == null) { throw IllegalStateException() } - request = request.header("Authorization", "Bearer $token") - } - when (verb) { - HTTPVerb.GET -> request = request.get() - HTTPVerb.DELETE -> request = request.delete() - else -> { - val parametersAsJSON = JsonUtil.toJson(parameters) - val body = RequestBody.create(MediaType.get("application/json"), parametersAsJSON) - when (verb) { - HTTPVerb.PUT -> request = request.put(body) - HTTPVerb.POST -> request = request.post(body) - HTTPVerb.PATCH -> request = request.patch(body) - else -> throw IllegalStateException() - } - } - } - val serverPublicKeyPromise = if (server == FileServerAPI.shared.server) Promise.of(FileServerAPI.fileServerPublicKey) - else FileServerAPI.shared.getPublicKeyForOpenGroupServer(server) - return serverPublicKeyPromise.bind { serverPublicKey -> - OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, isJSONRequired = isJSONRequired).recover { exception -> - if (exception is HTTP.HTTPRequestFailedException) { - val statusCode = exception.statusCode - if (statusCode == 401 || statusCode == 403) { - apiDatabase.setAuthToken(server, null) - throw SnodeAPI.Error.TokenExpired - } - } - throw exception - } - } - } - return if (isAuthRequired) { - getAuthToken(server).bind { execute(it) } - } else { - execute(null) - } - } - - internal fun getUserProfiles(publicKeys: Set, server: String, includeAnnotations: Boolean): Promise>, Exception> { - val parameters = mapOf( "include_user_annotations" to includeAnnotations.toInt(), "ids" to publicKeys.joinToString { "@$it" } ) - return execute(HTTPVerb.GET, server, "users", parameters = parameters).map { json -> - val data = json["data"] as? List> - if (data == null) { - Log.d("Loki", "Couldn't parse user profiles for: $publicKeys from: $json.") - throw SnodeAPI.Error.ParsingFailed - } - data!! // For some reason the compiler can't infer that this can't be null at this point - } - } - - internal fun setSelfAnnotation(server: String, type: String, newValue: Any?): Promise, Exception> { - val annotation = mutableMapOf( "type" to type ) - if (newValue != null) { annotation["value"] = newValue } - val parameters = mapOf( "annotations" to listOf( annotation ) ) - return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters) - } - - @Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class) - fun uploadAttachment(server: String, attachment: PushAttachmentData): UploadResult { - // This function mimics what Signal does in PushServiceSocket - val contentType = "application/octet-stream" - val file = DigestingRequestBody(attachment.data, attachment.outputStreamFactory, contentType, attachment.dataSize, attachment.listener) - Log.d("Loki", "File size: ${attachment.dataSize} bytes.") - val body = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("type", "network.loki") - .addFormDataPart("Content-Type", contentType) - .addFormDataPart("content", UUID.randomUUID().toString(), file) - .build() - val request = Request.Builder().url("$server/files").post(body) - return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob - val data = json["data"] as? Map<*, *> - if (data == null) { - Log.d("Loki", "Couldn't parse attachment from: $json.") - throw SnodeAPI.Error.ParsingFailed - } - val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong() - val url = data["url"] as? String - if (id == null || url == null || url.isEmpty()) { - Log.d("Loki", "Couldn't parse upload from: $json.") - throw SnodeAPI.Error.ParsingFailed - } - UploadResult(id, url, file.transmittedDigest) - }.get() - } - - @Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class) - fun uploadProfilePicture(server: String, key: ByteArray, profilePicture: StreamDetails, setLastProfilePictureUpload: () -> Unit): UploadResult { - val profilePictureUploadData = ProfileAvatarData(profilePicture.stream, ProfileCipherOutputStream.getCiphertextLength(profilePicture.length), profilePicture.contentType, ProfileCipherOutputStreamFactory(key)) - val file = DigestingRequestBody(profilePictureUploadData.data, profilePictureUploadData.outputStreamFactory, - profilePictureUploadData.contentType, profilePictureUploadData.dataLength, null) - val body = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("type", "network.loki") - .addFormDataPart("Content-Type", "application/octet-stream") - .addFormDataPart("content", UUID.randomUUID().toString(), file) - .build() - val request = Request.Builder().url("$server/files").post(body) - return retryIfNeeded(4) { - upload(server, request) { json -> - val data = json["data"] as? Map<*, *> - if (data == null) { - Log.d("Loki", "Couldn't parse profile picture from: $json.") - throw SnodeAPI.Error.ParsingFailed - } - val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong() - val url = data["url"] as? String - if (id == null || url == null || url.isEmpty()) { - Log.d("Loki", "Couldn't parse profile picture from: $json.") - throw SnodeAPI.Error.ParsingFailed - } - setLastProfilePictureUpload() - UploadResult(id, url, file.transmittedDigest) - } - }.get() - } - - @Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class) - private fun upload(server: String, request: Request.Builder, parse: (Map<*, *>) -> UploadResult): Promise { - val promise: Promise, Exception> - if (server == FileServerAPI.shared.server) { - request.addHeader("Authorization", "Bearer loki") - // Uploads to the Loki File Server shouldn't include any personally identifiable information, so use a dummy auth token - promise = OnionRequestAPI.sendOnionRequest(request.build(), FileServerAPI.shared.server, FileServerAPI.fileServerPublicKey) - } else { - promise = FileServerAPI.shared.getPublicKeyForOpenGroupServer(server).bind { openGroupServerPublicKey -> - getAuthToken(server).bind { token -> - request.addHeader("Authorization", "Bearer $token") - OnionRequestAPI.sendOnionRequest(request.build(), server, openGroupServerPublicKey) - } - } - } - return promise.map { json -> - parse(json) - }.recover { exception -> - if (exception is HTTP.HTTPRequestFailedException) { - val statusCode = exception.statusCode - if (statusCode == 401 || statusCode == 403) { - apiDatabase.setAuthToken(server, null) - } - throw NonSuccessfulResponseCodeException("Request returned with status code ${exception.statusCode}.") - } - throw PushNetworkException(exception) - } - } -} - -private fun Boolean.toInt(): Int { return if (this) 1 else 0 } diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/LokiMessage.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/LokiMessage.kt deleted file mode 100644 index 97e1b4ecf4..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/LokiMessage.kt +++ /dev/null @@ -1,86 +0,0 @@ -package org.session.libsignal.service.loki.api - -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.service.loki.api.crypto.ProofOfWork -import org.session.libsignal.service.loki.utilities.TTLUtilities -import org.session.libsignal.utilities.ThreadUtils -import org.session.libsignal.service.loki.utilities.prettifiedDescription - -internal data class LokiMessage( - /** - * The hex encoded public key of the receiver. - */ - internal val recipientPublicKey: String, - /** - * The content of the message. - */ - internal val data: String, - /** - * The time to live for the message in milliseconds. - */ - internal val ttl: Int, - /** - * Whether this message is a ping. - */ - internal val isPing: Boolean, - /** - * When the proof of work was calculated, if applicable (P2P messages don't require proof of work). - * - * - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970. - */ - internal var timestamp: Long? = null, - /** - * The base 64 encoded proof of work, if applicable (P2P messages don't require proof of work). - */ - internal var nonce: String? = null -) { - - internal companion object { - - internal fun from(message: SignalMessageInfo): LokiMessage? { - try { - val wrappedMessage = MessageWrapper.wrap(message) - val data = Base64.encodeBytes(wrappedMessage) - val destination = message.recipientPublicKey - var ttl = TTLUtilities.fallbackMessageTTL - val messageTTL = message.ttl - if (messageTTL != null && messageTTL != 0) { ttl = messageTTL } - val isPing = message.isPing - return LokiMessage(destination, data, ttl, isPing) - } catch (e: Exception) { - Log.d("Loki", "Failed to convert Signal message to Loki message: ${message.prettifiedDescription()}.") - return null - } - } - } - - @kotlin.ExperimentalUnsignedTypes - internal fun calculatePoW(): Promise { - val deferred = deferred() - // Run PoW in a background thread - ThreadUtils.queue { - val now = System.currentTimeMillis() - val nonce = ProofOfWork.calculate(data, recipientPublicKey, now, ttl) - if (nonce != null ) { - deferred.resolve(copy(nonce = nonce, timestamp = now)) - } else { - deferred.reject(SnodeAPI.Error.ProofOfWorkCalculationFailed) - } - } - return deferred.promise - } - - internal fun toJSON(): Map { - val result = mutableMapOf( "pubKey" to recipientPublicKey, "data" to data, "ttl" to ttl.toString() ) - val timestamp = timestamp - val nonce = nonce - if (timestamp != null && nonce != null) { - result["timestamp"] = timestamp.toString() - result["nonce"] = nonce - } - return result - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/PushNotificationAPI.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/PushNotificationAPI.kt deleted file mode 100644 index 28c370f503..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/PushNotificationAPI.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.session.libsignal.service.loki.api - -import nl.komponents.kovenant.functional.map -import okhttp3.* -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI -import org.session.libsignal.service.loki.utilities.retryIfNeeded - -public class PushNotificationAPI private constructor(public val server: String) { - - companion object { - private val maxRetryCount = 4 - public val pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" - - lateinit var shared: PushNotificationAPI - - public fun configureIfNeeded(isDebugMode: Boolean) { - if (::shared.isInitialized) { return; } - val server = if (isDebugMode) "https://live.apns.getsession.org" else "https://live.apns.getsession.org" - shared = PushNotificationAPI(server) - } - } - - public fun notify(messageInfo: SignalMessageInfo) { - val message = LokiMessage.from(messageInfo) ?: return - val parameters = mapOf( "data" to message.data, "send_to" to message.recipientPublicKey ) - val url = "${server}/notify" - val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) - val request = Request.Builder().url(url).post(body) - retryIfNeeded(maxRetryCount) { - OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.pnServerPublicKey, "/loki/v2/lsrpc").map { json -> - val code = json["code"] as? Int - if (code == null || code == 0) { - Log.d("Loki", "[Loki] Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.") - } - }.fail { exception -> - Log.d("Loki", "[Loki] Couldn't notify PN server due to error: $exception.") - } - } - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/SnodeAPI.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/SnodeAPI.kt deleted file mode 100644 index 48972afab9..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/SnodeAPI.kt +++ /dev/null @@ -1,280 +0,0 @@ -package org.session.libsignal.service.loki.api - -import nl.komponents.kovenant.Kovenant -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.functional.map -import nl.komponents.kovenant.task -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI -import org.session.libsignal.service.loki.api.utilities.HTTP -import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol -import org.session.libsignal.service.loki.utilities.* -import org.session.libsignal.utilities.* -import java.net.ConnectException -import java.net.SocketTimeoutException - -class SnodeAPI private constructor(public var userPublicKey: String, public val database: LokiAPIDatabaseProtocol, public val broadcaster: Broadcaster) { - - companion object { - val messageSendingContext = Kovenant.createContext() - val messagePollingContext = Kovenant.createContext() - /** - * For operations that are shared between message sending and message polling. - */ - val sharedContext = Kovenant.createContext() - - // region Initialization - lateinit var shared: SnodeAPI - - fun configureIfNeeded(userHexEncodedPublicKey: String, database: LokiAPIDatabaseProtocol, broadcaster: Broadcaster) { - if (::shared.isInitialized) { return; } - shared = SnodeAPI(userHexEncodedPublicKey, database, broadcaster) - } - // endregion - - // region Settings - private val maxRetryCount = 6 - private val useOnionRequests = true - - internal var powDifficulty = 1 - // endregion - } - - // region Error - sealed class Error(val description: String) : Exception() { - class HTTPRequestFailed(val code: Int) : Error("HTTP request failed with error code: $code.") - object Generic : Error("An error occurred.") - object ResponseBodyMissing: Error("Response body missing.") - object MessageSigningFailed: Error("Failed to sign message.") - /** - * Only applicable to snode targets as proof of work isn't required for P2P messaging. - */ - object ProofOfWorkCalculationFailed : Error("Failed to calculate proof of work.") - object MessageConversionFailed : Error("Failed to convert Signal message to Loki message.") - object ClockOutOfSync : Error("The user's clock is out of sync with the service node network.") - object SnodeMigrated : Error("The snode previously associated with the given public key has migrated to a different swarm.") - object InsufficientProofOfWork : Error("The proof of work is insufficient.") - object TokenExpired : Error("The auth token being used has expired.") - object ParsingFailed : Error("Couldn't parse JSON.") - } - // endregion - - // region Internal API - /** - * `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance. - */ - internal fun invoke(method: Snode.Method, snode: Snode, publicKey: String, parameters: Map): RawResponsePromise { - val url = "${snode.address}:${snode.port}/storage_rpc/v1" - if (useOnionRequests) { - return OnionRequestAPI.sendOnionRequest(method, parameters, snode, publicKey) - } else { - val deferred = deferred, Exception>() - ThreadUtils.queue { - val payload = mapOf( "method" to method.rawValue, "params" to parameters ) - try { - val json = HTTP.execute(HTTP.Verb.POST, url, payload) - deferred.resolve(json) - } catch (exception: Exception) { - if (exception is ConnectException || exception is SocketTimeoutException) { - dropSnodeIfNeeded(snode, publicKey) - } else { - val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException - if (httpRequestFailedException != null) { - @Suppress("NAME_SHADOWING") val exception = handleSnodeError(httpRequestFailedException.statusCode, httpRequestFailedException.json, snode, publicKey) - return@queue deferred.reject(exception) - } - Log.d("Loki", "Unhandled exception: $exception.") - } - deferred.reject(exception) - } - } - return deferred.promise - } - } - - public fun getRawMessages(snode: Snode, publicKey: String): RawResponsePromise { - val lastHashValue = database.getLastMessageHashValue(snode, publicKey) ?: "" - val parameters = mapOf( "pubKey" to publicKey, "lastHash" to lastHashValue ) - return invoke(Snode.Method.GetMessages, snode, publicKey, parameters) - } - // endregion - - // region Public API - fun getMessages(publicKey: String): MessageListPromise { - return retryIfNeeded(maxRetryCount) { - SwarmAPI.shared.getSingleTargetSnode(publicKey).bind(messagePollingContext) { snode -> - getRawMessages(snode, publicKey).map(messagePollingContext) { parseRawMessagesResponse(it, snode, publicKey) } - } - } - } - - @kotlin.ExperimentalUnsignedTypes - fun sendSignalMessage(message: SignalMessageInfo): Promise, Exception> { - val lokiMessage = LokiMessage.from(message) ?: return task { throw Error.MessageConversionFailed } - val destination = lokiMessage.recipientPublicKey - fun broadcast(event: String) { - val dayInMs = 86400000 - if (message.ttl != dayInMs && message.ttl != 4 * dayInMs) { return } - broadcaster.broadcast(event, message.timestamp) - } - broadcast("calculatingPoW") - return lokiMessage.calculatePoW().bind { lokiMessageWithPoW -> - broadcast("contactingNetwork") - retryIfNeeded(maxRetryCount) { - SwarmAPI.shared.getTargetSnodes(destination).map { swarm -> - swarm.map { snode -> - broadcast("sendingMessage") - val parameters = lokiMessageWithPoW.toJSON() - retryIfNeeded(maxRetryCount) { - invoke(Snode.Method.SendMessage, snode, destination, parameters).map { rawResponse -> - val json = rawResponse as? Map<*, *> - val powDifficulty = json?.get("difficulty") as? Int - if (powDifficulty != null) { - if (powDifficulty != SnodeAPI.powDifficulty && powDifficulty < 100) { - Log.d("Loki", "Setting proof of work difficulty to $powDifficulty (snode: $snode).") - SnodeAPI.powDifficulty = powDifficulty - } - } else { - Log.d("Loki", "Failed to update proof of work difficulty from: ${rawResponse.prettifiedDescription()}.") - } - rawResponse - } - } - }.toSet() - } - } - } - } - // endregion - - // region Parsing - - // The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions. - - public fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List { - val messages = rawResponse["messages"] as? List<*> - if (messages != null) { - updateLastMessageHashValueIfPossible(snode, publicKey, messages) - val newRawMessages = removeDuplicates(publicKey, messages) - return parseEnvelopes(newRawMessages) - } else { - return listOf() - } - } - - private fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>) { - val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *> - val hashValue = lastMessageAsJSON?.get("hash") as? String - val expiration = lastMessageAsJSON?.get("expiration") as? Int - if (hashValue != null) { - database.setLastMessageHashValue(snode, publicKey, hashValue) - } else if (rawMessages.isNotEmpty()) { - Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.") - } - } - - private fun removeDuplicates(publicKey: String, rawMessages: List<*>): List<*> { - val receivedMessageHashValues = database.getReceivedMessageHashValues(publicKey)?.toMutableSet() ?: mutableSetOf() - return rawMessages.filter { rawMessage -> - val rawMessageAsJSON = rawMessage as? Map<*, *> - val hashValue = rawMessageAsJSON?.get("hash") as? String - if (hashValue != null) { - val isDuplicate = receivedMessageHashValues.contains(hashValue) - receivedMessageHashValues.add(hashValue) - database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues) - !isDuplicate - } else { - Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") - false - } - } - } - - private fun parseEnvelopes(rawMessages: List<*>): List { - return rawMessages.mapNotNull { rawMessage -> - val rawMessageAsJSON = rawMessage as? Map<*, *> - val base64EncodedData = rawMessageAsJSON?.get("data") as? String - val data = base64EncodedData?.let { Base64.decode(it) } - if (data != null) { - try { - MessageWrapper.unwrap(data) - } catch (e: Exception) { - Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") - null - } - } else { - Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.") - null - } - } - } - // endregion - - // region Error Handling - private fun dropSnodeIfNeeded(snode: Snode, publicKey: String? = null) { - val oldFailureCount = SwarmAPI.shared.snodeFailureCount[snode] ?: 0 - val newFailureCount = oldFailureCount + 1 - SwarmAPI.shared.snodeFailureCount[snode] = newFailureCount - Log.d("Loki", "Couldn't reach snode at $snode; setting failure count to $newFailureCount.") - if (newFailureCount >= SwarmAPI.snodeFailureThreshold) { - Log.d("Loki", "Failure threshold reached for: $snode; dropping it.") - if (publicKey != null) { - SwarmAPI.shared.dropSnodeFromSwarmIfNeeded(snode, publicKey) - } - SwarmAPI.shared.snodePool = SwarmAPI.shared.snodePool.toMutableSet().minus(snode).toSet() - Log.d("Loki", "Snode pool count: ${SwarmAPI.shared.snodePool.count()}.") - SwarmAPI.shared.snodeFailureCount[snode] = 0 - } - } - - internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Exception { - when (statusCode) { - 400, 500, 503 -> { // Usually indicates that the snode isn't up to date - dropSnodeIfNeeded(snode, publicKey) - return Error.HTTPRequestFailed(statusCode) - } - 406 -> { - Log.d("Loki", "The user's clock is out of sync with the service node network.") - broadcaster.broadcast("clockOutOfSync") - return Error.ClockOutOfSync - } - 421 -> { - // The snode isn't associated with the given public key anymore - if (publicKey != null) { - Log.d("Loki", "Invalidating swarm for: $publicKey.") - SwarmAPI.shared.dropSnodeFromSwarmIfNeeded(snode, publicKey) - } else { - Log.d("Loki", "Got a 421 without an associated public key.") - } - return Error.SnodeMigrated - } - 432 -> { - // The PoW difficulty is too low - val powDifficulty = json?.get("difficulty") as? Int - if (powDifficulty != null && powDifficulty < 100) { - Log.d("Loki", "Setting proof of work difficulty to $powDifficulty (snode: $snode).") - SnodeAPI.powDifficulty = powDifficulty - } else { - Log.d("Loki", "Failed to update proof of work difficulty.") - } - return Error.InsufficientProofOfWork - } - else -> { - dropSnodeIfNeeded(snode, publicKey) - Log.d("Loki", "Unhandled response code: ${statusCode}.") - return Error.Generic - } - } - } - // endregion -} - -// region Convenience -typealias RawResponse = Map<*, *> -typealias MessageListPromise = Promise, Exception> -typealias RawResponsePromise = Promise -// endregion diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/SwarmAPI.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/SwarmAPI.kt deleted file mode 100644 index 26dbf698e6..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/SwarmAPI.kt +++ /dev/null @@ -1,185 +0,0 @@ -package org.session.libsignal.service.loki.api - -import android.os.Build -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.functional.map -import nl.komponents.kovenant.task -import org.session.libsignal.service.loki.api.utilities.HTTP -import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol -import org.session.libsignal.service.loki.utilities.getRandomElement -import org.session.libsignal.service.loki.utilities.prettifiedDescription -import org.session.libsignal.service.loki.utilities.retryIfNeeded -import org.session.libsignal.utilities.ThreadUtils -import org.session.libsignal.utilities.logging.Log -import java.security.SecureRandom -import java.util.* - -class SwarmAPI private constructor(private val database: LokiAPIDatabaseProtocol) { - internal var snodeFailureCount: MutableMap = mutableMapOf() - - internal var snodePool: Set - get() = database.getSnodePool() - set(newValue) { database.setSnodePool(newValue) } - - companion object { - - // use port 4433 if API level can handle network security config and enforce pinned certificates - private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433 - private val seedNodePool: Set = setOf( - "https://storage.seed1.loki.network:$seedPort", - "https://storage.seed3.loki.network:$seedPort", - "https://public.loki.foundation:$seedPort" - ) - - // region Settings - private val minimumSnodePoolCount = 64 - private val minimumSwarmSnodeCount = 2 - private val targetSwarmSnodeCount = 2 - private val maxRetryCount = 6 - - /** - * A snode is kicked out of a swarm and/or the snode pool if it fails this many times. - */ - internal val snodeFailureThreshold = 2 - // endregion - - // region Initialization - lateinit var shared: SwarmAPI - - fun configureIfNeeded(database: LokiAPIDatabaseProtocol) { - if (::shared.isInitialized) { return; } - shared = SwarmAPI(database) - } - // endregion - } - - // region Swarm API - internal fun getRandomSnode(): Promise { - val snodePool = this.snodePool - val lastRefreshDate = database.getLastSnodePoolRefreshDate() - val now = Date() - val needsRefresh = (snodePool.count() < minimumSnodePoolCount) || lastRefreshDate == null || (now.time - lastRefreshDate.time) > 24 * 60 * 60 * 1000 - if (needsRefresh) { - database.setLastSnodePoolRefreshDate(now) - - val target = seedNodePool.random() - val url = "$target/json_rpc" - Log.d("Loki", "Populating snode pool using: $target.") - val parameters = mapOf( - "method" to "get_n_service_nodes", - "params" to mapOf( - "active_only" to true, - "fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true ) - ) - ) - val deferred = deferred() - deferred(SnodeAPI.sharedContext) - ThreadUtils.queue { - try { - val json = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true) - val intermediate = json["result"] as? Map<*, *> - val rawSnodes = intermediate?.get("service_node_states") as? List<*> - if (rawSnodes != null) { - @Suppress("NAME_SHADOWING") val snodePool = rawSnodes.mapNotNull { rawSnode -> - val rawSnodeAsJSON = rawSnode as? Map<*, *> - val address = rawSnodeAsJSON?.get("public_ip") as? String - val port = rawSnodeAsJSON?.get("storage_port") as? Int - val ed25519Key = rawSnodeAsJSON?.get("pubkey_ed25519") as? String - val x25519Key = rawSnodeAsJSON?.get("pubkey_x25519") as? String - if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") { - Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key)) - } else { - Log.d("Loki", "Failed to parse: ${rawSnode?.prettifiedDescription()}.") - null - } - }.toMutableSet() - Log.d("Loki", "Persisting snode pool to database.") - this.snodePool = snodePool - try { - deferred.resolve(snodePool.getRandomElement()) - } catch (exception: Exception) { - Log.d("Loki", "Got an empty snode pool from: $target.") - deferred.reject(SnodeAPI.Error.Generic) - } - } else { - Log.d("Loki", "Failed to update snode pool from: ${(rawSnodes as List<*>?)?.prettifiedDescription()}.") - deferred.reject(SnodeAPI.Error.Generic) - } - } catch (exception: Exception) { - deferred.reject(exception) - } - } - return deferred.promise - } else { - return Promise.of(snodePool.getRandomElement()) - } - } - - public fun getSwarm(publicKey: String): Promise, Exception> { - val cachedSwarm = database.getSwarm(publicKey) - if (cachedSwarm != null && cachedSwarm.size >= minimumSwarmSnodeCount) { - val cachedSwarmCopy = mutableSetOf() // Workaround for a Kotlin compiler issue - cachedSwarmCopy.addAll(cachedSwarm) - return task { cachedSwarmCopy } - } else { - val parameters = mapOf( "pubKey" to publicKey ) - return getRandomSnode().bind { - retryIfNeeded(maxRetryCount) { - SnodeAPI.shared.invoke(Snode.Method.GetSwarm, it, publicKey, parameters) - } - - }.map(SnodeAPI.sharedContext) { - parseSnodes(it).toSet() - }.success { - database.setSwarm(publicKey, it) - } - } - } - - internal fun dropSnodeFromSwarmIfNeeded(snode: Snode, publicKey: String) { - val swarm = database.getSwarm(publicKey)?.toMutableSet() - if (swarm != null && swarm.contains(snode)) { - swarm.remove(snode) - database.setSwarm(publicKey, swarm) - } - } - - internal fun getSingleTargetSnode(publicKey: String): Promise { - // SecureRandom() should be cryptographically secure - return getSwarm(publicKey).map { it.shuffled(SecureRandom()).random() } - } - - internal fun getTargetSnodes(publicKey: String): Promise, Exception> { - // SecureRandom() should be cryptographically secure - return getSwarm(publicKey).map { it.shuffled(SecureRandom()).take(targetSwarmSnodeCount) } - } - // endregion - - // region Parsing - private fun parseSnodes(rawResponse: Any): List { - val json = rawResponse as? Map<*, *> - val rawSnodes = json?.get("snodes") as? List<*> - if (rawSnodes != null) { - return rawSnodes.mapNotNull { rawSnode -> - val rawSnodeAsJSON = rawSnode as? Map<*, *> - val address = rawSnodeAsJSON?.get("ip") as? String - val portAsString = rawSnodeAsJSON?.get("port") as? String - val port = portAsString?.toInt() - val ed25519Key = rawSnodeAsJSON?.get("pubkey_ed25519") as? String - val x25519Key = rawSnodeAsJSON?.get("pubkey_x25519") as? String - if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") { - Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key)) - } else { - Log.d("Loki", "Failed to parse snode from: ${rawSnode?.prettifiedDescription()}.") - null - } - } - } else { - Log.d("Loki", "Failed to parse snodes from: ${rawResponse.prettifiedDescription()}.") - return listOf() - } - } - // endregion -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/ProofOfWork.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/ProofOfWork.kt deleted file mode 100644 index f1bfc59cc6..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/ProofOfWork.kt +++ /dev/null @@ -1,64 +0,0 @@ -package org.session.libsignal.service.loki.api.crypto - -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.service.loki.api.SnodeAPI -import java.math.BigInteger -import java.nio.ByteBuffer -import java.security.MessageDigest - -/** - * Based on the desktop messenger's proof of work implementation. For more information, see libloki/proof-of-work.js. - */ -object ProofOfWork { - - // region Settings - private val nonceSize = 8 - // endregion - - // region Implementation - @kotlin.ExperimentalUnsignedTypes - fun calculate(data: String, hexEncodedPublicKey: String, timestamp: Long, ttl: Int): String? { - try { - val sha512 = MessageDigest.getInstance("SHA-512") - val payloadAsString = timestamp.toString() + ttl.toString() + hexEncodedPublicKey + data - val payload = payloadAsString.toByteArray() - val target = determineTarget(ttl, payload.size) - var currentTrialValue = ULong.MAX_VALUE - var nonce: Long = 0 - val initialHash = sha512.digest(payload) - while (currentTrialValue > target) { - nonce += 1 - // This is different from bitmessage's PoW implementation - // newHash = hash(nonce + hash(data)) → hash(nonce + initialHash) - val newHash = sha512.digest(nonce.toByteArray() + initialHash) - currentTrialValue = newHash.sliceArray(0 until nonceSize).toULong() - } - return Base64.encodeBytes(nonce.toByteArray()) - } catch (e: Exception) { - Log.d("Loki", "Couldn't calculate proof of work due to error: $e.") - return null - } - } - - @kotlin.ExperimentalUnsignedTypes - private fun determineTarget(ttl: Int, payloadSize: Int): ULong { - val x1 = BigInteger.valueOf(2).pow(16) - 1.toBigInteger() - val x2 = BigInteger.valueOf(2).pow(64) - 1.toBigInteger() - val size = (payloadSize + nonceSize).toBigInteger() - val ttlInSeconds = (ttl / 1000).toBigInteger() - val x3 = (ttlInSeconds * size) / x1 - val x4 = size + x3 - val x5 = SnodeAPI.powDifficulty.toBigInteger() * x4 - return (x2 / x5).toULong() - } - // endregion -} - -// region Convenience -@kotlin.ExperimentalUnsignedTypes -private fun BigInteger.toULong() = toLong().toULong() -private fun Long.toByteArray() = ByteBuffer.allocate(8).putLong(this).array() -@kotlin.ExperimentalUnsignedTypes -private fun ByteArray.toULong() = ByteBuffer.wrap(this).long.toULong() -// endregion diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/fileserver/FileServerAPI.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/fileserver/FileServerAPI.kt deleted file mode 100644 index 2e63a76564..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/fileserver/FileServerAPI.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.session.libsignal.service.loki.api.fileserver - -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.map -import okhttp3.Request -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.service.loki.api.LokiDotNetAPI -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI -import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol -import org.session.libsignal.service.loki.utilities.* -import java.net.URL -import java.util.concurrent.ConcurrentHashMap - -class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : LokiDotNetAPI(userPublicKey, userPrivateKey, database) { - - companion object { - // region Settings - internal val fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C" - internal val maxRetryCount = 4 - - public val maxFileSize = 10_000_000 // 10 MB - /** - * The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes - * is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP - * request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also - * be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when - * uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only - * possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds. - */ - public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5? - public val fileStorageBucketURL = "https://file-static.lokinet.org" - // endregion - - // region Initialization - lateinit var shared: FileServerAPI - - /** - * Must be called before `LokiAPI` is used. - */ - fun configure(userPublicKey: String, userPrivateKey: ByteArray, database: LokiAPIDatabaseProtocol) { - if (Companion::shared.isInitialized) { return } - val server = "https://file.getsession.org" - shared = FileServerAPI(server, userPublicKey, userPrivateKey, database) - } - // endregion - } - - // region Open Group Server Public Key - fun getPublicKeyForOpenGroupServer(openGroupServer: String): Promise { - val publicKey = database.getOpenGroupPublicKey(openGroupServer) - if (publicKey != null && PublicKeyValidation.isValid(publicKey, 64, false)) { - return Promise.of(publicKey) - } else { - val url = "$server/loki/v1/getOpenGroupKey/${URL(openGroupServer).host}" - val request = Request.Builder().url(url) - request.addHeader("Content-Type", "application/json") - request.addHeader("Authorization", "Bearer loki") // Tokenless request; use a dummy token - return OnionRequestAPI.sendOnionRequest(request.build(), server, fileServerPublicKey).map { json -> - try { - val bodyAsString = json["data"] as String - val body = JsonUtil.fromJson(bodyAsString) - val base64EncodedPublicKey = body.get("data").asText() - val prefixedPublicKey = Base64.decode(base64EncodedPublicKey) - val hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString() - val result = hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded() - database.setOpenGroupPublicKey(openGroupServer, result) - result - } catch (exception: Exception) { - Log.d("Loki", "Couldn't parse open group public key from: $json.") - throw exception - } - } - } - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/onionrequests/OnionRequestAPI.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/onionrequests/OnionRequestAPI.kt deleted file mode 100644 index efeab36fa4..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/onionrequests/OnionRequestAPI.kt +++ /dev/null @@ -1,460 +0,0 @@ -package org.session.libsignal.service.loki.api.onionrequests - -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.all -import nl.komponents.kovenant.deferred -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.functional.map -import okhttp3.Request -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.service.loki.api.* -import org.session.libsignal.service.loki.api.fileserver.FileServerAPI -import org.session.libsignal.service.loki.api.utilities.* -import org.session.libsignal.service.loki.api.utilities.EncryptionResult -import org.session.libsignal.service.loki.api.utilities.getBodyForOnionRequest -import org.session.libsignal.service.loki.api.utilities.getHeadersForOnionRequest -import org.session.libsignal.service.loki.utilities.* -import org.session.libsignal.utilities.* - -private typealias Path = List - -/** - * See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. - */ -public object OnionRequestAPI { - private val pathFailureCount = mutableMapOf() - private val snodeFailureCount = mutableMapOf() - public var guardSnodes = setOf() - public var paths: List // Not a set to ensure we consistently show the same path to the user - get() = SnodeAPI.shared.database.getOnionRequestPaths() - set(newValue) { - if (newValue.isEmpty()) { - SnodeAPI.shared.database.clearOnionRequestPaths() - } else { - SnodeAPI.shared.database.setOnionRequestPaths(newValue) - } - } - - // region Settings - /** - * The number of snodes (including the guard snode) in a path. - */ - private val pathSize = 3 - /** - * The number of times a path can fail before it's replaced. - */ - private val pathFailureThreshold = 1 - /** - * The number of times a snode can fail before it's replaced. - */ - private val snodeFailureThreshold = 1 - /** - * The number of paths to maintain. - */ - public val targetPathCount = 2 // A main path and a backup path for the case where the target snode is in the main path - - /** - * The number of guard snodes required to maintain `targetPathCount` paths. - */ - private val targetGuardSnodeCount - get() = targetPathCount // One per path - // endregion - - class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>) - : Exception("HTTP request failed at destination with status code $statusCode.") - class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.") - - private data class OnionBuildingResult( - internal val guardSnode: Snode, - internal val finalEncryptionResult: EncryptionResult, - internal val destinationSymmetricKey: ByteArray - ) - - internal sealed class Destination { - class Snode(val snode: org.session.libsignal.service.loki.api.Snode) : Destination() - class Server(val host: String, val target: String, val x25519PublicKey: String) : Destination() - } - - // region Private API - /** - * Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. - */ - private fun testSnode(snode: Snode): Promise { - val deferred = deferred() - ThreadUtils.queue { // No need to block the shared context for this - val url = "${snode.address}:${snode.port}/get_stats/v1" - try { - val json = HTTP.execute(HTTP.Verb.GET, url) - val version = json["version"] as? String - if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue } - if (version >= "2.0.7") { - deferred.resolve(Unit) - } else { - val message = "Unsupported snode version: $version." - Log.d("Loki", message) - deferred.reject(Exception(message)) - } - } catch (exception: Exception) { - deferred.reject(exception) - } - } - return deferred.promise - } - - /** - * Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out if not - * enough (reliable) snodes are available. - */ - private fun getGuardSnodes(reusableGuardSnodes: List): Promise, Exception> { - if (guardSnodes.count() >= targetGuardSnodeCount) { - return Promise.of(guardSnodes) - } else { - Log.d("Loki", "Populating guard snode cache.") - return SwarmAPI.shared.getRandomSnode().bind(SnodeAPI.sharedContext) { // Just used to populate the snode pool - var unusedSnodes = SwarmAPI.shared.snodePool.minus(reusableGuardSnodes) - val reusableGuardSnodeCount = reusableGuardSnodes.count() - if (unusedSnodes.count() < (targetGuardSnodeCount - reusableGuardSnodeCount)) { throw InsufficientSnodesException() } - fun getGuardSnode(): Promise { - val candidate = unusedSnodes.getRandomElementOrNull() - ?: return Promise.ofFail(InsufficientSnodesException()) - unusedSnodes = unusedSnodes.minus(candidate) - Log.d("Loki", "Testing guard snode: $candidate.") - // Loop until a reliable guard snode is found - val deferred = deferred() - testSnode(candidate).success { - deferred.resolve(candidate) - }.fail { - getGuardSnode().success { - deferred.resolve(candidate) - }.fail { exception -> - if (exception is InsufficientSnodesException) { - deferred.reject(exception) - } - } - } - return deferred.promise - } - val promises = (0 until (targetGuardSnodeCount - reusableGuardSnodeCount)).map { getGuardSnode() } - all(promises).map(SnodeAPI.sharedContext) { guardSnodes -> - val guardSnodesAsSet = (guardSnodes + reusableGuardSnodes).toSet() - OnionRequestAPI.guardSnodes = guardSnodesAsSet - guardSnodesAsSet - } - } - } - } - - /** - * Builds and returns `targetPathCount` paths. The returned promise errors out if not - * enough (reliable) snodes are available. - */ - private fun buildPaths(reusablePaths: List): Promise, Exception> { - Log.d("Loki", "Building onion request paths.") - SnodeAPI.shared.broadcaster.broadcast("buildingPaths") - return SwarmAPI.shared.getRandomSnode().bind(SnodeAPI.sharedContext) { // Just used to populate the snode pool - val reusableGuardSnodes = reusablePaths.map { it[0] } - getGuardSnodes(reusableGuardSnodes).map(SnodeAPI.sharedContext) { guardSnodes -> - var unusedSnodes = SwarmAPI.shared.snodePool.minus(guardSnodes).minus(reusablePaths.flatten()) - val reusableGuardSnodeCount = reusableGuardSnodes.count() - val pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount) - if (unusedSnodes.count() < pathSnodeCount) { throw InsufficientSnodesException() } - // Don't test path snodes as this would reveal the user's IP to them - guardSnodes.minus(reusableGuardSnodes).map { guardSnode -> - val result = listOf( guardSnode ) + (0 until (pathSize - 1)).map { - val pathSnode = unusedSnodes.getRandomElement() - unusedSnodes = unusedSnodes.minus(pathSnode) - pathSnode - } - Log.d("Loki", "Built new onion request path: $result.") - result - } - }.map { paths -> - OnionRequestAPI.paths = paths + reusablePaths - SnodeAPI.shared.broadcaster.broadcast("pathsBuilt") - paths - } - } - } - - /** - * Returns a `Path` to be used for building an onion request. Builds new paths as needed. - */ - private fun getPath(snodeToExclude: Snode?): Promise { - if (pathSize < 1) { throw Exception("Can't build path of size zero.") } - val paths = this.paths - val guardSnodes = mutableSetOf() - if (paths.isNotEmpty()) { - guardSnodes.add(paths[0][0]) - if (paths.count() >= 2) { - guardSnodes.add(paths[1][0]) - } - } - OnionRequestAPI.guardSnodes = guardSnodes - fun getPath(paths: List): Path { - if (snodeToExclude != null) { - return paths.filter { !it.contains(snodeToExclude) }.getRandomElement() - } else { - return paths.getRandomElement() - } - } - if (paths.count() >= targetPathCount) { - return Promise.of(getPath(paths)) - } else if (paths.isNotEmpty()) { - if (paths.any { !it.contains(snodeToExclude) }) { - buildPaths(paths) // Re-build paths in the background - return Promise.of(getPath(paths)) - } else { - return buildPaths(paths).map(SnodeAPI.sharedContext) { newPaths -> - getPath(newPaths) - } - } - } else { - return buildPaths(listOf()).map(SnodeAPI.sharedContext) { newPaths -> - getPath(newPaths) - } - } - } - - private fun dropGuardSnode(snode: Snode) { - guardSnodes = guardSnodes.filter { it != snode }.toSet() - } - - private fun dropSnode(snode: Snode) { - // We repair the path here because we can do it sync. In the case where we drop a whole - // path we leave the re-building up to getPath() because re-building the path in that case - // is async. - snodeFailureCount[snode] = 0 - val oldPaths = paths.toMutableList() - val pathIndex = oldPaths.indexOfFirst { it.contains(snode) } - if (pathIndex == -1) { return } - val path = oldPaths[pathIndex].toMutableList() - val snodeIndex = path.indexOf(snode) - if (snodeIndex == -1) { return } - path.removeAt(snodeIndex) - val unusedSnodes = SwarmAPI.shared.snodePool.minus(oldPaths.flatten()) - if (unusedSnodes.isEmpty()) { throw InsufficientSnodesException() } - path.add(unusedSnodes.getRandomElement()) - // Don't test the new snode as this would reveal the user's IP - oldPaths.removeAt(pathIndex) - val newPaths = oldPaths + listOf( path ) - paths = newPaths - } - - private fun dropPath(path: Path) { - pathFailureCount[path] = 0 - val paths = OnionRequestAPI.paths.toMutableList() - val pathIndex = paths.indexOf(path) - if (pathIndex == -1) { return } - paths.removeAt(pathIndex) - OnionRequestAPI.paths = paths - } - - /** - * Builds an onion around `payload` and returns the result. - */ - private fun buildOnionForDestination(payload: Map<*, *>, destination: Destination): Promise { - lateinit var guardSnode: Snode - lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination - lateinit var encryptionResult: EncryptionResult - val snodeToExclude = when (destination) { - is Destination.Snode -> destination.snode - is Destination.Server -> null - } - return getPath(snodeToExclude).bind(SnodeAPI.sharedContext) { path -> - guardSnode = path.first() - // Encrypt in reverse order, i.e. the destination first - OnionRequestEncryption.encryptPayloadForDestination(payload, destination).bind(SnodeAPI.sharedContext) { r -> - destinationSymmetricKey = r.symmetricKey - // Recursively encrypt the layers of the onion (again in reverse order) - encryptionResult = r - @Suppress("NAME_SHADOWING") var path = path - var rhs = destination - fun addLayer(): Promise { - if (path.isEmpty()) { - return Promise.of(encryptionResult) - } else { - val lhs = Destination.Snode(path.last()) - path = path.dropLast(1) - return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind(SnodeAPI.sharedContext) { r -> - encryptionResult = r - rhs = lhs - addLayer() - } - } - } - addLayer() - } - }.map(SnodeAPI.sharedContext) { OnionBuildingResult(guardSnode, encryptionResult, destinationSymmetricKey) } - } - - /** - * Sends an onion request to `destination`. Builds new paths as needed. - */ - private fun sendOnionRequest(destination: Destination, payload: Map<*, *>, isJSONRequired: Boolean = true): Promise, Exception> { - val deferred = deferred, Exception>() - var guardSnode: Snode? = null - buildOnionForDestination(payload, destination).success { result -> - guardSnode = result.guardSnode - val url = "${guardSnode!!.address}:${guardSnode!!.port}/onion_req/v2" - val finalEncryptionResult = result.finalEncryptionResult - val onion = finalEncryptionResult.ciphertext - if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPI.maxFileSize.toDouble()) { - Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.") - } - @Suppress("NAME_SHADOWING") val parameters = mapOf( - "ephemeral_key" to finalEncryptionResult.ephemeralPublicKey.toHexString() - ) - val body: ByteArray - try { - body = OnionRequestEncryption.encode(onion, parameters) - } catch (exception: Exception) { - return@success deferred.reject(exception) - } - val destinationSymmetricKey = result.destinationSymmetricKey - ThreadUtils.queue { - try { - val json = HTTP.execute(HTTP.Verb.POST, url, body) - val base64EncodedIVAndCiphertext = json["result"] as? String ?: return@queue deferred.reject(Exception("Invalid JSON")) - val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext) - try { - val plaintext = DecryptionUtilities.decryptUsingAESGCM(ivAndCiphertext, destinationSymmetricKey) - try { - @Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java) - val statusCode = json["status"] as Int - if (statusCode == 406) { - @Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." ) - val exception = HTTPRequestFailedAtDestinationException(statusCode, body) - return@queue deferred.reject(exception) - } else if (json["body"] != null) { - @Suppress("NAME_SHADOWING") val body: Map<*, *> - if (json["body"] is Map<*, *>) { - body = json["body"] as Map<*, *> - } else { - val bodyAsString = json["body"] as String - if (!isJSONRequired) { - body = mapOf( "result" to bodyAsString ) - } else { - body = JsonUtil.fromJson(bodyAsString, Map::class.java) - } - } - if (statusCode != 200) { - val exception = HTTPRequestFailedAtDestinationException(statusCode, body) - return@queue deferred.reject(exception) - } - deferred.resolve(body) - } else { - if (statusCode != 200) { - val exception = HTTPRequestFailedAtDestinationException(statusCode, json) - return@queue deferred.reject(exception) - } - deferred.resolve(json) - } - } catch (exception: Exception) { - deferred.reject(Exception("Invalid JSON: ${plaintext.toString(Charsets.UTF_8)}.")) - } - } catch (exception: Exception) { - deferred.reject(exception) - } - } catch (exception: Exception) { - deferred.reject(exception) - } - } - }.fail { exception -> - deferred.reject(exception) - } - val promise = deferred.promise - promise.fail { exception -> - val path = if (guardSnode != null) paths.firstOrNull { it.contains(guardSnode!!) } else null - if (exception is HTTP.HTTPRequestFailedException) { - fun handleUnspecificError() { - if (path == null) { return } - var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?: 0 - pathFailureCount += 1 - if (pathFailureCount >= pathFailureThreshold) { - dropGuardSnode(guardSnode!!) - path.forEach { snode -> - @Suppress("ThrowableNotThrown") - SnodeAPI.shared.handleSnodeError(exception.statusCode, exception.json, snode, null) // Intentionally don't throw - } - dropPath(path) - } else { - OnionRequestAPI.pathFailureCount[path] = pathFailureCount - } - } - val json = exception.json - val message = json?.get("result") as? String - val prefix = "Next node not found: " - if (message != null && message.startsWith(prefix)) { - val ed25519PublicKey = message.substringAfter(prefix) - val snode = path?.firstOrNull { it.publicKeySet!!.ed25519Key == ed25519PublicKey } - if (snode != null) { - var snodeFailureCount = OnionRequestAPI.snodeFailureCount[snode] ?: 0 - snodeFailureCount += 1 - if (snodeFailureCount >= snodeFailureThreshold) { - @Suppress("ThrowableNotThrown") - SnodeAPI.shared.handleSnodeError(exception.statusCode, json, snode, null) // Intentionally don't throw - try { - dropSnode(snode) - } catch (exception: Exception) { - handleUnspecificError() - } - } else { - OnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount - } - } else { - handleUnspecificError() - } - } else if (message == "Loki Server error") { - // Do nothing - } else { - handleUnspecificError() - } - } - } - return promise - } - // endregion - - // region Internal API - /** - * Sends an onion request to `snode`. Builds new paths as needed. - */ - internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String): Promise, Exception> { - val payload = mapOf( "method" to method.rawValue, "params" to parameters ) - return sendOnionRequest(Destination.Snode(snode), payload).recover { exception -> - @Suppress("NAME_SHADOWING") val exception = exception as? HTTPRequestFailedAtDestinationException ?: throw exception - throw SnodeAPI.shared.handleSnodeError(exception.statusCode, exception.json, snode, publicKey) - } - } - - /** - * Sends an onion request to `server`. Builds new paths as needed. - * - * `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance. - */ - public fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, target: String = "/loki/v3/lsrpc", isJSONRequired: Boolean = true): Promise, Exception> { - val headers = request.getHeadersForOnionRequest() - val url = request.url() - val urlAsString = url.toString() - val host = url.host() - val endpoint = when { - server.count() < urlAsString.count() -> urlAsString.substringAfter("$server/") - else -> "" - } - val body = request.getBodyForOnionRequest() ?: "null" - val payload = mapOf( - "body" to body, - "endpoint" to endpoint, - "method" to request.method(), - "headers" to headers - ) - val destination = Destination.Server(host, target, x25519PublicKey) - return sendOnionRequest(destination, payload, isJSONRequired).recover { exception -> - Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.") - throw exception - } - } - // endregion -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/onionrequests/OnionRequestEncryption.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/onionrequests/OnionRequestEncryption.kt deleted file mode 100644 index 63f1c9636e..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/onionrequests/OnionRequestEncryption.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.session.libsignal.service.loki.api.onionrequests - -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.service.loki.api.utilities.EncryptionResult -import org.session.libsignal.service.loki.api.utilities.EncryptionUtilities -import org.session.libsignal.utilities.ThreadUtils -import org.session.libsignal.service.loki.utilities.toHexString -import java.nio.Buffer -import java.nio.ByteBuffer -import java.nio.ByteOrder - -object OnionRequestEncryption { - - internal fun encode(ciphertext: ByteArray, json: Map<*, *>): ByteArray { - // The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 | - val jsonAsData = JsonUtil.toJson(json).toByteArray() - val ciphertextSize = ciphertext.size - val buffer = ByteBuffer.allocate(Int.SIZE_BYTES) - buffer.order(ByteOrder.LITTLE_ENDIAN) - buffer.putInt(ciphertextSize) - val ciphertextSizeAsData = ByteArray(buffer.capacity()) - // Casting here avoids an issue where this gets compiled down to incorrect byte code. See - // https://github.com/eclipse/jetty.project/issues/3244 for more info - (buffer as Buffer).position(0) - buffer.get(ciphertextSizeAsData) - return ciphertextSizeAsData + ciphertext + jsonAsData - } - - /** - * Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. - */ - internal fun encryptPayloadForDestination(payload: Map<*, *>, destination: OnionRequestAPI.Destination): Promise { - val deferred = deferred() - ThreadUtils.queue { - try { - // Wrapping isn't needed for file server or open group onion requests - when (destination) { - is OnionRequestAPI.Destination.Snode -> { - val snodeX25519PublicKey = destination.snode.publicKeySet!!.x25519Key - val payloadAsData = JsonUtil.toJson(payload).toByteArray() - val plaintext = encode(payloadAsData, mapOf( "headers" to "" )) - val result = EncryptionUtilities.encryptForX25519PublicKey(plaintext, snodeX25519PublicKey) - deferred.resolve(result) - } - is OnionRequestAPI.Destination.Server -> { - val plaintext = JsonUtil.toJson(payload).toByteArray() - val result = EncryptionUtilities.encryptForX25519PublicKey(plaintext, destination.x25519PublicKey) - deferred.resolve(result) - } - } - } catch (exception: Exception) { - deferred.reject(exception) - } - } - return deferred.promise - } - - /** - * Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request. - */ - internal fun encryptHop(lhs: OnionRequestAPI.Destination, rhs: OnionRequestAPI.Destination, previousEncryptionResult: EncryptionResult): Promise { - val deferred = deferred() - ThreadUtils.queue { - try { - val payload: MutableMap - when (rhs) { - is OnionRequestAPI.Destination.Snode -> { - payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key ) - } - is OnionRequestAPI.Destination.Server -> { - payload = mutableMapOf( "host" to rhs.host, "target" to rhs.target, "method" to "POST" ) - } - } - payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() - val x25519PublicKey: String - when (lhs) { - is OnionRequestAPI.Destination.Snode -> { - x25519PublicKey = lhs.snode.publicKeySet!!.x25519Key - } - is OnionRequestAPI.Destination.Server -> { - x25519PublicKey = lhs.x25519PublicKey - } - } - val plaintext = encode(previousEncryptionResult.ciphertext, payload) - val result = EncryptionUtilities.encryptForX25519PublicKey(plaintext, x25519PublicKey) - deferred.resolve(result) - } catch (exception: Exception) { - deferred.reject(exception) - } - } - return deferred.promise - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt deleted file mode 100644 index a582c0fa31..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt +++ /dev/null @@ -1,88 +0,0 @@ -package org.session.libsignal.service.loki.utilities - -import okhttp3.HttpUrl -import okhttp3.Request -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.service.api.messages.SignalServiceAttachment -import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException -import org.session.libsignal.service.api.push.exceptions.PushNetworkException -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.service.loki.api.fileserver.FileServerAPI -import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI -import java.io.* - -object DownloadUtilities { - - /** - * Blocks the calling thread. - */ - @JvmStatic - fun downloadFile(destination: File, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { - val outputStream = FileOutputStream(destination) // Throws - var remainingAttempts = 4 - var exception: Exception? = null - while (remainingAttempts > 0) { - remainingAttempts -= 1 - try { - downloadFile(outputStream, url, maxSize, listener) - exception = null - break - } catch (e: Exception) { - exception = e - } - } - if (exception != null) { throw exception } - } - - /** - * Blocks the calling thread. - */ - @JvmStatic - fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { - // We need to throw a PushNetworkException or NonSuccessfulResponseCodeException - // because the underlying Signal logic requires these to work correctly - val oldPrefixedHost = "https://" + HttpUrl.get(url).host() - var newPrefixedHost = oldPrefixedHost - if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) { - newPrefixedHost = FileServerAPI.shared.server - } - // Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM - // → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35 - val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/") - val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID" - val request = Request.Builder().url(sanitizedURL).get() - try { - val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey - else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get() - val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get() - val result = json["result"] as? String - if (result == null) { - Log.d("Loki", "Couldn't parse attachment from: $json.") - throw PushNetworkException("Missing response body.") - } - val body = Base64.decode(result) - if (body.size > maxSize) { - Log.d("Loki", "Attachment size limit exceeded.") - throw PushNetworkException("Max response size exceeded.") - } - body.inputStream().use { input -> - val buffer = ByteArray(32768) - var count = 0 - var bytes = input.read(buffer) - while (bytes >= 0) { - outputStream.write(buffer, 0, bytes) - count += bytes - if (count > maxSize) { - Log.d("Loki", "Attachment size limit exceeded.") - throw PushNetworkException("Max response size exceeded.") - } - listener?.onAttachmentProgress(body.size.toLong(), count.toLong()) - bytes = input.read(buffer) - } - } - } catch (e: Exception) { - Log.d("Loki", "Couldn't download attachment due to error: $e.") - throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e) - } - } -}