Use V2 file server for profile pictures

Also don't randomly rotate profile key
This commit is contained in:
Niels Andriesse 2021-05-13 14:24:27 +10:00
parent 3e1727fdbc
commit d83c257491
5 changed files with 102 additions and 55 deletions

View File

@ -42,6 +42,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.session.libsession.snode.SnodeModule; import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.IdentityKeyUtil; import org.session.libsession.utilities.IdentityKeyUtil;
import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
@ -50,6 +51,7 @@ import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsession.utilities.preferences.ProfileKeyUtil; import org.session.libsession.utilities.preferences.ProfileKeyUtil;
import org.session.libsignal.service.api.util.StreamDetails; import org.session.libsignal.service.api.util.StreamDetails;
import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol; import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
import org.session.libsignal.utilities.ThreadUtils;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.signal.aesgcmprovider.AesGcmProvider; import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.components.TypingStatusSender;
@ -91,8 +93,10 @@ import org.webrtc.PeerConnectionFactory.InitializationOptions;
import org.webrtc.voiceengine.WebRtcAudioManager; import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils; import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.util.Date; import java.util.Date;
@ -101,6 +105,7 @@ import java.util.Set;
import dagger.ObjectGraph; import dagger.ObjectGraph;
import kotlin.Unit; import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import kotlinx.coroutines.Job; import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
@ -481,21 +486,31 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
} }
private void resubmitProfilePictureIfNeeded() { private void resubmitProfilePictureIfNeeded() {
// Files expire on the file server after a while, so we simply re-upload the user's profile picture
// at a certain interval to ensure it's always available.
String userPublicKey = TextSecurePreferences.getLocalNumber(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return; if (userPublicKey == null) return;
long now = new Date().getTime(); long now = new Date().getTime();
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this); long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return; if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
AsyncTask.execute(() -> { ThreadUtils.queue(() -> {
String encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this); // Don't generate a new profile key here; we do that when the user changes their profile picture
byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey); String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
try { try {
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey)); // Read the file into a byte array
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length()); InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey));
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> { ByteArrayOutputStream baos = new ByteArrayOutputStream();
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime()); int count;
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt()); byte[] buffer = new byte[1024];
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey); while ((count = inputStream.read(buffer, 0, buffer.length)) != -1) {
baos.write(buffer, 0, count);
}
baos.flush();
byte[] profilePicture = baos.toByteArray();
// Re-upload it
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
// Update the last profile picture upload date
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
return Unit.INSTANCE; return Unit.INSTANCE;
}); });
} catch (Exception exception) { } catch (Exception exception) {

View File

@ -23,18 +23,14 @@ import network.loki.messenger.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.alwaysUi
import org.session.libsession.messaging.avatars.AvatarHelper import org.session.libsession.messaging.avatars.AvatarHelper
import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.open_groups.OpenGroupAPI import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.preferences.ProfileKeyUtil import org.session.libsession.utilities.preferences.ProfileKeyUtil
import org.session.libsignal.service.api.util.StreamDetails
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.avatar.AvatarSelection
@ -51,7 +47,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
@ -127,7 +122,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
when (requestCode) { when (requestCode) {
AvatarSelection.REQUEST_CODE_AVATAR -> { AvatarSelection.REQUEST_CODE_AVATAR -> {
if (resultCode != Activity.RESULT_OK) { return } if (resultCode != Activity.RESULT_OK) {
return
}
val outputFile = Uri.fromFile(File(cacheDir, "cropped")) val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
var inputFile: Uri? = data?.data var inputFile: Uri? = data?.data
if (inputFile == null && tempFile != null) { if (inputFile == null && tempFile != null) {
@ -136,7 +133,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar) AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
} }
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> { AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
if (resultCode != Activity.RESULT_OK) { return } if (resultCode != Activity.RESULT_OK) {
return
}
AsyncTask.execute { AsyncTask.execute {
try { try {
profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
@ -186,37 +185,23 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
val profilePicture = profilePictureToBeUploaded val profilePicture = profilePictureToBeUploaded
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this) val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
if (isUpdatingProfilePicture && profilePicture != null) { if (isUpdatingProfilePicture && profilePicture != null) {
val storageAPI = FileServerAPI.shared promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this))
val deferred = deferred<Unit, Exception>()
AsyncTask.execute {
val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong())
val (_, url) = storageAPI.uploadProfilePicture(storageAPI.server, profileKey, stream) {
TextSecurePreferences.setLastProfilePictureUpload(this@SettingsActivity, Date().time)
} }
TextSecurePreferences.setProfilePictureURL(this, url) val compoundPromise = all(promises)
deferred.resolve(Unit) compoundPromise.success {
}
promises.add(deferred.promise)
}
all(promises).bind {
// updating the profile name or picture
if (profilePicture != null || displayName != null) {
task {
if (isUpdatingProfilePicture && profilePicture != null) { if (isUpdatingProfilePicture && profilePicture != null) {
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded() ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
} }
if (profilePicture != null || displayName != null) {
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
} }
} else {
Promise.of(Unit)
} }
}.alwaysUi { compoundPromise.alwaysUi {
if (displayName != null) { if (displayName != null) {
btnGroupNameDisplay.text = displayName btnGroupNameDisplay.text = displayName
} }

View File

@ -36,28 +36,29 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} }
override fun execute() { override fun execute() {
val storage = MessagingModuleConfiguration.shared.storage
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val handleFailure: (java.lang.Exception) -> Unit = { exception -> val handleFailure: (java.lang.Exception) -> Unit = { exception ->
if (exception == Error.NoAttachment) { if (exception == Error.NoAttachment) {
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception) this.handlePermanentFailure(exception)
} else if (exception == DotNetAPI.Error.ParsingFailed) { } else if (exception == DotNetAPI.Error.ParsingFailed) {
// No need to retry if the response is invalid. Most likely this means we (incorrectly) // No need to retry if the response is invalid. Most likely this means we (incorrectly)
// got a "Cannot GET ..." error from the file server. // got a "Cannot GET ..." error from the file server.
MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception) this.handlePermanentFailure(exception)
} else { } else {
this.handleFailure(exception) this.handleFailure(exception)
} }
} }
try { try {
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
?: return handleFailure(Error.NoAttachment) ?: return handleFailure(Error.NoAttachment)
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID) messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
val tempFile = createTempFile() val tempFile = createTempFile()
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID) val threadID = storage.getThreadIdForMms(databaseMessageID)
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString()) val openGroupV2 = storage.getV2OpenGroup(threadID.toString())
val stream = if (openGroupV2 == null) { val inputStream = if (openGroupV2 == null) {
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null) DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
// Assume we're retrieving an attachment for an open group server if the digest is not set // Assume we're retrieving an attachment for an open group server if the digest is not set
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) { if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
@ -67,13 +68,13 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} }
} else { } else {
val url = HttpUrl.parse(attachment.url)!! val url = HttpUrl.parse(attachment.url)!!
val fileId = url.pathSegments().last() val fileID = url.pathSegments().last()
OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let { OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
tempFile.writeBytes(it) tempFile.writeBytes(it)
} }
FileInputStream(tempFile) FileInputStream(tempFile)
} }
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream) messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream)
tempFile.delete() tempFile.delete()
handleSuccess() handleSuccess()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -101,7 +101,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
// encrypts as it writes data. // encrypts as it writes data.
val inputStream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream val inputStream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream
val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory() val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory()
// Create a multipart request body but immediately read it out to a buffer. Doing this makes // Create a digesting request body but immediately read it out to a buffer. Doing this makes
// it easier to deal with inputStream and outputStreamFactory. // it easier to deal with inputStream and outputStreamFactory.
val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener) val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener)
val contentType = "application/octet-stream" val contentType = "application/octet-stream"

View File

@ -0,0 +1,46 @@
package org.session.libsession.utilities
import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import okio.Buffer
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.utilities.preferences.ProfileKeyUtil
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
import org.session.libsignal.service.internal.push.ProfileAvatarData
import org.session.libsignal.service.internal.push.http.DigestingRequestBody
import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory
import org.session.libsignal.service.loki.utilities.retryIfNeeded
import org.session.libsignal.utilities.ThreadUtils
import java.io.ByteArrayInputStream
import java.util.*
object ProfilePictureUtilities {
fun upload(profilePicture: ByteArray, encodedProfileKey: String, context: Context): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
ThreadUtils.queue {
val inputStream = ByteArrayInputStream(profilePicture)
val outputStream = ProfileCipherOutputStream.getCiphertextLength(profilePicture.size.toLong())
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
val pad = ProfileAvatarData(inputStream, outputStream, "image/jpeg", ProfileCipherOutputStreamFactory(profileKey))
val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, pad.contentType, pad.dataLength, null)
val b = Buffer()
drb.writeTo(b)
val data = b.readByteArray()
var id: Long = 0
try {
id = retryIfNeeded(4) {
FileServerAPIV2.upload(data)
}.get()
} catch (e: Exception) {
deferred.reject(e)
}
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
val url = "${FileServerAPIV2.SERVER}/files/$id"
TextSecurePreferences.setProfilePictureURL(context, url)
deferred.resolve(Unit)
}
return deferred.promise
}
}