Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import/Export encryption and progress notification #317

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Bundle
import android.provider.Telephony
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.documentfile.provider.DocumentFile
import com.simplemobiletools.commons.dialogs.FilePickerDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
Expand All @@ -24,20 +26,19 @@ import com.simplemobiletools.smsmessenger.adapters.ConversationsAdapter
import com.simplemobiletools.smsmessenger.dialogs.ExportMessagesDialog
import com.simplemobiletools.smsmessenger.dialogs.ImportMessagesDialog
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.EXPORT_MIME_TYPE
import com.simplemobiletools.smsmessenger.helpers.MessagesExporter
import com.simplemobiletools.smsmessenger.helpers.THREAD_ID
import com.simplemobiletools.smsmessenger.helpers.THREAD_TITLE
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.Conversation
import com.simplemobiletools.smsmessenger.models.Events
import kotlinx.android.synthetic.main.activity_main.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.BufferedInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.*
import kotlin.collections.ArrayList


class MainActivity : SimpleActivity() {
private val MAKE_DEFAULT_APP_REQUEST = 1
Expand Down Expand Up @@ -367,7 +368,7 @@ class MainActivity : SimpleActivity() {
if (isQPlus()) {
ExportMessagesDialog(this, config.lastExportPath, true) { file ->
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = EXPORT_MIME_TYPE
type = if (config.exportBackupPassword != "") EXPORT_SECURE_MIME_TYPE else EXPORT_MIME_TYPE
putExtra(Intent.EXTRA_TITLE, file.name)
addCategory(Intent.CATEGORY_OPENABLE)
startActivityForResult(this, PICK_EXPORT_FILE_INTENT)
Expand Down Expand Up @@ -404,7 +405,7 @@ class MainActivity : SimpleActivity() {
if (isQPlus()) {
Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = EXPORT_MIME_TYPE
type = "*/*"
startActivityForResult(this, PICK_IMPORT_SOURCE_INTENT)
}
} else {
Expand All @@ -427,6 +428,7 @@ class MainActivity : SimpleActivity() {
}

private fun tryImportMessagesFromFile(uri: Uri) {
Log.d("uri", uri.scheme.toString())
when (uri.scheme) {
"file" -> showImportEventsDialog(uri.path!!)
"content" -> {
Expand All @@ -435,7 +437,6 @@ class MainActivity : SimpleActivity() {
toast(R.string.unknown_error_occurred)
return
}

try {
val inputStream = contentResolver.openInputStream(uri)
val out = FileOutputStream(tempFile)
Expand All @@ -444,11 +445,13 @@ class MainActivity : SimpleActivity() {
} catch (e: Exception) {
showErrorToast(e)
}

}
else -> toast(R.string.invalid_file_format)
}
}


@Subscribe(threadMode = ThreadMode.MAIN)
fun refreshMessages(event: Events.RefreshMessages) {
initMessenger()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.helpers.EXPORT_FILE_EXT
import com.simplemobiletools.smsmessenger.helpers.EXPORT_SECURE_FILE_EXT
import kotlinx.android.synthetic.main.dialog_export_messages.view.*
import java.io.File

Expand Down Expand Up @@ -51,7 +52,8 @@ class ExportMessagesDialog(
when {
filename.isEmpty() -> activity.toast(R.string.empty_name)
filename.isAValidFilename() -> {
val file = File(realPath, "$filename$EXPORT_FILE_EXT")
config.exportBackupPassword = view.export_messages_password.value //We need to get this early to set proper extension
val file = if (config.exportBackupPassword == "") File(realPath, "$filename$EXPORT_FILE_EXT") else File(realPath, "$filename$EXPORT_SECURE_FILE_EXT")
if (!hidePath && file.exists()) {
activity.toast(R.string.name_taken)
return@setOnClickListener
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.simplemobiletools.smsmessenger.dialogs


import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.extensions.value
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
Expand All @@ -12,6 +14,7 @@ import com.simplemobiletools.smsmessenger.helpers.MessagesImporter
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_OK
import com.simplemobiletools.smsmessenger.helpers.MessagesImporter.ImportResult.IMPORT_PARTIAL
import kotlinx.android.synthetic.main.dialog_import_messages.view.*
import java.io.InputStream

class ImportMessagesDialog(
private val activity: SimpleActivity,
Expand Down Expand Up @@ -46,6 +49,7 @@ class ImportMessagesDialog(
activity.toast(R.string.importing)
config.importSms = view.import_sms_checkbox.isChecked
config.importMms = view.import_mms_checkbox.isChecked
config.importBackupPassword = view.import_messages_password.value
ensureBackgroundThread {
MessagesImporter(activity).importMessages(path) {
handleParseResult(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class Config(context: Context) : BaseConfig(context) {
get() = prefs.getString(LAST_EXPORT_PATH, "")!!
set(lastExportPath) = prefs.edit().putString(LAST_EXPORT_PATH, lastExportPath).apply()

var exportBackupPassword: String
get() = prefs.getString(EXPORT_BACKUP_PASSWORD, "")!!
set(exportBackupPassword) = prefs.edit().putString(EXPORT_BACKUP_PASSWORD, exportBackupPassword).apply()

var exportSms: Boolean
get() = prefs.getBoolean(EXPORT_SMS, true)
set(exportSms) = prefs.edit().putBoolean(EXPORT_SMS, exportSms).apply()
Expand All @@ -72,6 +76,10 @@ class Config(context: Context) : BaseConfig(context) {
get() = prefs.getBoolean(EXPORT_MMS, true)
set(exportMms) = prefs.edit().putBoolean(EXPORT_MMS, exportMms).apply()

var importBackupPassword: String
get() = prefs.getString(IMPORT_BACKUP_PASSWORD, "")!!
set(importBackupPassword) = prefs.edit().putString(IMPORT_BACKUP_PASSWORD, importBackupPassword).apply()

var importSms: Boolean
get() = prefs.getBoolean(IMPORT_SMS, true)
set(importSms) = prefs.edit().putBoolean(IMPORT_SMS, importSms).apply()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,22 @@ const val SEND_LONG_MESSAGE_MMS = "send_long_message_mms"
const val MMS_FILE_SIZE_LIMIT = "mms_file_size_limit"
const val PINNED_CONVERSATIONS = "pinned_conversations"
const val LAST_EXPORT_PATH = "last_export_path"
const val EXPORT_BACKUP_PASSWORD = "export_backup_password"
const val EXPORT_SMS = "export_sms"
const val EXPORT_MMS = "export_mms"
const val EXPORT_MIME_TYPE = "application/json"
const val EXPORT_SECURE_MIME_TYPE = "application/sec"
const val EXPORT_FILE_EXT = ".json"
const val EXPORT_SECURE_FILE_EXT = ".sec"
const val IMPORT_BACKUP_PASSWORD = "import_backup_password"
const val IMPORT_SMS = "import_sms"
const val IMPORT_MMS = "import_mms"
const val WAS_DB_CLEARED = "was_db_cleared"

//Secure Backup Cipher Parameters
const val KEY_ITERATIONS = 65536
const val KEY_LENGTH = 256

private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read"
const val REPLY = PATH + "reply"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package com.simplemobiletools.smsmessenger.helpers

import android.content.Context
import android.util.Log
import com.google.gson.Gson
import com.google.gson.stream.JsonWriter
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.smsmessenger.extensions.config
import com.simplemobiletools.smsmessenger.extensions.getConversationIds
import org.json.JSONArray
import org.json.JSONObject
import java.io.OutputStream
import java.security.SecureRandom
import java.security.spec.KeySpec
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec


class MessagesExporter(private val context: Context) {
enum class ExportResult {
Expand All @@ -23,45 +33,77 @@ class MessagesExporter(private val context: Context) {
callback.invoke(ExportResult.EXPORT_FAIL)
return@ensureBackgroundThread
}
val writer = JsonWriter(outputStream.bufferedWriter())
writer.use {
try {
var written = 0
writer.beginArray()
val conversationIds = context.getConversationIds()
val totalMessages = messageReader.getMessagesCount()
for (threadId in conversationIds) {
writer.beginObject()
if (config.exportSms && messageReader.getSmsCount() > 0) {
writer.name("sms")
writer.beginArray()
messageReader.forEachSms(threadId) {
writer.jsonValue(gson.toJson(it))
written++
onProgress.invoke(totalMessages, written)
}
writer.endArray()
}

if (config.exportMms && messageReader.getMmsCount() > 0) {
writer.name("mms")
writer.beginArray()
messageReader.forEachMms(threadId) {
writer.jsonValue(gson.toJson(it))
written++
onProgress.invoke(totalMessages, written)
}
//To keep same layout as previous version and avoid rewriting importer
val mainArray = JSONArray()

//Main object containing Array of object jSMS & jMMS
val mainObject = JSONObject()

val jSMS = JSONArray()
val jMMS = JSONArray()

try {
var written = 0
val conversationIds = context.getConversationIds()
val totalMessages = messageReader.getMessagesCount()
for (threadId in conversationIds) {

if (config.exportSms && messageReader.getSmsCount() > 0) {
messageReader.forEachSms(threadId) {
jSMS.put(JSONObject(gson.toJson(it)))
written++
onProgress.invoke(totalMessages, written)
}
}

writer.endArray()
if (config.exportMms && messageReader.getMmsCount() > 0) {
messageReader.forEachMms(threadId) {
jMMS.put(JSONObject(gson.toJson(it)))
written++
onProgress.invoke(totalMessages, written)
}
}
}
if (jSMS.length() > 0) mainObject.put("sms", jSMS)
if (jMMS.length() > 0) mainObject.put("mms", jMMS)

mainArray.put(mainObject)
Log.d("debugFilePath", config.lastExportPath)
if (config.exportBackupPassword == "")
{
//If user didn't set a password we simply save the json in plain
outputStream.write(mainArray.toString().encodeToByteArray())
}
else
{
Log.d("debugFilePath", config.lastExportPath)
val salt = ByteArray(16)
val random = SecureRandom()
random.nextBytes(salt)

writer.endObject()
//Taken from FairEmail project (setting export mechanism)
// https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
val keyFactory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val keySpec: KeySpec = PBEKeySpec(config.exportBackupPassword.toCharArray(), salt, KEY_ITERATIONS, KEY_LENGTH)
val secret: SecretKey = keyFactory.generateSecret(keySpec)
val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secret)

outputStream.write(salt)
outputStream.write(cipher.iv)

val cout: OutputStream = CipherOutputStream(outputStream, cipher)
cout.bufferedWriter().use { writer ->
cout.write(mainArray.toString().toByteArray())
}
writer.endArray()
callback.invoke(ExportResult.EXPORT_OK)
} catch (e: Exception) {
callback.invoke(ExportResult.EXPORT_FAIL)
cout.flush()
outputStream.write(cipher.doFinal())

}
callback.invoke(ExportResult.EXPORT_OK)
} catch (e: Exception) {
callback.invoke(ExportResult.EXPORT_FAIL)
}
}
}
Expand Down
Loading