From 17a7fb55ce502d055da2f0977c75cee6419bdf64 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:53:28 +1100 Subject: [PATCH 1/4] Remove jackson from a few more places --- .../utilities/TextSecurePreferences.kt | 125 ------------------ .../BytesAsCompactB64Serializer.kt | 25 ++++ .../securesms/ApplicationContext.kt | 4 + .../LocalEncryptedFileInputStream.kt | 5 +- .../LocalEncryptedFileOutputStream.kt | 5 +- .../securesms/auth/LoginStateRepository.kt | 37 +++--- .../securesms/crypto/AttachmentSecret.java | 115 ---------------- .../securesms/crypto/AttachmentSecret.kt | 16 +++ .../crypto/AttachmentSecretProvider.java | 90 ------------- .../crypto/AttachmentSecretProvider.kt | 73 ++++++++++ .../crypto/DatabaseSecretProvider.java | 73 ---------- .../crypto/DatabaseSecretProvider.kt | 62 +++++++++ .../securesms/crypto/IdentityKeyUtil.java | 37 +----- .../securesms/crypto/KeyStoreHelper.java | 71 +--------- .../securesms/crypto/SealedData.kt | 29 ++++ .../securesms/dependencies/DatabaseModule.kt | 6 +- .../securesms/logging/LogSecretProvider.java | 48 ------- .../securesms/logging/LogSecretProvider.kt | 35 +++++ .../securesms/logging/PersistentLogger.kt | 3 +- .../securesms/mms/SignalGlideModule.java | 2 +- .../onboarding/landing/LandingActivity.kt | 2 - .../securesms/preferences/PreferenceKey.kt | 9 +- .../preferences/SharedPreferenceStorage.kt | 7 +- .../securesms/providers/BlobUtils.java | 7 +- 24 files changed, 295 insertions(+), 591 deletions(-) create mode 100644 app/src/main/java/org/session/libsession/utilities/serializable/BytesAsCompactB64Serializer.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecret.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecret.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/SealedData.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.kt diff --git a/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 78c50e12c3..81a63f787d 100644 --- a/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/app/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -100,14 +100,6 @@ interface TextSecurePreferences { fun setBackupSaveDir(dirUri: String?) fun getBackupSaveDir(): String? fun getNeedsSqlCipherMigration(): Boolean - fun setAttachmentEncryptedSecret(secret: String) - fun setAttachmentUnencryptedSecret(secret: String?) - fun getAttachmentEncryptedSecret(): String? - fun getAttachmentUnencryptedSecret(): String? - fun setDatabaseEncryptedSecret(secret: String) - fun setDatabaseUnencryptedSecret(secret: String?) - fun getDatabaseUnencryptedSecret(): String? - fun getDatabaseEncryptedSecret(): String? fun isIncognitoKeyboardEnabled(): Boolean fun setIncognitoKeyboardEnabled(enabled : Boolean) fun isReadReceiptsEnabled(): Boolean @@ -154,10 +146,6 @@ interface TextSecurePreferences { fun getNotificationLedColor(): Int fun setThreadLengthTrimmingEnabled(enabled : Boolean) fun isThreadLengthTrimmingEnabled(): Boolean - fun getLogEncryptedSecret(): String? - fun setLogEncryptedSecret(base64Secret: String?) - fun getLogUnencryptedSecret(): String? - fun setLogUnencryptedSecret(base64Secret: String?) fun getNotificationChannelVersion(): Int fun setNotificationChannelVersion(version: Int) fun getNotificationMessagesChannelVersion(): Int @@ -297,10 +285,6 @@ interface TextSecurePreferences { var pushSuffix = "" - // This is a stop-gap solution for static access to shared preference. - val preferenceInstance: TextSecurePreferences - get() = MessagingModuleConfiguration.shared.preferences - const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase" const val LANGUAGE_PREF = "pref_language" const val LAST_VERSION_CODE_PREF = "last_version_code" @@ -323,10 +307,6 @@ interface TextSecurePreferences { const val DIRECT_CAPTURE_CAMERA_ID = "pref_direct_capture_camera_id" const val READ_RECEIPTS_PREF = "pref_read_receipts" const val INCOGNITO_KEYBOARD_PREF = "pref_incognito_keyboard" - const val DATABASE_ENCRYPTED_SECRET = "pref_database_encrypted_secret" - const val DATABASE_UNENCRYPTED_SECRET = "pref_database_unencrypted_secret" - const val ATTACHMENT_ENCRYPTED_SECRET = "pref_attachment_encrypted_secret" - const val ATTACHMENT_UNENCRYPTED_SECRET = "pref_attachment_unencrypted_secret" const val NEEDS_SQLCIPHER_MIGRATION = "pref_needs_sql_cipher_migration" const val BACKUP_ENABLED = "pref_backup_enabled_v3" const val BACKUP_PASSPHRASE = "pref_backup_passphrase" @@ -335,8 +315,6 @@ interface TextSecurePreferences { const val BACKUP_SAVE_DIR = "pref_save_dir" const val SCREEN_LOCK = "pref_android_screen_lock" const val SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout" - const val LOG_ENCRYPTED_SECRET = "pref_log_encrypted_secret" - const val LOG_UNENCRYPTED_SECRET = "pref_log_unencrypted_secret" const val NOTIFICATION_CHANNEL_VERSION = "pref_notification_channel_version" const val NOTIFICATION_MESSAGES_CHANNEL_VERSION = "pref_notification_messages_channel_version" const val UNIVERSAL_UNIDENTIFIED_ACCESS = "pref_universal_unidentified_access" @@ -468,46 +446,6 @@ interface TextSecurePreferences { setLongPreference(context, SCREEN_LOCK_TIMEOUT, value) } - @JvmStatic - fun setAttachmentEncryptedSecret(context: Context, secret: String) { - setStringPreference(context, ATTACHMENT_ENCRYPTED_SECRET, secret) - } - - @JvmStatic - fun setAttachmentUnencryptedSecret(context: Context, secret: String?) { - setStringPreference(context, ATTACHMENT_UNENCRYPTED_SECRET, secret) - } - - @JvmStatic - fun getAttachmentEncryptedSecret(context: Context): String? { - return getStringPreference(context, ATTACHMENT_ENCRYPTED_SECRET, null) - } - - @JvmStatic - fun getAttachmentUnencryptedSecret(context: Context): String? { - return getStringPreference(context, ATTACHMENT_UNENCRYPTED_SECRET, null) - } - - @JvmStatic - fun setDatabaseEncryptedSecret(context: Context, secret: String) { - setStringPreference(context, DATABASE_ENCRYPTED_SECRET, secret) - } - - @JvmStatic - fun setDatabaseUnencryptedSecret(context: Context, secret: String?) { - setStringPreference(context, DATABASE_UNENCRYPTED_SECRET, secret) - } - - @JvmStatic - fun getDatabaseUnencryptedSecret(context: Context): String? { - return getStringPreference(context, DATABASE_UNENCRYPTED_SECRET, null) - } - - @JvmStatic - fun getDatabaseEncryptedSecret(context: Context): String? { - return getStringPreference(context, DATABASE_ENCRYPTED_SECRET, null) - } - @JvmStatic fun isIncognitoKeyboardEnabled(context: Context): Boolean { return getBooleanPreference(context, INCOGNITO_KEYBOARD_PREF, true) @@ -593,21 +531,6 @@ interface TextSecurePreferences { return getBooleanPreference(context, THREAD_TRIM_ENABLED, true) } - @JvmStatic - fun getLogEncryptedSecret(context: Context): String? { - return getStringPreference(context, LOG_ENCRYPTED_SECRET, null) - } - - @JvmStatic - fun setLogEncryptedSecret(context: Context, base64Secret: String?) { - setStringPreference(context, LOG_ENCRYPTED_SECRET, base64Secret) - } - - @JvmStatic - fun getLogUnencryptedSecret(context: Context): String? { - return getStringPreference(context, LOG_UNENCRYPTED_SECRET, null) - } - @JvmStatic fun getNotificationChannelVersion(context: Context): Int { return getIntegerPreference(context, NOTIFICATION_CHANNEL_VERSION, 1) @@ -839,38 +762,6 @@ class AppTextSecurePreferences @Inject constructor( return getBooleanPreference(TextSecurePreferences.NEEDS_SQLCIPHER_MIGRATION, false) } - override fun setAttachmentEncryptedSecret(secret: String) { - setStringPreference(TextSecurePreferences.ATTACHMENT_ENCRYPTED_SECRET, secret) - } - - override fun setAttachmentUnencryptedSecret(secret: String?) { - setStringPreference(TextSecurePreferences.ATTACHMENT_UNENCRYPTED_SECRET, secret) - } - - override fun getAttachmentEncryptedSecret(): String? { - return getStringPreference(TextSecurePreferences.ATTACHMENT_ENCRYPTED_SECRET, null) - } - - override fun getAttachmentUnencryptedSecret(): String? { - return getStringPreference(TextSecurePreferences.ATTACHMENT_UNENCRYPTED_SECRET, null) - } - - override fun setDatabaseEncryptedSecret(secret: String) { - setStringPreference(TextSecurePreferences.DATABASE_ENCRYPTED_SECRET, secret) - } - - override fun setDatabaseUnencryptedSecret(secret: String?) { - setStringPreference(TextSecurePreferences.DATABASE_UNENCRYPTED_SECRET, secret) - } - - override fun getDatabaseUnencryptedSecret(): String? { - return getStringPreference(TextSecurePreferences.DATABASE_UNENCRYPTED_SECRET, null) - } - - override fun getDatabaseEncryptedSecret(): String? { - return getStringPreference(TextSecurePreferences.DATABASE_ENCRYPTED_SECRET, null) - } - override fun isIncognitoKeyboardEnabled(): Boolean { return getBooleanPreference(TextSecurePreferences.INCOGNITO_KEYBOARD_PREF, true) } @@ -1086,22 +977,6 @@ class AppTextSecurePreferences @Inject constructor( return getBooleanPreference(TextSecurePreferences.THREAD_TRIM_ENABLED, true) } - override fun getLogEncryptedSecret(): String? { - return getStringPreference(TextSecurePreferences.LOG_ENCRYPTED_SECRET, null) - } - - override fun setLogEncryptedSecret(base64Secret: String?) { - setStringPreference(TextSecurePreferences.LOG_ENCRYPTED_SECRET, base64Secret) - } - - override fun getLogUnencryptedSecret(): String? { - return getStringPreference(TextSecurePreferences.LOG_UNENCRYPTED_SECRET, null) - } - - override fun setLogUnencryptedSecret(base64Secret: String?) { - setStringPreference(TextSecurePreferences.LOG_UNENCRYPTED_SECRET, base64Secret) - } - override fun getNotificationChannelVersion(): Int { return getIntegerPreference(TextSecurePreferences.NOTIFICATION_CHANNEL_VERSION, 1) } diff --git a/app/src/main/java/org/session/libsession/utilities/serializable/BytesAsCompactB64Serializer.kt b/app/src/main/java/org/session/libsession/utilities/serializable/BytesAsCompactB64Serializer.kt new file mode 100644 index 0000000000..3a72a8a2d5 --- /dev/null +++ b/app/src/main/java/org/session/libsession/utilities/serializable/BytesAsCompactB64Serializer.kt @@ -0,0 +1,25 @@ +package org.session.libsession.utilities.serializable + +import android.util.Base64 +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +class BytesAsCompactB64Serializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("BytesAsCompactB64Serializer", PrimitiveKind.STRING) + + private val base64Flags = Base64.NO_WRAP or Base64.NO_PADDING + + override fun serialize( + encoder: Encoder, + value: ByteArray + ) { + encoder.encodeString(Base64.encodeToString(value, base64Flags)) + } + + override fun deserialize(decoder: Decoder): ByteArray { + return Base64.decode(decoder.decodeString(), base64Flags) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt index d4756864d4..23f74c0cb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.kt @@ -46,6 +46,7 @@ import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.pushSuffix import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.auth.LoginStateRepository +import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider import org.thoughtcrime.securesms.debugmenu.DebugActivity import org.thoughtcrime.securesms.debugmenu.DebugLogger import org.thoughtcrime.securesms.dependencies.DatabaseComponent @@ -97,6 +98,9 @@ class ApplicationContext : Application(), DefaultLifecycleObserver, Configuratio @Inject lateinit var loginStateRepository: Lazy + @Inject + lateinit var attachmentSecretProvider: AttachmentSecretProvider + @Volatile var isAppVisible: Boolean = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/LocalEncryptedFileInputStream.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/LocalEncryptedFileInputStream.kt index efaf94eaad..2e3ede1933 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/LocalEncryptedFileInputStream.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/LocalEncryptedFileInputStream.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.attachments -import android.app.Application import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -22,11 +21,11 @@ import java.io.InputStream class LocalEncryptedFileInputStream @AssistedInject constructor( @Assisted file: File, codec: EmbeddedMetadataCodec, - application: Application + attachmentSecretProvider: AttachmentSecretProvider, ) : InputStream() { private val inputStream: InputStream = DecryptionStream( inStream = file.inputStream(), - key = AttachmentSecretProvider.getInstance(application).orCreateAttachmentSecret.modernKey, + key = attachmentSecretProvider.getOrCreateAttachmentSecret().modernKey, ) val meta: FileMetadata = codec.decodeFromStream(inputStream) diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/LocalEncryptedFileOutputStream.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/LocalEncryptedFileOutputStream.kt index a0ec8852e9..9488dbb112 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/LocalEncryptedFileOutputStream.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/LocalEncryptedFileOutputStream.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.attachments -import android.app.Application import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -22,13 +21,13 @@ class LocalEncryptedFileOutputStream @AssistedInject constructor( @Assisted file: File, @Assisted meta: FileMetadata, codec: EmbeddedMetadataCodec, - application: Application + attachmentSecretProvider: AttachmentSecretProvider, ): OutputStream() { private val outputStream = EncryptionStream( out = FileOutputStream(file.also { it.parentFile!!.mkdirs() // Make sure the parent directory exists }), - key = AttachmentSecretProvider.getInstance(application).orCreateAttachmentSecret.modernKey, + key = attachmentSecretProvider.getOrCreateAttachmentSecret().modernKey, ).also { codec.encodeToStream(meta, it) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/auth/LoginStateRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/auth/LoginStateRepository.kt index f8279dced4..5cb74ee239 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/auth/LoginStateRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/auth/LoginStateRepository.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.auth import android.content.Context -import android.content.SharedPreferences import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -22,7 +21,11 @@ import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.KeyStoreHelper +import org.thoughtcrime.securesms.crypto.SealedData import org.thoughtcrime.securesms.dependencies.ManagerScope +import org.thoughtcrime.securesms.preferences.PreferenceKey +import org.thoughtcrime.securesms.preferences.PreferenceStorage +import org.thoughtcrime.securesms.preferences.SharedPreferenceStorage import javax.inject.Inject import javax.inject.Singleton @@ -35,18 +38,20 @@ class LoginStateRepository @Inject constructor( @ApplicationContext context: Context, private val json: Json, @param:ManagerScope private val scope: CoroutineScope, + prefStorageFactory: SharedPreferenceStorage.Factory, ) { - private val sharedPrefs = context.getSharedPreferences("login_state", Context.MODE_PRIVATE) + private val prefs = prefStorageFactory.create( + context.getSharedPreferences("login_state", Context.MODE_PRIVATE) + ) private val mutableLoggedInState: MutableStateFlow init { - var initialState = sharedPrefs.getString(PREF_KEY_STATE, null)?.let { serializedState -> + var initialState = prefs[keyState]?.let { serializedState -> runCatching { json.decodeFromString( - KeyStoreHelper.unseal(KeyStoreHelper.SealedData.fromString(serializedState)).toString( - Charsets.UTF_8) + KeyStoreHelper.unseal(serializedState).toString(Charsets.UTF_8) ) }.onFailure { @@ -86,7 +91,7 @@ class LoginStateRepository @Inject constructor( if (initialState != null) { // Migrate legacy state to new format Log.i(TAG, "Migrating legacy login state to new format") - saveLoggedInState(sharedPrefs, initialState, json) + saveLoggedInState(prefs, initialState, json) //TODO: Consider removing legacy data here after a grace period } @@ -102,12 +107,10 @@ class LoginStateRepository @Inject constructor( .drop(1) // Skip the initial value .collect { newState -> if (newState != null) { - saveLoggedInState(sharedPrefs, newState, json) + saveLoggedInState(prefs, newState, json) Log.d(TAG, "Persisted new login state: $newState") } else { - sharedPrefs.edit() - .remove(PREF_KEY_STATE) - .apply() + prefs.remove(keyState) Log.d(TAG, "Cleared login state") } } @@ -189,19 +192,17 @@ class LoginStateRepository @Inject constructor( companion object { private const val TAG = "LoginStateRepository" - private const val PREF_KEY_STATE = "state" + private val keyState = PreferenceKey.json("state") + private fun saveLoggedInState( - prefs: SharedPreferences, + prefs: PreferenceStorage, state: LoggedInState, json: Json ) { - prefs.edit() - .putString(PREF_KEY_STATE, - KeyStoreHelper.seal( - json.encodeToString(state).toByteArray(Charsets.UTF_8) - ).serialize()) - .apply() + prefs[keyState] = KeyStoreHelper.seal( + json.encodeToString(state).toByteArray(Charsets.UTF_8) + ) } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecret.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecret.java deleted file mode 100644 index 64ac3bcab9..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecret.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.thoughtcrime.securesms.crypto; - - -import androidx.annotation.NonNull; -import android.util.Base64; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import org.session.libsignal.utilities.JsonUtil; - -import java.io.IOException; - -/** - * Encapsulates the key material used to encrypt attachments on disk. - * - * There are two logical pieces of material, a deprecated set of keys used to encrypt - * legacy attachments, and a key that is used to encrypt attachments going forward. - */ -public class AttachmentSecret { - - @JsonProperty - @JsonSerialize(using = ByteArraySerializer.class) - @JsonDeserialize(using = ByteArrayDeserializer.class) - private byte[] classicCipherKey; - - @JsonProperty - @JsonSerialize(using = ByteArraySerializer.class) - @JsonDeserialize(using = ByteArrayDeserializer.class) - private byte[] classicMacKey; - - @JsonProperty - @JsonSerialize(using = ByteArraySerializer.class) - @JsonDeserialize(using = ByteArrayDeserializer.class) - private byte[] modernKey; - - public AttachmentSecret(byte[] classicCipherKey, byte[] classicMacKey, byte[] modernKey) - { - this.classicCipherKey = classicCipherKey; - this.classicMacKey = classicMacKey; - this.modernKey = modernKey; - } - - @SuppressWarnings("unused") - public AttachmentSecret() { - - } - - @JsonIgnore - byte[] getClassicCipherKey() { - return classicCipherKey; - } - - @JsonIgnore - byte[] getClassicMacKey() { - return classicMacKey; - } - - @JsonIgnore - public byte[] getModernKey() { - return modernKey; - } - - @JsonIgnore - void setClassicCipherKey(byte[] classicCipherKey) { - this.classicCipherKey = classicCipherKey; - } - - @JsonIgnore - void setClassicMacKey(byte[] classicMacKey) { - this.classicMacKey = classicMacKey; - } - - public String serialize() { - try { - return JsonUtil.toJsonThrows(this); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - static AttachmentSecret fromString(@NonNull String value) { - try { - return JsonUtil.fromJson(value, AttachmentSecret.class); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - private static class ByteArraySerializer extends JsonSerializer { - @Override - public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING)); - } - } - - private static class ByteArrayDeserializer extends JsonDeserializer { - - @Override - public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING); - } - } - - - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecret.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecret.kt new file mode 100644 index 0000000000..93ec84eaa9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecret.kt @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.crypto + +import kotlinx.serialization.Serializable +import org.session.libsession.utilities.serializable.BytesAsCompactB64Serializer + +@Serializable +class AttachmentSecret( + @Serializable(with = BytesAsCompactB64Serializer::class) + val classicMacKey: ByteArray? = null, + + @Serializable(with = BytesAsCompactB64Serializer::class) + val classicCipherKey: ByteArray? = null, + + @Serializable(with = BytesAsCompactB64Serializer::class) + val modernKey: ByteArray, +) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.java deleted file mode 100644 index 844ba975f1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.thoughtcrime.securesms.crypto; - - -import static org.session.libsignal.utilities.Util.SECURE_RANDOM; - -import android.content.Context; -import android.os.Build; -import androidx.annotation.NonNull; - -import org.session.libsession.utilities.TextSecurePreferences; - -/** - * A provider that is responsible for creating or retrieving the AttachmentSecret model. - * - * On modern Android, the serialized secrets are themselves encrypted using a key that lives - * in the system KeyStore, for whatever that is worth. - */ -public class AttachmentSecretProvider { - - private static AttachmentSecretProvider provider; - - public static synchronized AttachmentSecretProvider getInstance(@NonNull Context context) { - if (provider == null) provider = new AttachmentSecretProvider(context.getApplicationContext()); - return provider; - } - - private final Context context; - - private AttachmentSecret attachmentSecret; - - private AttachmentSecretProvider(@NonNull Context context) { - this.context = context.getApplicationContext(); - } - - public synchronized AttachmentSecret getOrCreateAttachmentSecret() { - if (attachmentSecret != null) return attachmentSecret; - - String unencryptedSecret = TextSecurePreferences.getAttachmentUnencryptedSecret(context); - String encryptedSecret = TextSecurePreferences.getAttachmentEncryptedSecret(context); - - if (unencryptedSecret != null) attachmentSecret = getUnencryptedAttachmentSecret(context, unencryptedSecret); - else if (encryptedSecret != null) attachmentSecret = getEncryptedAttachmentSecret(encryptedSecret); - else attachmentSecret = createAndStoreAttachmentSecret(context); - - return attachmentSecret; - } - - public synchronized AttachmentSecret setClassicKey(@NonNull Context context, @NonNull byte[] classicCipherKey, @NonNull byte[] classicMacKey) { - AttachmentSecret currentSecret = getOrCreateAttachmentSecret(); - currentSecret.setClassicCipherKey(classicCipherKey); - currentSecret.setClassicMacKey(classicMacKey); - - storeAttachmentSecret(context, attachmentSecret); - - return attachmentSecret; - } - - private AttachmentSecret getUnencryptedAttachmentSecret(@NonNull Context context, @NonNull String unencryptedSecret) - { - AttachmentSecret attachmentSecret = AttachmentSecret.fromString(unencryptedSecret); - - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes()); - - TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize()); - TextSecurePreferences.setAttachmentUnencryptedSecret(context, null); - - return attachmentSecret; - } - - private AttachmentSecret getEncryptedAttachmentSecret(@NonNull String serializedEncryptedSecret) { - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret); - return AttachmentSecret.fromString(new String(KeyStoreHelper.unseal(encryptedSecret))); - } - - private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) { - byte[] secret = new byte[32]; - SECURE_RANDOM.nextBytes(secret); - - AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret); - storeAttachmentSecret(context, attachmentSecret); - - return attachmentSecret; - } - - private void storeAttachmentSecret(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret) { - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes()); - TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize()); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.kt new file mode 100644 index 0000000000..4d2ceec882 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.kt @@ -0,0 +1,73 @@ +package org.thoughtcrime.securesms.crypto + +import kotlinx.serialization.json.Json +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Util +import org.thoughtcrime.securesms.preferences.PreferenceKey +import org.thoughtcrime.securesms.preferences.PreferenceStorage +import javax.inject.Inject +import javax.inject.Singleton + + +/** + * A provider that is responsible for creating or retrieving the AttachmentSecret model. + * + * On modern Android, the serialized secrets are themselves encrypted using a key that lives + * in the system KeyStore, for whatever that is worth. + */ +@Singleton +class AttachmentSecretProvider @Inject constructor( + private val json: Json, + private val prefs: PreferenceStorage, +) { + private var attachmentSecret: AttachmentSecret? = null + + + @Synchronized + fun getOrCreateAttachmentSecret(): AttachmentSecret { + if (attachmentSecret != null) return attachmentSecret!! + + val secret = runCatching { prefs[unencryptedKey]?.let(this::migrateUnencryptedKey) }.getOrNull() + ?: runCatching { prefs[encryptedKey]?.let(this::getEncryptedAttachmentSecret) } + .onFailure { + Log.w("AttachmentSecretProvider", "Failed to read encrypted attachment secret, will create a new one", it) + } + .getOrNull() + ?: createAndStoreAttachmentSecret() + + attachmentSecret = secret + return secret + } + + private fun migrateUnencryptedKey( + unencryptedSecret: AttachmentSecret + ): AttachmentSecret { + val encryptedSecret = KeyStoreHelper.seal(json.encodeToString(unencryptedSecret).toByteArray()) + prefs[encryptedKey] = encryptedSecret + prefs.remove(unencryptedKey) + return unencryptedSecret + } + + private fun getEncryptedAttachmentSecret(encryptedSecret: SealedData): AttachmentSecret { + return json.decodeFromString(KeyStoreHelper.unseal(encryptedSecret).decodeToString()) + } + + private fun createAndStoreAttachmentSecret(): AttachmentSecret { + val secret = ByteArray(32) + Util.SECURE_RANDOM.nextBytes(secret) + + val attachmentSecret = AttachmentSecret( + modernKey = secret + ) + + prefs[encryptedKey] = KeyStoreHelper.seal(json.encodeToString(attachmentSecret).toByteArray()) + return attachmentSecret + } + + + companion object { + private val encryptedKey = PreferenceKey.json("pref_attachment_encrypted_secret") + private val unencryptedKey = PreferenceKey.json("pref_attachment_unencrypted_secret") + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java deleted file mode 100644 index ffa50493fd..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.thoughtcrime.securesms.crypto; - - -import static org.session.libsignal.utilities.Util.SECURE_RANDOM; - -import android.content.Context; -import android.os.Build; -import androidx.annotation.NonNull; - -import org.session.libsession.utilities.TextSecurePreferences; - -import java.io.IOException; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import dagger.hilt.android.qualifiers.ApplicationContext; - -@Singleton -public class DatabaseSecretProvider { - - @SuppressWarnings("unused") - private static final String TAG = DatabaseSecretProvider.class.getSimpleName(); - - private final Context context; - - @Inject - public DatabaseSecretProvider(@ApplicationContext @NonNull Context context) { - this.context = context.getApplicationContext(); - } - - public DatabaseSecret getOrCreateDatabaseSecret() { - String unencryptedSecret = TextSecurePreferences.getDatabaseUnencryptedSecret(context); - String encryptedSecret = TextSecurePreferences.getDatabaseEncryptedSecret(context); - - if (unencryptedSecret != null) return getUnencryptedDatabaseSecret(context, unencryptedSecret); - else if (encryptedSecret != null) return getEncryptedDatabaseSecret(encryptedSecret); - else return createAndStoreDatabaseSecret(context); - } - - private DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret) - { - try { - DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret); - - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes()); - - TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize()); - TextSecurePreferences.setDatabaseUnencryptedSecret(context, null); - - return databaseSecret; - } catch (IOException e) { - throw new AssertionError(e); - } - } - - private DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) { - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret); - return new DatabaseSecret(KeyStoreHelper.unseal(encryptedSecret)); - } - - private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) { - byte[] secret = new byte[32]; - SECURE_RANDOM.nextBytes(secret); - - DatabaseSecret databaseSecret = new DatabaseSecret(secret); - - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes()); - TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize()); - - return databaseSecret; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt new file mode 100644 index 0000000000..4fffa4cbef --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.crypto + +import kotlinx.serialization.json.Json +import org.session.libsignal.utilities.Util +import org.thoughtcrime.securesms.preferences.PreferenceKey +import org.thoughtcrime.securesms.preferences.PreferenceStorage +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DatabaseSecretProvider @Inject constructor( + private val prefs: PreferenceStorage, + private val json: Json, +) { + companion object { + private val keyUnencryptedSecret = PreferenceKey.string("pref_database_unencrypted_secret") + private val keyEncryptedSecret = PreferenceKey.json("pref_database_encrypted_secret") + } + + @Synchronized + fun getOrCreateDatabaseSecret(): DatabaseSecret { + prefs[keyUnencryptedSecret]?.let { + return getUnencryptedDatabaseSecret(it) + } + + prefs[keyEncryptedSecret]?.let { + return getEncryptedDatabaseSecret(it) + } + + return createAndStoreDatabaseSecret() + } + + private fun getUnencryptedDatabaseSecret( + unencryptedSecret: String + ): DatabaseSecret { + try { + val databaseSecret = DatabaseSecret(unencryptedSecret) + val encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes()) + + prefs[keyEncryptedSecret] = json.encodeToString(encryptedSecret) + prefs.remove(keyUnencryptedSecret) + + return databaseSecret + } catch (e: IOException) { + throw AssertionError(e) + } + } + + private fun getEncryptedDatabaseSecret(encryptedSecret: SealedData): DatabaseSecret { + return DatabaseSecret(KeyStoreHelper.unseal(encryptedSecret)) + } + + private fun createAndStoreDatabaseSecret(): DatabaseSecret { + val secret = ByteArray(32) + Util.SECURE_RANDOM.nextBytes(secret) + + val databaseSecret = DatabaseSecret(secret) + prefs[keyEncryptedSecret] = KeyStoreHelper.seal(databaseSecret.asBytes()) + return databaseSecret + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java index e262f18022..4f2ab0bf3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java @@ -23,18 +23,11 @@ import androidx.annotation.NonNull; -import org.session.libsignal.crypto.IdentityKey; -import org.session.libsignal.crypto.IdentityKeyPair; -import org.session.libsignal.crypto.ecc.Curve; import org.session.libsignal.crypto.ecc.DjbECPrivateKey; import org.session.libsignal.crypto.ecc.DjbECPublicKey; import org.session.libsignal.crypto.ecc.ECKeyPair; -import org.session.libsignal.crypto.ecc.ECPrivateKey; -import org.session.libsignal.exceptions.InvalidKeyException; import org.session.libsignal.utilities.Base64; -import java.io.IOException; - import kotlin.Unit; import kotlinx.coroutines.channels.BufferOverflow; import kotlinx.coroutines.flow.MutableSharedFlow; @@ -94,30 +87,6 @@ public static void checkUpdate(Context context) { } } - public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) { - if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!"); - - try { - byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF)); - return new IdentityKey(publicKeyBytes, 0); - } catch (IOException | InvalidKeyException e) { - throw new AssertionError(e); - } - } - - public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) { - if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!"); - - try { - IdentityKey publicKey = getIdentityKey(context); - ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF))); - - return new IdentityKeyPair(publicKey, privateKey); - } catch (IOException e) { - throw new AssertionError(e); - } - } - public static void generateIdentityKeyPair(@NonNull Context context) { KeyPair keyPair = Curve25519.INSTANCE.generateKeyPair(); ECKeyPair ecKeyPair = new ECKeyPair( @@ -142,7 +111,7 @@ public static String retrieve(Context context, String key) { } private static String getUnencryptedSecret(String key, String unencryptedSecret, Context context) { - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(unencryptedSecret.getBytes()); + SealedData encryptedSecret = KeyStoreHelper.seal(unencryptedSecret.getBytes()); // save the encrypted suffix secret "key_encrypted" save(context,key+ENCRYPTED_SUFFIX,encryptedSecret.serialize()); @@ -153,7 +122,7 @@ private static String getUnencryptedSecret(String key, String unencryptedSecret, } private static String getEncryptedSecret(String encryptedSecret) { - KeyStoreHelper.SealedData sealedData = KeyStoreHelper.SealedData.fromString(encryptedSecret); + SealedData sealedData = SealedData.fromString(encryptedSecret); return new String(KeyStoreHelper.unseal(sealedData)); } @@ -166,7 +135,7 @@ public static void save(Context context, String key, String value) { if (isEncryptedSuffix) { preferencesEditor.putString(key, value); } else { - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(value.getBytes()); + SealedData encryptedSecret = KeyStoreHelper.seal(value.getBytes()); preferencesEditor.putString(key+ENCRYPTED_SUFFIX, encryptedSecret.serialize()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java index 7f0edddeb0..b6a1db9633 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/KeyStoreHelper.java @@ -5,22 +5,9 @@ import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; -import android.util.Base64; import androidx.annotation.NonNull; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import org.session.libsignal.utilities.JsonUtil; - import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -75,9 +62,9 @@ public static byte[] unseal(@NonNull SealedData sealedData) { // https://github.com/mozilla-mobile/android-components/issues/5342 synchronized (CIPHER_LOCK) { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv)); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.getIv())); - return cipher.doFinal(sealedData.data); + return cipher.doFinal(sealedData.getData()); } } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); @@ -153,58 +140,4 @@ private static boolean hasKeyStoreEntry() { } } - public static class SealedData { - - @SuppressWarnings("unused") - private static final String TAG = SealedData.class.getSimpleName(); - - @JsonProperty - @JsonSerialize(using = ByteArraySerializer.class) - @JsonDeserialize(using = ByteArrayDeserializer.class) - private byte[] iv; - - @JsonProperty - @JsonSerialize(using = ByteArraySerializer.class) - @JsonDeserialize(using = ByteArrayDeserializer.class) - private byte[] data; - - SealedData(@NonNull byte[] iv, @NonNull byte[] data) { - this.iv = iv; - this.data = data; - } - - @SuppressWarnings("unused") - public SealedData() {} - - public String serialize() { - try { - return JsonUtil.toJsonThrows(this); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - public static SealedData fromString(@NonNull String value) { - try { - return JsonUtil.fromJson(value, SealedData.class); - } catch (IOException e) { - throw new AssertionError(e); - } - } - - private static class ByteArraySerializer extends JsonSerializer { - @Override - public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING)); - } - } - - private static class ByteArrayDeserializer extends JsonDeserializer { - - @Override - public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING); - } - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/SealedData.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/SealedData.kt new file mode 100644 index 0000000000..bfd9744d5a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/SealedData.kt @@ -0,0 +1,29 @@ +package org.thoughtcrime.securesms.crypto + +import kotlinx.serialization.Serializable +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.utilities.serializable.BytesAsCompactB64Serializer + +@Serializable +class SealedData( + @Serializable(with = BytesAsCompactB64Serializer::class) + val iv: ByteArray, + + @Serializable(with = BytesAsCompactB64Serializer::class) + val data: ByteArray, +) { + @Deprecated("Use dependency injected json instead") + fun serialize(): String { + return MessagingModuleConfiguration.shared.json.encodeToString(this) + } + + companion object { + @JvmStatic + @Deprecated("Use dependency injected json instead") + fun fromString(serialized: String): SealedData { + return MessagingModuleConfiguration.shared.json.decodeFromString(serialized) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt index 1da24a0866..2c8d360b85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt @@ -9,7 +9,6 @@ import dagger.hilt.components.SingletonComponent import org.thoughtcrime.securesms.auth.LoginStateRepository import org.thoughtcrime.securesms.crypto.AttachmentSecret import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider -import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.ConfigDatabase import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.EmojiSearchDatabase @@ -23,7 +22,6 @@ import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.database.PushDatabase import org.thoughtcrime.securesms.database.ReactionDatabase -import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.SearchDatabase import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.migration.DatabaseMigrationManager @@ -41,7 +39,9 @@ object DatabaseModule { @Provides @Singleton - fun provideAttachmentSecret(@ApplicationContext context: Context) = AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret + fun provideAttachmentSecret(attachmentSecretProvider: AttachmentSecretProvider): AttachmentSecret { + return attachmentSecretProvider.getOrCreateAttachmentSecret() + } @Provides @Singleton diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.java b/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.java deleted file mode 100644 index 533e1a1d23..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.thoughtcrime.securesms.logging; - -import static org.session.libsignal.utilities.Util.SECURE_RANDOM; - -import android.content.Context; -import android.os.Build; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.crypto.KeyStoreHelper; -import org.session.libsignal.utilities.Base64; -import org.session.libsession.utilities.TextSecurePreferences; - -import java.io.IOException; - -class LogSecretProvider { - - static byte[] getOrCreateAttachmentSecret(@NonNull Context context) { - String unencryptedSecret = TextSecurePreferences.getLogUnencryptedSecret(context); - String encryptedSecret = TextSecurePreferences.getLogEncryptedSecret(context); - - if (unencryptedSecret != null) return parseUnencryptedSecret(unencryptedSecret); - else if (encryptedSecret != null) return parseEncryptedSecret(encryptedSecret); - else return createAndStoreSecret(context); - } - - private static byte[] parseUnencryptedSecret(String secret) { - try { - return Base64.decode(secret); - } catch (IOException e) { - throw new AssertionError("Failed to decode the unecrypted secret."); - } - } - - private static byte[] parseEncryptedSecret(String secret) { - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(secret); - return KeyStoreHelper.unseal(encryptedSecret); - } - - private static byte[] createAndStoreSecret(@NonNull Context context) { - byte[] secret = new byte[32]; - SECURE_RANDOM.nextBytes(secret); - - KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(secret); - TextSecurePreferences.setLogEncryptedSecret(context, encryptedSecret.serialize()); - - return secret; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.kt new file mode 100644 index 0000000000..6f678f14cb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.kt @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.logging + +import org.session.libsignal.utilities.Util.SECURE_RANDOM +import org.thoughtcrime.securesms.crypto.KeyStoreHelper +import org.thoughtcrime.securesms.crypto.SealedData +import org.thoughtcrime.securesms.preferences.PreferenceKey +import org.thoughtcrime.securesms.preferences.PreferenceStorage +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LogSecretProvider @Inject constructor( + private val prefs: PreferenceStorage +) { + companion object { + private val encryptedKey = + PreferenceKey.json("pref_log_encrypted_secret") + private val unencryptedKey = PreferenceKey.bytes("pref_log_unencrypted_secret") + } + + @Synchronized + fun getOrCreateAttachmentSecret(): ByteArray { + prefs[unencryptedKey]?.let { return it } + prefs[encryptedKey]?.let { + return KeyStoreHelper.unseal(it) + } + + val secret = ByteArray(32) + SECURE_RANDOM.nextBytes(secret) + + prefs[encryptedKey] = KeyStoreHelper.seal(secret) + + return secret + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.kt index 43fb455eb3..2e10b56541 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.kt @@ -32,6 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds class PersistentLogger @Inject constructor( @param:ApplicationContext private val context: Context, @ManagerScope scope: CoroutineScope, + logSecretProvider: LogSecretProvider, ) : Logger(), OnAppStartupComponent { private val freeLogEntryPool = LogEntryPool() private val logEntryChannel: SendChannel @@ -40,7 +41,7 @@ class PersistentLogger @Inject constructor( private val logDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS zzz", Locale.ENGLISH) private val secret by lazy { - LogSecretProvider.getOrCreateAttachmentSecret(context) + logSecretProvider.getOrCreateAttachmentSecret() } private val logFolder by lazy { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java index 40c34c26bf..63c6b2edd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -54,7 +54,7 @@ public void applyOptions(Context context, GlideBuilder builder) { @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - AttachmentSecretProvider secretProvider = AttachmentSecretProvider.getInstance(context); + AttachmentSecretProvider secretProvider = ((ApplicationContext) context.getApplicationContext()).getAttachmentSecretProvider(); registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance()); registry.prepend(InputStream.class, new EncryptedCacheEncoder(secretProvider, glide.getArrayPool())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/LandingActivity.kt index fd1d74664c..cabc138f2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/LandingActivity.kt @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.onboarding.landing -import android.content.Intent -import android.net.Uri import android.os.Bundle import dagger.hilt.android.AndroidEntryPoint import org.session.libsession.utilities.TextSecurePreferences diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PreferenceKey.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/PreferenceKey.kt index ef42dbaa7b..940d59d6d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/PreferenceKey.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/PreferenceKey.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.preferences import kotlinx.serialization.KSerializer +import kotlinx.serialization.serializer /** * A key definition for a preference. It includes the name of the preference and the @@ -21,6 +22,7 @@ class PreferenceKey( class PrimitiveFloat(val defaultValue: Float) : Strategy class PrimitiveBoolean(val defaultValue: Boolean) : Strategy class PrimitiveString(val defaultValue: String?) : Strategy + object Bytes : Strategy class Enum>(val choices: List, val defaultValue: T?) : Strategy class Json(val serializer: KSerializer) : Strategy } @@ -41,8 +43,11 @@ class PreferenceKey( fun float(name: String, defaultValue: Float): PreferenceKey = PreferenceKey(name, Strategy.PrimitiveFloat(defaultValue)) - fun json(name: String, serializer: KSerializer): PreferenceKey = - PreferenceKey(name, Strategy.Json(serializer)) + inline fun json(name: String): PreferenceKey = + PreferenceKey(name, Strategy.Json(serializer())) + + fun bytes(name: String): PreferenceKey = + PreferenceKey(name, Strategy.Bytes) inline fun > enum( name: String, diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SharedPreferenceStorage.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SharedPreferenceStorage.kt index f0857010c9..4ca6c75186 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SharedPreferenceStorage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SharedPreferenceStorage.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.json.Json +import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.util.mapToStateFlow import java.util.Optional @@ -49,6 +50,9 @@ class SharedPreferenceStorage @AssistedInject constructor( is PreferenceKey.Strategy.Enum<*> -> { putString(key.name, if (value == null) null else (value as Enum<*>).name) } + is PreferenceKey.Strategy.Bytes -> { + putString(key.name, if (value == null) null else Base64.encodeBytes(value as ByteArray)) + } } } cache.remove(key.name) @@ -86,6 +90,7 @@ class SharedPreferenceStorage @AssistedInject constructor( is PreferenceKey.Strategy.PrimitiveInt -> prefs.getInt(key.name, strategy.defaultValue) is PreferenceKey.Strategy.PrimitiveLong -> prefs.getLong(key.name, strategy.defaultValue) is PreferenceKey.Strategy.PrimitiveString -> prefs.getString(key.name, strategy.defaultValue) + is PreferenceKey.Strategy.Bytes -> prefs.getString(key.name, null)?.let(Base64::decode) is PreferenceKey.Strategy.Enum<*> -> { val name = prefs.getString(key.name, null) if (name != null) { @@ -93,7 +98,7 @@ class SharedPreferenceStorage @AssistedInject constructor( if (it == null) { Log.w(TAG, "Unable to find enum value for pref key = ${key.name}, name = $name") } - } + } ?: strategy.defaultValue } else { null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobUtils.java b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobUtils.java index 4b6a2e3079..9a807a1231 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/providers/BlobUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/providers/BlobUtils.java @@ -13,8 +13,8 @@ import org.session.libsession.utilities.Util; import org.session.libsession.utilities.concurrent.SignalExecutors; import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.AttachmentSecret; -import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; @@ -102,7 +102,8 @@ public BlobBuilder forData(@NonNull InputStream data, long fileSize) { String directory = getDirectory(storageType); File file = new File(getOrCreateCacheDirectory(context, directory), buildFileName(id)); - return ModernDecryptingPartInputStream.createFor(AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), file, 0); + return ModernDecryptingPartInputStream.createFor( + ((ApplicationContext) context).getAttachmentSecretProvider().getOrCreateAttachmentSecret(), file, 0); } } else { throw new IOException("Provided URI does not match this spec. Uri: " + uri); @@ -185,7 +186,7 @@ public static boolean isAuthority(@NonNull Uri uri) { @WorkerThread @NonNull private static CompletableFuture writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec, @Nullable ErrorListener errorListener) throws IOException { - AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + AttachmentSecret attachmentSecret = ((ApplicationContext) context).getAttachmentSecretProvider().getOrCreateAttachmentSecret(); String directory = getDirectory(blobSpec.getStorageType()); File outputFile = new File(getOrCreateCacheDirectory(context, directory), buildFileName(blobSpec.id)); OutputStream outputStream = ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second; From c7a392068cf15ccb531621ee84abd85493cbff08 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:06:50 +1100 Subject: [PATCH 2/4] PR feedback --- .../utilities/serializable/BytesAsCompactB64Serializer.kt | 3 +++ .../thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt | 2 +- .../org/thoughtcrime/securesms/preferences/PreferenceKey.kt | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/session/libsession/utilities/serializable/BytesAsCompactB64Serializer.kt b/app/src/main/java/org/session/libsession/utilities/serializable/BytesAsCompactB64Serializer.kt index 3a72a8a2d5..44ea1918b5 100644 --- a/app/src/main/java/org/session/libsession/utilities/serializable/BytesAsCompactB64Serializer.kt +++ b/app/src/main/java/org/session/libsession/utilities/serializable/BytesAsCompactB64Serializer.kt @@ -7,6 +7,9 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +/** + * A serializer that encodes a byte array as a compact Base64 string (without padding or line breaks). + */ class BytesAsCompactB64Serializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor("BytesAsCompactB64Serializer", PrimitiveKind.STRING) diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt index 4fffa4cbef..b01f3637d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.kt @@ -38,7 +38,7 @@ class DatabaseSecretProvider @Inject constructor( val databaseSecret = DatabaseSecret(unencryptedSecret) val encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes()) - prefs[keyEncryptedSecret] = json.encodeToString(encryptedSecret) + prefs[keyEncryptedSecret] = encryptedSecret prefs.remove(keyUnencryptedSecret) return databaseSecret diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/PreferenceKey.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/PreferenceKey.kt index 940d59d6d1..505a7330ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/PreferenceKey.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/PreferenceKey.kt @@ -11,9 +11,9 @@ import kotlinx.serialization.serializer * instances of [PreferenceKey] with the appropriate strategy for the type of preference you want to * store. */ -class PreferenceKey( +class PreferenceKey( val name: String, - val strategy: Strategy, + val strategy: Strategy, ) { sealed interface Strategy { From b6852c31227d6bb38afbf7259017463c88f59c0f Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:32:09 +1100 Subject: [PATCH 3/4] Remove jackson from UpdateMessageData --- .../messages/signal/IncomingTextMessage.kt | 4 +- .../messages/signal/OutgoingTextMessage.kt | 4 +- .../messaging/utilities/UpdateMessageData.kt | 176 ++++++++++-------- .../v2/messages/OpenGroupInvitationView.kt | 3 +- .../v2/utilities/ResendMessageUtilities.kt | 4 +- .../securesms/database/Storage.kt | 6 +- .../database/model/MessageRecord.java | 14 +- .../DefaultConversationRepository.kt | 3 + 8 files changed, 121 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt index 3d3be4641e..199db32ae0 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.messages.signal +import kotlinx.serialization.json.Json import network.loki.messenger.libsession_util.protocol.ProFeature import org.session.libsession.messaging.calls.CallMessageType import org.session.libsession.messaging.messages.visible.OpenGroupInvitation @@ -83,6 +84,7 @@ data class IncomingTextMessage( companion object { fun fromOpenGroupInvitation( + json: Json, invitation: OpenGroupInvitation, sender: Address, sentTimestampMillis: Long, @@ -92,7 +94,7 @@ data class IncomingTextMessage( val body = UpdateMessageData.buildOpenGroupInvitation( url = invitation.url ?: return null, name = invitation.name ?: return null, - ).toJSON() + ).toJSON(json) return IncomingTextMessage( message = body, diff --git a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt index eab343b176..97a3079fbe 100644 --- a/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt +++ b/app/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.messages.signal +import kotlinx.serialization.json.Json import network.loki.messenger.libsession_util.protocol.ProFeature import org.session.libsession.messaging.messages.visible.OpenGroupInvitation import org.session.libsession.messaging.messages.visible.VisibleMessage @@ -32,6 +33,7 @@ data class OutgoingTextMessage private constructor( companion object { fun fromOpenGroupInvitation( + json: Json, invitation: OpenGroupInvitation, recipient: Address, sentTimestampMillis: Long, @@ -44,7 +46,7 @@ data class OutgoingTextMessage private constructor( message = UpdateMessageData.buildOpenGroupInvitation( url = invitation.url ?: return null, name = invitation.name ?: return null, - ).toJSON(), + ).toJSON(json), expiresInMillis = expiresInMillis, expireStartedAtMillis = expireStartedAtMillis, sentTimestampMillis = sentTimestampMillis, diff --git a/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt b/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt index 5d41f02917..66f9906604 100644 --- a/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt +++ b/app/src/main/java/org/session/libsession/messaging/utilities/UpdateMessageData.kt @@ -1,95 +1,111 @@ package org.session.libsession.messaging.utilities -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.core.JsonParseException +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonClassDiscriminator import org.session.libsession.messaging.messages.control.GroupUpdated +import org.session.libsignal.utilities.Log import org.session.protos.SessionProtos.GroupUpdateInfoChangeMessage import org.session.protos.SessionProtos.GroupUpdateMemberChangeMessage.Type -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.utilities.Log -import java.util.Collections - -// class used to save update messages details -class UpdateMessageData () { - - var kind: Kind? = null - - //the annotations below are required for serialization. Any new Kind class MUST be declared as JsonSubTypes as well - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) - @JsonSubTypes( - JsonSubTypes.Type(Kind.GroupCreation::class, name = "GroupCreation"), - JsonSubTypes.Type(Kind.GroupNameChange::class, name = "GroupNameChange"), - JsonSubTypes.Type(Kind.GroupMemberAdded::class, name = "GroupMemberAdded"), - JsonSubTypes.Type(Kind.GroupMemberRemoved::class, name = "GroupMemberRemoved"), - JsonSubTypes.Type(Kind.GroupMemberLeft::class, name = "GroupMemberLeft"), - JsonSubTypes.Type(Kind.OpenGroupInvitation::class, name = "OpenGroupInvitation"), - JsonSubTypes.Type(Kind.GroupAvatarUpdated::class, name = "GroupAvatarUpdated"), - JsonSubTypes.Type(Kind.GroupMemberUpdated::class, name = "GroupMemberUpdated"), - JsonSubTypes.Type(Kind.GroupExpirationUpdated::class, name = "GroupExpirationUpdated"), - JsonSubTypes.Type(Kind.GroupInvitation::class, name = "GroupInvitation"), - JsonSubTypes.Type(Kind.GroupLeaving::class, name = "GroupLeaving"), - JsonSubTypes.Type(Kind.GroupErrorQuit::class, name = "GroupErrorQuit"), - ) - sealed class Kind { - data object GroupCreation: Kind() - class GroupNameChange(val name: String): Kind() { - constructor(): this("") //default constructor required for json serialization - } - class GroupMemberAdded(val updatedMembers: Collection, val groupName: String): Kind() { - constructor(): this(Collections.emptyList(), "") - } - class GroupMemberRemoved(val updatedMembers: Collection, val groupName:String): Kind() { - constructor(): this(Collections.emptyList(), "") - } - class GroupMemberLeft(val updatedMembers: Collection, val groupName:String): Kind() { - constructor(): this(Collections.emptyList(), "") - } + +/** + * Represents certain type of message. + * + * This class is an afterthought to save "rich message" into a message's body as JSON text. + * We've since moved away from this setup, a dedicated + * [org.thoughtcrime.securesms.database.model.content.MessageContent] is now used for rich + * message types. + * + * If you want to store a new message type, you should use the new setup instead. + * + * We'll look into migrating this class into the new setup in the future. + */ +@Serializable +class UpdateMessageData(val kind: Kind) { + + @OptIn(ExperimentalSerializationApi::class) + @Serializable + @JsonClassDiscriminator("@type") + sealed interface Kind { + @Serializable + @SerialName("GroupCreation") + data object GroupCreation: Kind + + @Serializable + @SerialName("GroupNameChange") + class GroupNameChange(val name: String): Kind + + @Serializable + @SerialName("GroupMemberAdded") + class GroupMemberAdded(val updatedMembers: Collection, val groupName: String): Kind + + @Serializable + @SerialName("GroupMemberRemoved") + class GroupMemberRemoved(val updatedMembers: Collection, val groupName:String): Kind + + @Serializable + @SerialName("GroupMemberLeft") + class GroupMemberLeft(val updatedMembers: Collection, val groupName:String): Kind + + @Serializable + @SerialName("GroupMemberUpdated") class GroupMemberUpdated( val sessionIds: List, val type: MemberUpdateType?, val groupName: String, val historyShared: Boolean - ): Kind() { - constructor(): this(emptyList(), null, "", false) - } - data object GroupAvatarUpdated: Kind() - class GroupExpirationUpdated(val updatedExpiration: Long, val updatingAdmin: String): Kind() { - constructor(): this(0L, "") - } - class OpenGroupInvitation(val groupUrl: String, val groupName: String): Kind() { - constructor(): this("", "") - } - data object GroupLeaving: Kind() - data class GroupErrorQuit(val groupName: String): Kind() { - constructor(): this("") - } + ): Kind + + @Serializable + @SerialName("GroupAvatarUpdated") + data object GroupAvatarUpdated: Kind + + @Serializable + @SerialName("GroupExpirationUpdated") + class GroupExpirationUpdated(val updatedExpiration: Long, val updatingAdmin: String): Kind + + @Serializable + @SerialName("OpenGroupInvitation") + class OpenGroupInvitation(val groupUrl: String, val groupName: String): Kind + + @Serializable + @SerialName("GroupLeaving") + data object GroupLeaving: Kind + + @Serializable + @SerialName("GroupErrorQuit") + data class GroupErrorQuit(val groupName: String): Kind + + @Serializable + @SerialName("GroupInvitation") class GroupInvitation( val groupAccountId: String, val invitingAdminId: String, val invitingAdminName: String?, val groupName: String - ) : Kind() { - constructor(): this("", "", null, "") - } + ) : Kind } - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) - @JsonSubTypes( - JsonSubTypes.Type(MemberUpdateType.ADDED::class, name = "ADDED"), - JsonSubTypes.Type(MemberUpdateType.REMOVED::class, name = "REMOVED"), - JsonSubTypes.Type(MemberUpdateType.PROMOTED::class, name = "PROMOTED"), - ) - sealed class MemberUpdateType { - data object ADDED: MemberUpdateType() - data object REMOVED: MemberUpdateType() - data object PROMOTED: MemberUpdateType() - } + @Serializable + @JsonClassDiscriminator("@type") + sealed interface MemberUpdateType { + @Serializable + @SerialName("ADDED") + data object ADDED: MemberUpdateType + + @Serializable + @SerialName("REMOVED") + data object REMOVED: MemberUpdateType + + @Serializable + @SerialName("PROMOTED") + data object PROMOTED: MemberUpdateType - constructor(kind: Kind): this() { - this.kind = kind } + companion object { val TAG = UpdateMessageData::class.simpleName @@ -137,19 +153,17 @@ class UpdateMessageData () { } @JvmStatic - fun fromJSON(json: String): UpdateMessageData? { - return try { - JsonUtil.fromJson(json, UpdateMessageData::class.java) - } catch (e: JsonParseException) { - Log.e(TAG, "${e.message}") - null - } + fun fromJSON(json: Json, value: String): UpdateMessageData? { + return runCatching { + json.decodeFromString(value) + }.onFailure { Log.e(TAG, "Error decoding updateMessageData", it) } + .getOrNull() } } - fun toJSON(): String { - return JsonUtil.toJson(this) + fun toJSON(json: Json): String { + return json.encodeToString(this) } fun isGroupLeavingKind(): Boolean { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt index 62329b2cda..09d5f8bc0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt @@ -8,6 +8,7 @@ import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import network.loki.messenger.R import network.loki.messenger.databinding.ViewOpenGroupInvitationBinding +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.utilities.OpenGroupUrlParser import org.thoughtcrime.securesms.database.model.MessageRecord @@ -23,7 +24,7 @@ class OpenGroupInvitationView : LinearLayout { fun bind(message: MessageRecord, @ColorInt textColor: Int) { // FIXME: This is a really weird approach... - val umd = UpdateMessageData.fromJSON(message.body)!! + val umd = UpdateMessageData.fromJSON(MessagingModuleConfiguration.shared.json, message.body)!! val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation this.data = data val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt index baeffda3f9..2975ee7431 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.conversation.v2.utilities +import kotlinx.serialization.json.Json import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.visible.LinkPreview @@ -17,6 +18,7 @@ import javax.inject.Inject class ResendMessageUtilities @Inject constructor( private val messageSender: MessageSender, private val storage: StorageProtocol, + private val json: Json, ) { suspend fun resend(accountId: String?, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { @@ -25,7 +27,7 @@ class ResendMessageUtilities @Inject constructor( message.id = messageRecord.messageId if (messageRecord.isOpenGroupInvitation) { val openGroupInvitation = OpenGroupInvitation() - UpdateMessageData.fromJSON(messageRecord.body)?.let { updateMessageData -> + UpdateMessageData.fromJSON(json, messageRecord.body)?.let { updateMessageData -> val kind = updateMessageData.kind if (kind is UpdateMessageData.Kind.OpenGroupInvitation) { openGroupInvitation.name = kind.groupName diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 4104419edd..8c8e52cbef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -6,6 +6,7 @@ import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import network.loki.messenger.libsession_util.MutableConversationVolatileConfig import network.loki.messenger.libsession_util.PRIORITY_PINNED import network.loki.messenger.libsession_util.PRIORITY_VISIBLE @@ -108,6 +109,7 @@ open class Storage @Inject constructor( private val openGroupManager: Lazy, private val recipientRepository: RecipientRepository, private val loginStateRepository: LoginStateRepository, + private val json: Json, ) : Database(context, helper), StorageProtocol { override fun getUserPublicKey(): String? { return loginStateRepository.peekLoginState()?.accountId?.hexString } @@ -368,6 +370,7 @@ open class Storage @Inject constructor( val insertResult = if (isUserSender || isUserBlindedSender) { val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation( + json = json, invitation = message.openGroupInvitation!!, recipient = targetAddress, sentTimestampMillis = message.sentTimestamp!!, @@ -385,6 +388,7 @@ open class Storage @Inject constructor( smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate) } else { val textMessage = if (isOpenGroupInvitation) IncomingTextMessage.fromOpenGroupInvitation( + json = json, invitation = message.openGroupInvitation!!, sender = senderAddress, sentTimestampMillis = message.sentTimestamp!!, @@ -797,7 +801,7 @@ open class Storage @Inject constructor( val expiryMode = recipient.expiryMode val expiresInMillis = expiryMode.expiryMillis val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0 - val inviteJson = updateData.toJSON() + val inviteJson = updateData.toJSON(json) if (senderPublicKey == null || senderPublicKey == userPublicKey) { val infoMessage = OutgoingMediaMessage( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 577f6dad6b..69e8934e7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.utilities.UpdateMessageData; import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.model.content.MessageContent; @@ -106,19 +107,18 @@ public boolean isMediaPending() { @Nullable public UpdateMessageData getGroupUpdateMessage() { if (isGroupUpdateMessage()) { - groupUpdateMessage = UpdateMessageData.Companion.fromJSON(getBody()); + groupUpdateMessage = UpdateMessageData.Companion.fromJSON( + MessagingModuleConfiguration.getShared().getJson(), + getBody() + ); } return groupUpdateMessage; } public boolean isGroupExpirationTimerUpdate() { - if (!isGroupUpdateMessage()) { - return false; - } - - UpdateMessageData updateMessageData = UpdateMessageData.Companion.fromJSON(getBody()); - return updateMessageData != null && updateMessageData.getKind() instanceof UpdateMessageData.Kind.GroupExpirationUpdated; + return getGroupUpdateMessage() != null && + getGroupUpdateMessage().getKind() instanceof UpdateMessageData.Kind.GroupExpirationUpdated; } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/DefaultConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/DefaultConversationRepository.kt index fc373ac95d..d772ade0d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/DefaultConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/DefaultConversationRepository.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsession.database.MessageDataProvider @@ -95,6 +96,7 @@ class DefaultConversationRepository @Inject constructor( private val banUserApiFactory: BanUserApi.Factory, private val unbanUserApiFactory: UnbanUserApi.Factory, private val deleteUserMessageApiFactory: DeleteUserMessagesApi.Factory, + private val json: Json, ) : ConversationRepository { override val conversationListAddressesFlow get() = loginStateRepository.flowWithLoggedInState { @@ -227,6 +229,7 @@ class DefaultConversationRepository @Inject constructor( val expirationConfig = recipientRepository.getRecipientSync(contact).expiryMode val expireStartedAt = if (expirationConfig is ExpiryMode.AfterSend) message.sentTimestamp!! else 0 val outgoingTextMessage = OutgoingTextMessage.Companion.fromOpenGroupInvitation( + json, openGroupInvitation, contact, message.sentTimestamp!!, From 7e98e1e34650bbc2a2dd3adf723a2f8750e7fe80 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 16 Feb 2026 16:29:47 +1100 Subject: [PATCH 4/4] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../thoughtcrime/securesms/database/model/MessageRecord.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 69e8934e7a..9f45ac1317 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -117,8 +117,9 @@ public UpdateMessageData getGroupUpdateMessage() { } public boolean isGroupExpirationTimerUpdate() { - return getGroupUpdateMessage() != null && - getGroupUpdateMessage().getKind() instanceof UpdateMessageData.Kind.GroupExpirationUpdated; + UpdateMessageData message = getGroupUpdateMessage(); + return message != null && + message.getKind() instanceof UpdateMessageData.Kind.GroupExpirationUpdated; } @Override