commit
429b496a22
@ -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)
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|