further clean up

This commit is contained in:
ryanzhao 2023-05-10 15:36:10 +10:00
parent fa71ea1850
commit 89545f0406
23 changed files with 0 additions and 8943 deletions

View File

@ -407,12 +407,6 @@
<action android:name="network.loki.securesms.RESTART" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.service.LocalBackupListener"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.service.PersistentConnectionBootListener"
android:exported="true">
<intent-filter>

View File

@ -55,7 +55,6 @@ import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.Storage;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@ -78,7 +77,6 @@ import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
@ -142,7 +140,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject LokiAPIDatabase lokiAPIDatabase;
@Inject Storage storage;
@Inject MessageDataProvider messageDataProvider;
@Inject JobDatabase jobDatabase;
@Inject TextSecurePreferences textSecurePreferences;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@ -364,10 +361,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private void initializePeriodicTasks() {
BackgroundPollWorker.schedulePeriodic(this);
if (BuildConfig.PLAY_STORE_DISABLED) {
UpdateApkRefreshListener.schedule(this);
}
}
private void initializeWebRtc() {

View File

@ -1,14 +0,0 @@
package org.thoughtcrime.securesms.backup
data class BackupEvent constructor(val type: Type, val count: Int, val exception: Exception?) {
enum class Type {
PROGRESS, FINISHED
}
companion object {
@JvmStatic fun createProgress(count: Int) = BackupEvent(Type.PROGRESS, count, null)
@JvmStatic fun createFinished() = BackupEvent(Type.FINISHED, 0, null)
@JvmStatic fun createFinished(e: Exception?) = BackupEvent(Type.FINISHED, 0, e)
}
}

View File

@ -1,47 +0,0 @@
package org.thoughtcrime.securesms.backup;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
import org.session.libsignal.utilities.Log;
import org.session.libsession.utilities.TextSecurePreferences;
/**
* Allows the getting and setting of the backup passphrase, which is stored encrypted on API >= 23.
*/
public class BackupPassphrase {
private static final String TAG = BackupPassphrase.class.getSimpleName();
public static @Nullable String get(@NonNull Context context) {
String passphrase = TextSecurePreferences.getBackupPassphrase(context);
String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
if (Build.VERSION.SDK_INT < 23 || (passphrase == null && encryptedPassphrase == null)) {
return passphrase;
}
if (encryptedPassphrase == null) {
Log.i(TAG, "Migrating to encrypted passphrase.");
set(context, passphrase);
encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
}
KeyStoreHelper.SealedData data = KeyStoreHelper.SealedData.fromString(encryptedPassphrase);
return new String(KeyStoreHelper.unseal(data));
}
public static void set(@NonNull Context context, @Nullable String passphrase) {
if (passphrase == null || Build.VERSION.SDK_INT < 23) {
TextSecurePreferences.setBackupPassphrase(context, passphrase);
TextSecurePreferences.setEncryptedBackupPassphrase(context, null);
} else {
KeyStoreHelper.SealedData encryptedPassphrase = KeyStoreHelper.seal(passphrase.getBytes());
TextSecurePreferences.setEncryptedBackupPassphrase(context, encryptedPassphrase.serialize());
TextSecurePreferences.setBackupPassphrase(context, null);
}
}
}

View File

@ -1,101 +0,0 @@
package org.thoughtcrime.securesms.backup
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.preference.PreferenceManager
import android.preference.PreferenceManager.getDefaultSharedPreferencesName
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_BOOLEAN
import org.thoughtcrime.securesms.backup.FullBackupImporter.PREF_PREFIX_TYPE_INT
import java.util.*
object BackupPreferences {
// region Backup related
fun getBackupRecords(context: Context): List<BackupProtos.SharedPreference> {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val prefsFileName: String
prefsFileName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
getDefaultSharedPreferencesName(context)
} else {
context.packageName + "_preferences"
}
val prefList: LinkedList<BackupProtos.SharedPreference> = LinkedList<BackupProtos.SharedPreference>()
addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_REGISTRATION_ID_PREF)
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.LOCAL_NUMBER_PREF)
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_NAME_PREF)
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_URL_PREF)
addBackupEntryInt(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_AVATAR_ID_PREF)
addBackupEntryString(prefList, preferences, prefsFileName, TextSecurePreferences.PROFILE_KEY_PREF)
addBackupEntryBoolean(prefList, preferences, prefsFileName, TextSecurePreferences.IS_USING_FCM)
return prefList
}
private fun addBackupEntryString(
outPrefList: MutableList<BackupProtos.SharedPreference>,
prefs: SharedPreferences,
prefFileName: String,
prefKey: String,
) {
val value = prefs.getString(prefKey, null)
if (value == null) {
logBackupEntry(prefKey, false)
return
}
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefFileName)
.setKey(prefKey)
.setValue(value)
.build())
logBackupEntry(prefKey, true)
}
private fun addBackupEntryInt(
outPrefList: MutableList<BackupProtos.SharedPreference>,
prefs: SharedPreferences,
prefFileName: String,
prefKey: String,
) {
val value = prefs.getInt(prefKey, -1)
if (value == -1) {
logBackupEntry(prefKey, false)
return
}
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefFileName)
.setKey(PREF_PREFIX_TYPE_INT + prefKey) // The prefix denotes the type of the preference.
.setValue(value.toString())
.build())
logBackupEntry(prefKey, true)
}
private fun addBackupEntryBoolean(
outPrefList: MutableList<BackupProtos.SharedPreference>,
prefs: SharedPreferences,
prefFileName: String,
prefKey: String,
) {
if (!prefs.contains(prefKey)) {
logBackupEntry(prefKey, false)
return
}
outPrefList.add(BackupProtos.SharedPreference.newBuilder()
.setFile(prefFileName)
.setKey(PREF_PREFIX_TYPE_BOOLEAN + prefKey) // The prefix denotes the type of the preference.
.setValue(prefs.getBoolean(prefKey, false).toString())
.build())
logBackupEntry(prefKey, true)
}
private fun logBackupEntry(prefName: String, wasIncluded: Boolean) {
val sb = StringBuilder()
sb.append("Backup preference ")
sb.append(if (wasIncluded) "+ " else "- ")
sb.append('\"').append(prefName).append("\" ")
if (!wasIncluded) {
sb.append("(is empty and not included)")
}
Log.d("Loki", sb.toString())
} // endregion
}

View File

@ -1,447 +0,0 @@
package org.thoughtcrime.securesms.backup
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.text.TextUtils
import androidx.annotation.WorkerThread
import com.annimon.stream.function.Consumer
import com.annimon.stream.function.Predicate
import com.google.protobuf.ByteString
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.greenrobot.eventbus.EventBus
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.utilities.Conversions
import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
import org.thoughtcrime.securesms.backup.BackupProtos.Header
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
import org.thoughtcrime.securesms.backup.BackupProtos.Sticker
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.PushDatabase
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.util.BackupUtil
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.Flushable
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.LinkedList
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.Mac
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object FullBackupExporter {
private val TAG = FullBackupExporter::class.java.simpleName
@JvmStatic
@WorkerThread
@Throws(IOException::class)
fun export(context: Context,
attachmentSecret: AttachmentSecret,
input: SQLiteDatabase,
fileUri: Uri,
passphrase: String) {
val baseOutputStream = context.contentResolver.openOutputStream(fileUri)
?: throw IOException("Cannot open an output stream for the file URI: $fileUri")
var count = 0
try {
BackupFrameOutputStream(baseOutputStream, passphrase).use { outputStream ->
outputStream.writeDatabaseVersion(input.version)
val tables = exportSchema(input, outputStream)
for (table in tables) if (shouldExportTable(table)) {
count = when (table) {
SmsDatabase.TABLE_NAME, MmsDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0
},
null,
count)
}
GroupReceiptDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID)))
},
null,
count)
}
AttachmentDatabase.TABLE_NAME -> {
exportTable(table, input, outputStream,
{ cursor: Cursor ->
isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)))
},
{ cursor: Cursor ->
exportAttachment(attachmentSecret, cursor, outputStream)
},
count)
}
else -> {
exportTable(table, input, outputStream, null, null, count)
}
}
}
for (preference in BackupUtil.getBackupRecords(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writePreferenceEntry(preference)
}
for (preference in BackupPreferences.getBackupRecords(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writePreferenceEntry(preference)
}
for (avatar in AvatarHelper.getAvatarFiles(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writeAvatar(avatar.name, FileInputStream(avatar), avatar.length())
}
outputStream.writeEnd()
}
EventBus.getDefault().post(BackupEvent.createFinished())
} catch (e: Exception) {
Log.e(TAG, "Failed to make full backup.", e)
EventBus.getDefault().post(BackupEvent.createFinished(e))
throw e
}
}
private inline fun shouldExportTable(table: String): Boolean {
return table != PushDatabase.TABLE_NAME &&
table != LokiBackupFilesDatabase.TABLE_NAME &&
table != LokiAPIDatabase.openGroupProfilePictureTable &&
table != JobDatabase.Jobs.TABLE_NAME &&
table != JobDatabase.Constraints.TABLE_NAME &&
table != JobDatabase.Dependencies.TABLE_NAME &&
!table.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME) &&
!table.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME) &&
!table.startsWith("sqlite_")
}
@Throws(IOException::class)
private fun exportSchema(input: SQLiteDatabase, outputStream: BackupFrameOutputStream): List<String> {
val tables: MutableList<String> = LinkedList()
input.rawQuery("SELECT sql, name, type FROM sqlite_master", null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val sql = cursor.getString(0)
val name = cursor.getString(1)
val type = cursor.getString(2)
if (sql != null) {
val isSmsFtsSecretTable = name != null && name != SearchDatabase.SMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME)
val isMmsFtsSecretTable = name != null && name != SearchDatabase.MMS_FTS_TABLE_NAME && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME)
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
if ("table" == type) {
tables.add(name)
}
outputStream.writeSql(SqlStatement.newBuilder().setStatement(cursor.getString(0)).build())
}
}
}
}
return tables
}
@Throws(IOException::class)
private fun exportTable(table: String,
input: SQLiteDatabase,
outputStream: BackupFrameOutputStream,
predicate: Predicate<Cursor>?,
postProcess: Consumer<Cursor>?,
count: Int): Int {
var count = count
val template = "INSERT INTO $table VALUES "
input.rawQuery("SELECT * FROM $table", null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
if (predicate != null && !predicate.test(cursor)) continue
val statement = StringBuilder(template)
val statementBuilder = SqlStatement.newBuilder()
statement.append('(')
for (i in 0 until cursor.columnCount) {
statement.append('?')
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_STRING -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setStringParamter(cursor.getString(i)))
}
Cursor.FIELD_TYPE_FLOAT -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setDoubleParameter(cursor.getDouble(i)))
}
Cursor.FIELD_TYPE_INTEGER -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setIntegerParameter(cursor.getLong(i)))
}
Cursor.FIELD_TYPE_BLOB -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setBlobParameter(ByteString.copyFrom(cursor.getBlob(i))))
}
Cursor.FIELD_TYPE_NULL -> {
statementBuilder.addParameters(SqlStatement.SqlParameter.newBuilder()
.setNullparameter(true))
}
else -> {
throw AssertionError("unknown type?" + cursor.getType(i))
}
}
if (i < cursor.columnCount - 1) {
statement.append(',')
}
}
statement.append(')')
outputStream.writeSql(statementBuilder.setStatement(statement.toString()).build())
postProcess?.accept(cursor)
}
}
return count
}
private fun exportAttachment(attachmentSecret: AttachmentSecret, cursor: Cursor, outputStream: BackupFrameOutputStream) {
try {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID))
val uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))
var size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE))
val data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA))
val random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM))
if (!TextUtils.isEmpty(data) && size <= 0) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data)
}
if (!TextUtils.isEmpty(data) && size > 0) {
val inputStream: InputStream = if (random != null && random.size == 32) {
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
} else {
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
}
outputStream.writeAttachment(AttachmentId(rowId, uniqueId), inputStream, size)
}
} catch (e: IOException) {
Log.w(TAG, e)
}
}
@Throws(IOException::class)
private fun calculateVeryOldStreamLength(attachmentSecret: AttachmentSecret, random: ByteArray?, data: String): Long {
var result: Long = 0
val inputStream: InputStream = if (random != null && random.size == 32) {
ModernDecryptingPartInputStream.createFor(attachmentSecret, random, File(data), 0)
} else {
ClassicDecryptingPartInputStream.createFor(attachmentSecret, File(data))
}
var read: Int
val buffer = ByteArray(8192)
while (inputStream.read(buffer, 0, buffer.size).also { read = it } != -1) {
result += read.toLong()
}
return result
}
private fun isForNonExpiringMessage(db: SQLiteDatabase, mmsId: Long): Boolean {
val columns = arrayOf(MmsSmsColumns.EXPIRES_IN)
val where = MmsSmsColumns.ID + " = ?"
val args = arrayOf(mmsId.toString())
db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null).use { mmsCursor ->
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return mmsCursor.getLong(0) == 0L
}
}
return false
}
private class BackupFrameOutputStream : Closeable, Flushable {
private val outputStream: OutputStream
private var cipher: Cipher
private var mac: Mac
private val cipherKey: ByteArray
private val macKey: ByteArray
private val iv: ByteArray
private var counter: Int = 0
constructor(outputStream: OutputStream, passphrase: String) : super() {
try {
val salt = Util.getSecretBytes(32)
val key = BackupUtil.computeBackupKey(passphrase, salt)
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
val split = ByteUtil.split(derived, 32, 32)
cipherKey = split[0]
macKey = split[1]
cipher = Cipher.getInstance("AES/CTR/NoPadding")
mac = Mac.getInstance("HmacSHA256")
this.outputStream = outputStream
iv = Util.getSecretBytes(16)
counter = Conversions.byteArrayToInt(iv)
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
val header = BackupFrame.newBuilder().setHeader(Header.newBuilder()
.setIv(ByteString.copyFrom(iv))
.setSalt(ByteString.copyFrom(salt)))
.build().toByteArray()
outputStream.write(Conversions.intToByteArray(header.size))
outputStream.write(header)
} catch (e: Exception) {
when (e) {
is NoSuchAlgorithmException,
is NoSuchPaddingException,
is InvalidKeyException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
fun writeSql(statement: SqlStatement) {
write(outputStream, BackupFrame.newBuilder().setStatement(statement).build())
}
@Throws(IOException::class)
fun writePreferenceEntry(preference: SharedPreference?) {
write(outputStream, BackupFrame.newBuilder().setPreference(preference).build())
}
@Throws(IOException::class)
fun writeAvatar(avatarName: String, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setAvatar(Avatar.newBuilder()
.setName(avatarName)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeAttachment(attachmentId: AttachmentId, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setAttachment(Attachment.newBuilder()
.setRowId(attachmentId.rowId)
.setAttachmentId(attachmentId.uniqueId)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeSticker(rowId: Long, inputStream: InputStream, size: Long) {
write(outputStream, BackupFrame.newBuilder()
.setSticker(Sticker.newBuilder()
.setRowId(rowId)
.setLength(Util.toIntExact(size))
.build())
.build())
writeStream(inputStream)
}
@Throws(IOException::class)
fun writeDatabaseVersion(version: Int) {
write(outputStream, BackupFrame.newBuilder()
.setVersion(DatabaseVersion.newBuilder().setVersion(version))
.build())
}
@Throws(IOException::class)
fun writeEnd() {
write(outputStream, BackupFrame.newBuilder().setEnd(true).build())
}
@Throws(IOException::class)
private fun writeStream(inputStream: InputStream) {
try {
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
mac.update(iv)
val buffer = ByteArray(8192)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
val ciphertext = cipher.update(buffer, 0, read)
if (ciphertext != null) {
outputStream.write(ciphertext)
mac.update(ciphertext)
}
}
val remainder = cipher.doFinal()
outputStream.write(remainder)
mac.update(remainder)
val attachmentDigest = mac.doFinal()
outputStream.write(attachmentDigest, 0, 10)
} catch (e: Exception) {
when (e) {
is InvalidKeyException,
is InvalidAlgorithmParameterException,
is IllegalBlockSizeException,
is BadPaddingException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
private fun write(out: OutputStream, frame: BackupFrame) {
try {
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
val frameCiphertext = cipher.doFinal(frame.toByteArray())
val frameMac = mac.doFinal(frameCiphertext)
val length = Conversions.intToByteArray(frameCiphertext.size + 10)
out.write(length)
out.write(frameCiphertext)
out.write(frameMac, 0, 10)
} catch (e: Exception) {
when (e) {
is InvalidKeyException,
is InvalidAlgorithmParameterException,
is IllegalBlockSizeException,
is BadPaddingException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
override fun flush() {
outputStream.flush()
}
@Throws(IOException::class)
override fun close() {
outputStream.close()
}
}
}

View File

@ -1,352 +0,0 @@
package org.thoughtcrime.securesms.backup
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import androidx.annotation.WorkerThread
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.greenrobot.eventbus.EventBus
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Conversions
import org.session.libsession.utilities.Util
import org.session.libsignal.crypto.kdf.HKDFv3
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment
import org.thoughtcrime.securesms.backup.BackupProtos.Avatar
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.BackupProtos.SqlStatement
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.SearchDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.BackupUtil
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.LinkedList
import java.util.Locale
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.Mac
import javax.crypto.NoSuchPaddingException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object FullBackupImporter {
/**
* Because BackupProtos.SharedPreference was made only to serialize string values,
* we use these 3-char prefixes to explicitly cast the values before inserting to a preference file.
*/
const val PREF_PREFIX_TYPE_INT = "i__"
const val PREF_PREFIX_TYPE_BOOLEAN = "b__"
private val TAG = FullBackupImporter::class.java.simpleName
@JvmStatic
@WorkerThread
@Throws(IOException::class)
fun importFromUri(context: Context,
attachmentSecret: AttachmentSecret,
db: SQLiteDatabase,
fileUri: Uri,
passphrase: String) {
val baseInputStream = context.contentResolver.openInputStream(fileUri)
?: throw IOException("Cannot open an input stream for the file URI: $fileUri")
var count = 0
try {
BackupRecordInputStream(baseInputStream, passphrase).use { inputStream ->
db.beginTransaction()
dropAllTables(db)
var frame: BackupFrame
while (!inputStream.readFrame().also { frame = it }.end) {
if (count++ % 100 == 0) EventBus.getDefault().post(BackupEvent.createProgress(count))
when {
frame.hasVersion() -> processVersion(db, frame.version)
frame.hasStatement() -> processStatement(db, frame.statement)
frame.hasPreference() -> processPreference(context, frame.preference)
frame.hasAttachment() -> processAttachment(context, attachmentSecret, db, frame.attachment, inputStream)
frame.hasAvatar() -> processAvatar(context, frame.avatar, inputStream)
}
}
trimEntriesForExpiredMessages(context, db)
db.setTransactionSuccessful()
}
} finally {
if (db.inTransaction()) {
db.endTransaction()
}
}
EventBus.getDefault().post(BackupEvent.createFinished())
}
@Throws(IOException::class)
private fun processVersion(db: SQLiteDatabase, version: DatabaseVersion) {
if (version.version > db.version) {
throw DatabaseDowngradeException(db.version, version.version)
}
db.version = version.version
}
private fun processStatement(db: SQLiteDatabase, statement: SqlStatement) {
val isForSmsFtsSecretTable = statement.statement.contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_")
val isForMmsFtsSecretTable = statement.statement.contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_")
val isForSqliteSecretTable = statement.statement.toLowerCase(Locale.ENGLISH).startsWith("create table sqlite_")
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForSqliteSecretTable) {
Log.i(TAG, "Ignoring import for statement: " + statement.statement)
return
}
val parameters: MutableList<Any?> = LinkedList()
for (parameter in statement.parametersList) {
when {
parameter.hasStringParamter() -> parameters.add(parameter.stringParamter)
parameter.hasDoubleParameter() -> parameters.add(parameter.doubleParameter)
parameter.hasIntegerParameter() -> parameters.add(parameter.integerParameter)
parameter.hasBlobParameter() -> parameters.add(parameter.blobParameter.toByteArray())
parameter.hasNullparameter() -> parameters.add(null)
}
}
if (parameters.size > 0) {
db.execSQL(statement.statement, parameters.toTypedArray())
} else {
db.execSQL(statement.statement)
}
}
@Throws(IOException::class)
private fun processAttachment(context: Context, attachmentSecret: AttachmentSecret,
db: SQLiteDatabase, attachment: Attachment,
inputStream: BackupRecordInputStream) {
val partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE)
val dataFile = File.createTempFile("part", ".mms", partsDirectory)
val output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false)
inputStream.readAttachmentTo(output.second, attachment.length)
val contentValues = ContentValues()
contentValues.put(AttachmentDatabase.DATA, dataFile.absolutePath)
contentValues.put(AttachmentDatabase.THUMBNAIL, null as String?)
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first)
db.update(AttachmentDatabase.TABLE_NAME, contentValues,
"${AttachmentDatabase.ROW_ID} = ? AND ${AttachmentDatabase.UNIQUE_ID} = ?",
arrayOf(attachment.rowId.toString(), attachment.attachmentId.toString()))
}
@Throws(IOException::class)
private fun processAvatar(context: Context, avatar: Avatar, inputStream: BackupRecordInputStream) {
inputStream.readAttachmentTo(FileOutputStream(
AvatarHelper.getAvatarFile(context, Address.fromExternal(context, avatar.name))), avatar.length)
}
@SuppressLint("ApplySharedPref")
private fun processPreference(context: Context, preference: SharedPreference) {
val preferences = context.getSharedPreferences(preference.file, 0)
val key = preference.key
val value = preference.value
// See the comment next to PREF_PREFIX_TYPE_* constants.
when {
key.startsWith(PREF_PREFIX_TYPE_INT) ->
preferences.edit().putInt(
key.substring(PREF_PREFIX_TYPE_INT.length),
value.toInt()
).commit()
key.startsWith(PREF_PREFIX_TYPE_BOOLEAN) ->
preferences.edit().putBoolean(
key.substring(PREF_PREFIX_TYPE_BOOLEAN.length),
value.toBoolean()
).commit()
else ->
preferences.edit().putString(key, value).commit()
}
}
private fun dropAllTables(db: SQLiteDatabase) {
db.rawQuery("SELECT name, type FROM sqlite_master", null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val name = cursor.getString(0)
val type = cursor.getString(1)
if ("table" == type && !name.startsWith("sqlite_")) {
db.execSQL("DROP TABLE IF EXISTS $name")
}
}
}
}
private fun trimEntriesForExpiredMessages(context: Context, db: SQLiteDatabase) {
val trimmedCondition = " NOT IN (SELECT ${MmsSmsColumns.ID} FROM ${MmsDatabase.TABLE_NAME})"
db.delete(GroupReceiptDatabase.TABLE_NAME, GroupReceiptDatabase.MMS_ID + trimmedCondition, null)
val columns = arrayOf(AttachmentDatabase.ROW_ID, AttachmentDatabase.UNIQUE_ID)
val where = AttachmentDatabase.MMS_ID + trimmedCondition
db.query(AttachmentDatabase.TABLE_NAME, columns, where, null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
DatabaseComponent.get(context).attachmentDatabase()
.deleteAttachment(AttachmentId(cursor.getLong(0), cursor.getLong(1)))
}
}
db.query(ThreadDatabase.TABLE_NAME, arrayOf(ThreadDatabase.ID),
ThreadDatabase.EXPIRES_IN + " > 0", null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
DatabaseComponent.get(context).threadDatabase().update(cursor.getLong(0), false)
}
}
}
private class BackupRecordInputStream : Closeable {
private val inputStream: InputStream
private val cipher: Cipher
private val mac: Mac
private val cipherKey: ByteArray
private val macKey: ByteArray
private val iv: ByteArray
private var counter = 0
@Throws(IOException::class)
constructor(inputStream: InputStream, passphrase: String) : super() {
try {
this.inputStream = inputStream
val headerLengthBytes = ByteArray(4)
Util.readFully(this.inputStream, headerLengthBytes)
val headerLength = Conversions.byteArrayToInt(headerLengthBytes)
val headerFrame = ByteArray(headerLength)
Util.readFully(this.inputStream, headerFrame)
val frame = BackupFrame.parseFrom(headerFrame)
if (!frame.hasHeader()) {
throw IOException("Backup stream does not start with header!")
}
val header = frame.header
iv = header.iv.toByteArray()
if (iv.size != 16) {
throw IOException("Invalid IV length!")
}
val key = BackupUtil.computeBackupKey(passphrase, if (header.hasSalt()) header.salt.toByteArray() else null)
val derived = HKDFv3().deriveSecrets(key, "Backup Export".toByteArray(), 64)
val split = ByteUtil.split(derived, 32, 32)
cipherKey = split[0]
macKey = split[1]
cipher = Cipher.getInstance("AES/CTR/NoPadding")
mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(macKey, "HmacSHA256"))
counter = Conversions.byteArrayToInt(iv)
} catch (e: Exception) {
when (e) {
is NoSuchAlgorithmException,
is NoSuchPaddingException,
is InvalidKeyException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
fun readFrame(): BackupFrame {
return readFrame(inputStream)
}
@Throws(IOException::class)
fun readAttachmentTo(out: OutputStream, length: Int) {
var length = length
try {
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
mac.update(iv)
val buffer = ByteArray(8192)
while (length > 0) {
val read = inputStream.read(buffer, 0, Math.min(buffer.size, length))
if (read == -1) throw IOException("File ended early!")
mac.update(buffer, 0, read)
val plaintext = cipher.update(buffer, 0, read)
if (plaintext != null) {
out.write(plaintext, 0, plaintext.size)
}
length -= read
}
val plaintext = cipher.doFinal()
if (plaintext != null) {
out.write(plaintext, 0, plaintext.size)
}
out.close()
val ourMac = ByteUtil.trim(mac.doFinal(), 10)
val theirMac = ByteArray(10)
try {
Util.readFully(inputStream, theirMac)
} catch (e: IOException) {
throw IOException(e)
}
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw IOException("Bad MAC")
}
} catch (e: Exception) {
when (e) {
is InvalidKeyException,
is InvalidAlgorithmParameterException,
is IllegalBlockSizeException,
is BadPaddingException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
private fun readFrame(`in`: InputStream?): BackupFrame {
return try {
val length = ByteArray(4)
Util.readFully(`in`, length)
val frame = ByteArray(Conversions.byteArrayToInt(length))
Util.readFully(`in`, frame)
val theirMac = ByteArray(10)
System.arraycopy(frame, frame.size - 10, theirMac, 0, theirMac.size)
mac.update(frame, 0, frame.size - 10)
val ourMac = ByteUtil.trim(mac.doFinal(), 10)
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw IOException("Bad MAC")
}
Conversions.intToByteArray(iv, 0, counter++)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(cipherKey, "AES"), IvParameterSpec(iv))
val plaintext = cipher.doFinal(frame, 0, frame.size - 10)
BackupFrame.parseFrom(plaintext)
} catch (e: Exception) {
when (e) {
is InvalidKeyException,
is InvalidAlgorithmParameterException,
is IllegalBlockSizeException,
is BadPaddingException -> {
throw AssertionError(e)
}
else -> throw e
}
}
}
@Throws(IOException::class)
override fun close() {
inputStream.close()
}
}
class DatabaseDowngradeException internal constructor(currentVersion: Int, backupVersion: Int) :
IOException("Tried to import a backup with version $backupVersion into a database with version $currentVersion")
}

View File

@ -1,21 +1,7 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class JobDatabase extends Database {
@ -23,8 +9,6 @@ public class JobDatabase extends Database {
Constraints.CREATE_TABLE,
Dependencies.CREATE_TABLE };
private static final ArrayList<String> runningJobs = new ArrayList<>();
public static final class Jobs {
public static final String TABLE_NAME = "job_spec";
private static final String ID = "_id";
@ -84,166 +68,4 @@ public class JobDatabase extends Database {
public JobDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (FullSpec fullSpec : fullSpecs) {
insertJobSpec(db, fullSpec.getJobSpec());
insertConstraintSpecs(db, fullSpec.getConstraintSpecs());
insertDependencySpecs(db, fullSpec.getDependencySpecs());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public synchronized @NonNull List<JobSpec> getAllJobSpecs() {
List<JobSpec> jobs = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) {
while (cursor != null && cursor.moveToNext()) {
jobs.add(jobSpecFromCursor(cursor));
}
}
return jobs;
}
public synchronized void updateJobRunningState(@NonNull String id, boolean isRunning) {
if (!isRunning) {
JobDatabase.runningJobs.remove(id);
} else if (!JobDatabase.runningJobs.contains(id)) {
JobDatabase.runningJobs.add(id);
}
}
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime) {
updateJobRunningState(id, isRunning);
ContentValues contentValues = new ContentValues();
contentValues.put(Jobs.RUN_ATTEMPT, runAttempt);
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, nextRunAttemptTime);
String query = Jobs.JOB_SPEC_ID + " = ?";
String[] args = new String[]{ id };
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
}
public synchronized void updateAllJobsToBePending() {
JobDatabase.runningJobs.clear();
}
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (String jobId : jobIds) {
String[] arg = new String[]{jobId};
db.delete(Jobs.TABLE_NAME, Jobs.JOB_SPEC_ID + " = ?", arg);
db.delete(Constraints.TABLE_NAME, Constraints.JOB_SPEC_ID + " = ?", arg);
db.delete(Dependencies.TABLE_NAME, Dependencies.JOB_SPEC_ID + " = ?", arg);
db.delete(Dependencies.TABLE_NAME, Dependencies.DEPENDS_ON_JOB_SPEC_ID + " = ?", arg);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public synchronized @NonNull List<ConstraintSpec> getAllConstraintSpecs() {
List<ConstraintSpec> constraints = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
constraints.add(constraintSpecFromCursor(cursor));
}
}
return constraints;
}
public synchronized @NonNull List<DependencySpec> getAllDependencySpecs() {
List<DependencySpec> dependencies = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
dependencies.add(dependencySpecFromCursor(cursor));
}
}
return dependencies;
}
private void insertJobSpec(@NonNull SQLiteDatabase db, @NonNull JobSpec job) {
ContentValues contentValues = new ContentValues();
contentValues.put(Jobs.JOB_SPEC_ID, job.getId());
contentValues.put(Jobs.FACTORY_KEY, job.getFactoryKey());
contentValues.put(Jobs.QUEUE_KEY, job.getQueueKey());
contentValues.put(Jobs.CREATE_TIME, job.getCreateTime());
contentValues.put(Jobs.NEXT_RUN_ATTEMPT_TIME, job.getNextRunAttemptTime());
contentValues.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
contentValues.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
contentValues.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
contentValues.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
contentValues.put(Jobs.LIFESPAN, job.getLifespan());
contentValues.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
updateJobRunningState(job.getId(), job.isRunning());
db.insertWithOnConflict(Jobs.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
}
private void insertConstraintSpecs(@NonNull SQLiteDatabase db, @NonNull List<ConstraintSpec> constraints) {
for (ConstraintSpec constraintSpec : constraints) {
ContentValues contentValues = new ContentValues();
contentValues.put(Constraints.JOB_SPEC_ID, constraintSpec.getJobSpecId());
contentValues.put(Constraints.FACTORY_KEY, constraintSpec.getFactoryKey());
db.insertWithOnConflict(Constraints.TABLE_NAME, null ,contentValues, SQLiteDatabase.CONFLICT_IGNORE);
}
}
private void insertDependencySpecs(@NonNull SQLiteDatabase db, @NonNull List<DependencySpec> dependencies) {
for (DependencySpec dependencySpec : dependencies) {
ContentValues contentValues = new ContentValues();
contentValues.put(Dependencies.JOB_SPEC_ID, dependencySpec.getJobId());
contentValues.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, dependencySpec.getDependsOnJobId());
db.insertWithOnConflict(Dependencies.TABLE_NAME, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE);
}
}
private @NonNull JobSpec jobSpecFromCursor(@NonNull Cursor cursor) {
String jobId = cursor.getString(cursor.getColumnIndexOrThrow(Jobs.JOB_SPEC_ID));
return new JobSpec(jobId,
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.FACTORY_KEY)),
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.QUEUE_KEY)),
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.CREATE_TIME)),
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.NEXT_RUN_ATTEMPT_TIME)),
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.RUN_ATTEMPT)),
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_ATTEMPTS)),
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.MAX_BACKOFF)),
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.LIFESPAN)),
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)),
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
JobDatabase.runningJobs.contains(jobId));
}
private @NonNull ConstraintSpec constraintSpecFromCursor(@NonNull Cursor cursor) {
return new ConstraintSpec(cursor.getString(cursor.getColumnIndexOrThrow(Constraints.JOB_SPEC_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(Constraints.FACTORY_KEY)));
}
private @NonNull DependencySpec dependencySpecFromCursor(@NonNull Cursor cursor) {
return new DependencySpec(cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.JOB_SPEC_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)));
}
}

View File

@ -32,7 +32,6 @@ interface DatabaseComponent {
fun recipientDatabase(): RecipientDatabase
fun groupReceiptDatabase(): GroupReceiptDatabase
fun searchDatabase(): SearchDatabase
fun jobDatabase(): JobDatabase
fun lokiAPIDatabase(): LokiAPIDatabase
fun lokiMessageDatabase(): LokiMessageDatabase
fun lokiThreadDatabase(): LokiThreadDatabase

View File

@ -86,10 +86,6 @@ object DatabaseModule {
@Singleton
fun searchDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = SearchDatabase(context,openHelper)
@Provides
@Singleton
fun provideJobDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = JobDatabase(context, openHelper)
@Provides
@Singleton
fun provideLokiApiDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = LokiAPIDatabase(context,openHelper)

View File

@ -1,9 +0,0 @@
package org.thoughtcrime.securesms.jobmanager;
import androidx.annotation.NonNull;
import java.util.concurrent.ExecutorService;
public interface ExecutorFactory {
@NonNull ExecutorService newSingleThreadExecutor(@NonNull String name);
}

View File

@ -1,15 +0,0 @@
package org.thoughtcrime.securesms.jobmanager.impl;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.jobmanager.ExecutorFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DefaultExecutorFactory implements ExecutorFactory {
@Override
public @NonNull ExecutorService newSingleThreadExecutor(@NonNull String name) {
return Executors.newSingleThreadExecutor(r -> new Thread(r, name));
}
}

View File

@ -1,43 +0,0 @@
package org.thoughtcrime.securesms.jobmanager.persistence;
import androidx.annotation.NonNull;
import java.util.Objects;
public final class ConstraintSpec {
private final String jobSpecId;
private final String factoryKey;
public ConstraintSpec(@NonNull String jobSpecId, @NonNull String factoryKey) {
this.jobSpecId = jobSpecId;
this.factoryKey = factoryKey;
}
public String getJobSpecId() {
return jobSpecId;
}
public String getFactoryKey() {
return factoryKey;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConstraintSpec that = (ConstraintSpec) o;
return Objects.equals(jobSpecId, that.jobSpecId) &&
Objects.equals(factoryKey, that.factoryKey);
}
@Override
public int hashCode() {
return Objects.hash(jobSpecId, factoryKey);
}
@Override
public @NonNull String toString() {
return String.format("jobSpecId: %s | factoryKey: %s", jobSpecId, factoryKey);
}
}

View File

@ -1,43 +0,0 @@
package org.thoughtcrime.securesms.jobmanager.persistence;
import androidx.annotation.NonNull;
import java.util.Objects;
public final class DependencySpec {
private final String jobId;
private final String dependsOnJobId;
public DependencySpec(@NonNull String jobId, @NonNull String dependsOnJobId) {
this.jobId = jobId;
this.dependsOnJobId = dependsOnJobId;
}
public @NonNull String getJobId() {
return jobId;
}
public @NonNull String getDependsOnJobId() {
return dependsOnJobId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DependencySpec that = (DependencySpec) o;
return Objects.equals(jobId, that.jobId) &&
Objects.equals(dependsOnJobId, that.dependsOnJobId);
}
@Override
public int hashCode() {
return Objects.hash(jobId, dependsOnJobId);
}
@Override
public @NonNull String toString() {
return String.format("jobSpecId: %s | dependsOnJobSpecId: %s", jobId, dependsOnJobId);
}
}

View File

@ -1,50 +0,0 @@
package org.thoughtcrime.securesms.jobmanager.persistence;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.Objects;
public final class FullSpec {
private final JobSpec jobSpec;
private final List<ConstraintSpec> constraintSpecs;
private final List<DependencySpec> dependencySpecs;
public FullSpec(@NonNull JobSpec jobSpec,
@NonNull List<ConstraintSpec> constraintSpecs,
@NonNull List<DependencySpec> dependencySpecs)
{
this.jobSpec = jobSpec;
this.constraintSpecs = constraintSpecs;
this.dependencySpecs = dependencySpecs;
}
public @NonNull JobSpec getJobSpec() {
return jobSpec;
}
public @NonNull List<ConstraintSpec> getConstraintSpecs() {
return constraintSpecs;
}
public @NonNull List<DependencySpec> getDependencySpecs() {
return dependencySpecs;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FullSpec fullSpec = (FullSpec) o;
return Objects.equals(jobSpec, fullSpec.jobSpec) &&
Objects.equals(constraintSpecs, fullSpec.constraintSpecs) &&
Objects.equals(dependencySpecs, fullSpec.dependencySpecs);
}
@Override
public int hashCode() {
return Objects.hash(jobSpec, constraintSpecs, dependencySpecs);
}
}

View File

@ -1,129 +0,0 @@
package org.thoughtcrime.securesms.jobmanager.persistence;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
public final class JobSpec {
private final String id;
private final String factoryKey;
private final String queueKey;
private final long createTime;
private final long nextRunAttemptTime;
private final int runAttempt;
private final int maxAttempts;
private final long maxBackoff;
private final long lifespan;
private final int maxInstances;
private final String serializedData;
private final boolean isRunning;
public JobSpec(@NonNull String id,
@NonNull String factoryKey,
@Nullable String queueKey,
long createTime,
long nextRunAttemptTime,
int runAttempt,
int maxAttempts,
long maxBackoff,
long lifespan,
int maxInstances,
@NonNull String serializedData,
boolean isRunning)
{
this.id = id;
this.factoryKey = factoryKey;
this.queueKey = queueKey;
this.createTime = createTime;
this.nextRunAttemptTime = nextRunAttemptTime;
this.maxBackoff = maxBackoff;
this.runAttempt = runAttempt;
this.maxAttempts = maxAttempts;
this.lifespan = lifespan;
this.maxInstances = maxInstances;
this.serializedData = serializedData;
this.isRunning = isRunning;
}
public @NonNull String getId() {
return id;
}
public @NonNull String getFactoryKey() {
return factoryKey;
}
public @Nullable String getQueueKey() {
return queueKey;
}
public long getCreateTime() {
return createTime;
}
public long getNextRunAttemptTime() {
return nextRunAttemptTime;
}
public int getRunAttempt() {
return runAttempt;
}
public int getMaxAttempts() {
return maxAttempts;
}
public long getMaxBackoff() {
return maxBackoff;
}
public int getMaxInstances() {
return maxInstances;
}
public long getLifespan() {
return lifespan;
}
public @NonNull String getSerializedData() {
return serializedData;
}
public boolean isRunning() {
return isRunning;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JobSpec jobSpec = (JobSpec) o;
return createTime == jobSpec.createTime &&
nextRunAttemptTime == jobSpec.nextRunAttemptTime &&
runAttempt == jobSpec.runAttempt &&
maxAttempts == jobSpec.maxAttempts &&
maxBackoff == jobSpec.maxBackoff &&
lifespan == jobSpec.lifespan &&
maxInstances == jobSpec.maxInstances &&
isRunning == jobSpec.isRunning &&
Objects.equals(id, jobSpec.id) &&
Objects.equals(factoryKey, jobSpec.factoryKey) &&
Objects.equals(queueKey, jobSpec.queueKey) &&
Objects.equals(serializedData, jobSpec.serializedData);
}
@Override
public int hashCode() {
return Objects.hash(id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, lifespan, maxInstances, serializedData, isRunning);
}
@SuppressLint("DefaultLocale")
@Override
public @NonNull String toString() {
return String.format("id: %s | factoryKey: %s | queueKey: %s | createTime: %d | nextRunAttemptTime: %d | runAttempt: %d | maxAttempts: %d | maxBackoff: %d | maxInstances: %d | lifespan: %d | isRunning: %b | data: %s",
id, factoryKey, queueKey, createTime, nextRunAttemptTime, runAttempt, maxAttempts, maxBackoff, maxInstances, lifespan, isRunning, serializedData);
}
}

View File

@ -1,55 +0,0 @@
package org.thoughtcrime.securesms.jobmanager.persistence;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import java.util.List;
public interface JobStorage {
@WorkerThread
void init();
@WorkerThread
void insertJobs(@NonNull List<FullSpec> fullSpecs);
@WorkerThread
@Nullable JobSpec getJobSpec(@NonNull String id);
@WorkerThread
@NonNull List<JobSpec> getAllJobSpecs();
@WorkerThread
@NonNull List<JobSpec> getPendingJobsWithNoDependenciesInCreatedOrder(long currentTime);
@WorkerThread
int getJobInstanceCount(@NonNull String factoryKey);
@WorkerThread
void updateJobRunningState(@NonNull String id, boolean isRunning);
@WorkerThread
void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime);
@WorkerThread
void updateAllJobsToBePending();
@WorkerThread
void deleteJob(@NonNull String id);
@WorkerThread
void deleteJobs(@NonNull List<String> ids);
@WorkerThread
@NonNull List<ConstraintSpec> getConstraintSpecs(@NonNull String jobId);
@WorkerThread
@NonNull List<ConstraintSpec> getAllConstraintSpecs();
@WorkerThread
@NonNull List<DependencySpec> getDependencySpecsThatDependOnJob(@NonNull String jobSpecId);
@WorkerThread
@NonNull List<DependencySpec> getAllDependencySpecs();
}

View File

@ -1,61 +0,0 @@
package org.thoughtcrime.securesms.jobs
import android.content.Context
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.JobDelegate
import org.session.libsession.messaging.utilities.Data
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.service.GenericForegroundService
import org.thoughtcrime.securesms.util.BackupUtil.createBackupFile
import org.thoughtcrime.securesms.util.BackupUtil.deleteAllBackupFiles
import network.loki.messenger.R
class LocalBackupJob:Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 0
lateinit var context: Context
companion object {
val TAG = LocalBackupJob::class.simpleName
val KEY: String = "LocalBackupJob"
}
override fun execute(dispatcherName: String) {
Log.i(TAG, "Executing backup job...")
GenericForegroundService.startForegroundTask(
context,
context.getString(R.string.LocalBackupJob_creating_backup),
NotificationChannels.BACKUPS,
R.drawable.ic_launcher_foreground
)
// TODO: Maybe create a new backup icon like ic_signal_backup?
try {
val record = createBackupFile(context)
deleteAllBackupFiles(context, listOf(record))
} finally {
GenericForegroundService.stopForegroundTask(context)
}
}
override fun serialize(): Data {
return Data.EMPTY
}
override fun getFactoryKey(): String {
return KEY
}
class Factory: Job.Factory<LocalBackupJob> {
override fun create(data: Data): LocalBackupJob {
return LocalBackupJob()
}
}
}

View File

@ -1,200 +0,0 @@
package org.thoughtcrime.securesms.jobs
import androidx.annotation.Nullable
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import com.fasterxml.jackson.annotation.JsonProperty
import network.loki.messenger.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.Request
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.JobDelegate
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.FileUtils
import org.session.libsession.utilities.TextSecurePreferences.Companion.getUpdateApkDigest
import org.session.libsession.utilities.TextSecurePreferences.Companion.getUpdateApkDownloadId
import org.session.libsession.utilities.TextSecurePreferences.Companion.setUpdateApkDigest
import org.session.libsession.utilities.TextSecurePreferences.Companion.setUpdateApkDownloadId
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.service.UpdateApkReadyListener
import java.io.FileInputStream
import java.io.IOException
import java.security.MessageDigest
class UpdateApkJob: Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 0
lateinit var context: Context
companion object {
val TAG = UpdateApkJob::class.simpleName
val KEY: String = "UpdateApkJob"
}
override fun execute(dispatcherName: String) {
if (!BuildConfig.PLAY_STORE_DISABLED) return
Log.i(TAG, "Checking for APK update...")
val client = OkHttpClient()
val request = Request.Builder().url(String.format("%s/latest.json", BuildConfig.NOPLAY_UPDATE_URL)).build()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
throw IOException("Bad response: " + response.message())
}
val updateDescriptor: UpdateDescriptor = JsonUtil.fromJson(
response.body()!!.string(),
UpdateDescriptor::class.java
)
val digest = Hex.fromStringCondensed(updateDescriptor.digest)
Log.i(
TAG,
"Got descriptor: $updateDescriptor"
)
if (updateDescriptor.versionCode > getVersionCode()) {
val downloadStatus: DownloadStatus = getDownloadStatus(updateDescriptor.url, digest)
Log.i(TAG, "Download status: " + downloadStatus.status)
if (downloadStatus.status == DownloadStatus.Status.COMPLETE) {
Log.i(TAG, "Download status complete, notifying...")
handleDownloadNotify(downloadStatus.downloadId)
} else if (downloadStatus.status == DownloadStatus.Status.MISSING) {
Log.i(TAG, "Download status missing, starting download...")
handleDownloadStart(
updateDescriptor.url,
updateDescriptor.versionName,
digest
)
}
}
}
@Throws(PackageManager.NameNotFoundException::class)
private fun getVersionCode(): Int {
val packageManager: PackageManager = context.getPackageManager()
val packageInfo: PackageInfo = packageManager.getPackageInfo(context.getPackageName(), 0)
return packageInfo.versionCode
}
private fun getDownloadStatus(uri: String, theirDigest: ByteArray): DownloadStatus {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query()
query.setFilterByStatus(DownloadManager.STATUS_PAUSED or DownloadManager.STATUS_PENDING or DownloadManager.STATUS_RUNNING or DownloadManager.STATUS_SUCCESSFUL)
val pendingDownloadId = getUpdateApkDownloadId(context)
val pendingDigest = getPendingDigest(context)
val cursor = downloadManager.query(query)
return try {
var status = DownloadStatus(DownloadStatus.Status.MISSING, -1)
while (cursor != null && cursor.moveToNext()) {
val jobStatus =
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
val jobRemoteUri =
cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_URI))
val downloadId =
cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID))
val digest = getDigestForDownloadId(downloadId)
if (jobRemoteUri != null && jobRemoteUri == uri && downloadId == pendingDownloadId) {
if (jobStatus == DownloadManager.STATUS_SUCCESSFUL && digest != null && pendingDigest != null &&
MessageDigest.isEqual(pendingDigest, theirDigest) &&
MessageDigest.isEqual(digest, theirDigest)
) {
return DownloadStatus(DownloadStatus.Status.COMPLETE, downloadId)
} else if (jobStatus != DownloadManager.STATUS_SUCCESSFUL) {
status = DownloadStatus(DownloadStatus.Status.PENDING, downloadId)
}
}
}
status
} finally {
cursor?.close()
}
}
private fun handleDownloadStart(uri: String, versionName: String, digest: ByteArray) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadRequest = DownloadManager.Request(Uri.parse(uri))
downloadRequest.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
downloadRequest.setTitle("Downloading Signal update")
downloadRequest.setDescription("Downloading Signal $versionName")
downloadRequest.setVisibleInDownloadsUi(false)
downloadRequest.setDestinationInExternalFilesDir(context, null, "signal-update.apk")
downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
val downloadId = downloadManager.enqueue(downloadRequest)
setUpdateApkDownloadId(context, downloadId)
setUpdateApkDigest(context, Hex.toStringCondensed(digest))
}
private fun handleDownloadNotify(downloadId: Long) {
val intent = Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId)
UpdateApkReadyListener().onReceive(context, intent)
}
private fun getDigestForDownloadId(downloadId: Long): ByteArray? {
return try {
val downloadManager =
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val fin = FileInputStream(downloadManager.openDownloadedFile(downloadId).fileDescriptor)
val digest = FileUtils.getFileDigest(fin)
fin.close()
digest
} catch (e: IOException) {
Log.w(TAG, e)
null
}
}
private fun getPendingDigest(context: Context): ByteArray? {
return try {
val encodedDigest = getUpdateApkDigest(context) ?: return null
Hex.fromStringCondensed(encodedDigest)
} catch (e: IOException) {
Log.w(TAG, e)
null
}
}
override fun serialize(): Data {
return Data.EMPTY
}
override fun getFactoryKey(): String {
return KEY
}
private class UpdateDescriptor(
@JsonProperty("versionCode") @Nullable val versionCode: Int,
@JsonProperty("versionName") @Nullable val versionName: String,
@JsonProperty("url") @Nullable val url: String,
@JsonProperty("sha256sum") @Nullable val digest: String)
{
override fun toString(): String {
return "[$versionCode, $versionName, $url]"
}
}
private class DownloadStatus(val status: Status, val downloadId: Long) {
enum class Status {
PENDING, COMPLETE, MISSING
}
}
class Factory: Job.Factory<UpdateApkJob> {
override fun create(data: Data): UpdateApkJob {
return UpdateApkJob()
}
}
}

View File

@ -1,41 +0,0 @@
package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import org.session.libsession.messaging.jobs.JobQueue;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.jobs.LocalBackupJob;
import org.session.libsession.utilities.TextSecurePreferences;
import java.util.concurrent.TimeUnit;
public class LocalBackupListener extends PersistentAlarmManagerListener {
private static final long INTERVAL = TimeUnit.DAYS.toMillis(1);
@Override
protected long getNextScheduledExecutionTime(Context context) {
return TextSecurePreferences.getNextBackupTime(context);
}
@Override
protected long onAlarm(Context context, long scheduledTime) {
if (TextSecurePreferences.isBackupEnabled(context)) {
LocalBackupJob job = new LocalBackupJob();
job.context = context;
JobQueue.getShared().add(job);
}
long nextTime = System.currentTimeMillis() + INTERVAL;
TextSecurePreferences.setNextBackupTime(context, nextTime);
return nextTime;
}
public static void schedule(Context context) {
if (TextSecurePreferences.isBackupEnabled(context)) {
new LocalBackupListener().onReceive(context, new Intent());
}
}
}

View File

@ -1,49 +0,0 @@
package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import org.session.libsession.messaging.jobs.JobQueue;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import network.loki.messenger.BuildConfig;
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
import org.session.libsession.utilities.TextSecurePreferences;
import java.util.concurrent.TimeUnit;
public class UpdateApkRefreshListener extends PersistentAlarmManagerListener {
private static final String TAG = UpdateApkRefreshListener.class.getSimpleName();
private static final long INTERVAL = TimeUnit.HOURS.toMillis(6);
@Override
protected long getNextScheduledExecutionTime(Context context) {
return TextSecurePreferences.getUpdateApkRefreshTime(context);
}
@Override
protected long onAlarm(Context context, long scheduledTime) {
Log.i(TAG, "onAlarm...");
if (scheduledTime != 0 && BuildConfig.PLAY_STORE_DISABLED) {
Log.i(TAG, "Queueing APK update job...");
UpdateApkJob job = new UpdateApkJob();
job.context = context;
JobQueue.getShared().add(job);
}
long newTime = System.currentTimeMillis() + INTERVAL;
TextSecurePreferences.setUpdateApkRefreshTime(context, newTime);
return newTime;
}
public static void schedule(Context context) {
new UpdateApkRefreshListener().onReceive(context, new Intent());
}
}

View File

@ -1,313 +0,0 @@
package org.thoughtcrime.securesms.util
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.widget.Toast
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import network.loki.messenger.R
import org.greenrobot.eventbus.EventBus
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.ByteUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.backup.BackupEvent
import org.thoughtcrime.securesms.backup.BackupPassphrase
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.FullBackupExporter
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.BackupFileRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import java.io.IOException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.text.SimpleDateFormat
import java.util.*
object BackupUtil {
private const val MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences"
private const val TAG = "BackupUtil"
const val BACKUP_FILE_MIME_TYPE = "application/session-backup"
const val BACKUP_PASSPHRASE_LENGTH = 30
fun getBackupRecords(context: Context): List<SharedPreference> {
val prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME
val preferences = context.getSharedPreferences(prefName, 0)
val prefList = LinkedList<SharedPreference>()
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF)
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, null))
.build())
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF)
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, null))
.build())
if (preferences.contains(IdentityKeyUtil.ED25519_PUBLIC_KEY)) {
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.ED25519_PUBLIC_KEY)
.setValue(preferences.getString(IdentityKeyUtil.ED25519_PUBLIC_KEY, null))
.build())
}
if (preferences.contains(IdentityKeyUtil.ED25519_SECRET_KEY)) {
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.ED25519_SECRET_KEY)
.setValue(preferences.getString(IdentityKeyUtil.ED25519_SECRET_KEY, null))
.build())
}
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.LOKI_SEED)
.setValue(preferences.getString(IdentityKeyUtil.LOKI_SEED, null))
.build())
return prefList
}
@JvmStatic
fun getLastBackupTimeString(context: Context, locale: Locale): String {
val timestamp = DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFileTime()
if (timestamp == null) {
return context.getString(R.string.BackupUtil_never)
}
return DateUtils.getDisplayFormattedTimeSpanString(context, locale, timestamp.time)
}
@JvmStatic
fun getLastBackup(context: Context): BackupFileRecord? {
return DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFile()
}
@JvmStatic
fun generateBackupPassphrase(): Array<String> {
val random = ByteArray(BACKUP_PASSPHRASE_LENGTH).also { SecureRandom().nextBytes(it) }
return Array(6) { i ->
String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000)
}
}
@JvmStatic
fun validateDirAccess(context: Context, dirUri: Uri): Boolean {
val hasWritePermission = context.contentResolver.persistedUriPermissions.any {
it.isWritePermission && it.uri == dirUri
}
if (!hasWritePermission) return false
val document = DocumentFile.fromTreeUri(context, dirUri)
if (document == null || !document.exists()) {
return false
}
return true
}
@JvmStatic
fun getBackupDirUri(context: Context): Uri? {
val dirUriString = TextSecurePreferences.getBackupSaveDir(context) ?: return null
return Uri.parse(dirUriString)
}
@JvmStatic
fun setBackupDirUri(context: Context, uriString: String?) {
TextSecurePreferences.setBackupSaveDir(context, uriString)
}
/**
* @return The selected backup directory if it's valid (exists, is writable).
*/
@JvmStatic
fun getSelectedBackupDirIfValid(context: Context): Uri? {
val dirUri = getBackupDirUri(context)
if (dirUri == null) {
Log.v(TAG, "The backup dir wasn't selected yet.")
return null
}
if (!validateDirAccess(context, dirUri)) {
Log.v(TAG, "Cannot validate the access to the dir $dirUri.")
return null
}
return dirUri;
}
@JvmStatic
@WorkerThread
@Throws(IOException::class)
fun createBackupFile(context: Context): BackupFileRecord {
val backupPassword = BackupPassphrase.get(context)
?: throw IOException("Backup password is null")
val dirUri = getSelectedBackupDirIfValid(context)
?: throw IOException("Backup save directory is not selected or invalid")
val date = Date()
val timestamp = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(date)
val fileName = String.format("session-%s.backup", timestamp)
val fileUri = DocumentsContract.createDocument(
context.contentResolver,
DocumentFile.fromTreeUri(context, dirUri)!!.uri,
BACKUP_FILE_MIME_TYPE,
fileName)
if (fileUri == null) {
Toast.makeText(context, "Cannot create writable file in the dir $dirUri", Toast.LENGTH_LONG).show()
throw IOException("Cannot create writable file in the dir $dirUri")
}
try {
FullBackupExporter.export(context,
AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret,
DatabaseComponent.get(context).openHelper().readableDatabase,
fileUri,
backupPassword)
} catch (e: Exception) {
// Delete the backup file on any error.
DocumentsContract.deleteDocument(context.contentResolver, fileUri)
throw e
}
//TODO Use real file size.
val record = DatabaseComponent.get(context).lokiBackupFilesDatabase()
.insertBackupFile(BackupFileRecord(fileUri, -1, date))
Log.v(TAG, "A backup file was created: $fileUri")
return record
}
@JvmStatic
@JvmOverloads
fun deleteAllBackupFiles(context: Context, except: Collection<BackupFileRecord>? = null) {
val db = DatabaseComponent.get(context).lokiBackupFilesDatabase()
db.getBackupFiles().iterator().forEach { record ->
if (except != null && except.contains(record)) return@forEach
// Try to delete the related file. The operation may fail in many cases
// (the user moved/deleted the file, revoked the write permission, etc), so that's OK.
try {
val result = DocumentsContract.deleteDocument(context.contentResolver, record.uri)
if (!result) {
Log.w(TAG, "Failed to delete backup file: ${record.uri}")
}
} catch (e: Exception) {
Log.w(TAG, "Failed to delete backup file: ${record.uri}", e)
}
db.deleteBackupFile(record)
Log.v(TAG, "Backup file was deleted: ${record.uri}")
}
}
@JvmStatic
fun computeBackupKey(passphrase: String, salt: ByteArray?): ByteArray {
return try {
EventBus.getDefault().post(BackupEvent.createProgress(0))
val digest = MessageDigest.getInstance("SHA-512")
val input = passphrase.replace(" ", "").toByteArray()
var hash: ByteArray = input
if (salt != null) digest.update(salt)
for (i in 0..249999) {
if (i % 1000 == 0) EventBus.getDefault().post(BackupEvent.createProgress(0))
digest.update(hash)
hash = digest.digest(input)
}
ByteUtil.trim(hash, 32)
} catch (e: NoSuchAlgorithmException) {
throw AssertionError(e)
}
}
}
/**
* An utility class to help perform backup directory selection requests.
*
* An instance of this class should be created per an [Activity] or [Fragment]
* and [onActivityResult] should be called appropriately.
*/
class BackupDirSelector(private val contextProvider: ContextProvider) {
companion object {
private const val REQUEST_CODE_SAVE_DIR = 7844
}
private val context: Context get() = contextProvider.getContext()
private var listener: Listener? = null
constructor(activity: Activity) :
this(ActivityContextProvider(activity))
constructor(fragment: Fragment) :
this(FragmentContextProvider(fragment))
/**
* Performs ACTION_OPEN_DOCUMENT_TREE intent to select backup directory URI.
* If the directory is already selected and valid, the request will be skipped.
* @param force if true, the previous selection is ignored and the user is requested to select another directory.
* @param onSelectedListener an optional action to perform once the directory is selected.
*/
fun selectBackupDir(force: Boolean, onSelectedListener: Listener? = null) {
if (!force) {
val dirUri = BackupUtil.getSelectedBackupDirIfValid(context)
if (dirUri != null && onSelectedListener != null) {
onSelectedListener.onBackupDirSelected(dirUri)
}
return
}
// Let user pick the dir.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
// Request read/write permission grant for the dir.
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
// Set the default dir.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val dirUri = BackupUtil.getBackupDirUri(context)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, dirUri
?: Uri.fromFile(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)))
}
if (onSelectedListener != null) {
this.listener = onSelectedListener
}
contextProvider.startActivityForResult(intent, REQUEST_CODE_SAVE_DIR)
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_CODE_SAVE_DIR) return
if (resultCode == Activity.RESULT_OK && data != null && data.data != null) {
// Acquire persistent access permissions for the file selected.
val persistentFlags: Int = data.flags and
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
context.contentResolver.takePersistableUriPermission(data.data!!, persistentFlags)
BackupUtil.setBackupDirUri(context, data.dataString)
listener?.onBackupDirSelected(data.data!!)
}
listener = null
}
@FunctionalInterface
interface Listener {
fun onBackupDirSelected(uri: Uri)
}
}