Skip to content

Commit ac520d3

Browse files
authored
Merge pull request #698 from PhilKes/fix/decrypt-encrypt-errors
Improve de-/encrypting db error handling
2 parents 1384740 + d0e8ac2 commit ac520d3

23 files changed

Lines changed: 228 additions & 78 deletions

File tree

app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.activity.result.contract.ActivityResultContracts
1515
import androidx.activity.viewModels
1616
import androidx.appcompat.app.AppCompatActivity
1717
import androidx.core.content.ContextCompat
18+
import androidx.lifecycle.lifecycleScope
1819
import androidx.viewbinding.ViewBinding
1920
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2021
import com.philkes.notallyx.NotallyXApplication
@@ -23,6 +24,7 @@ import com.philkes.notallyx.presentation.showToast
2324
import com.philkes.notallyx.presentation.viewmodel.BaseNoteModel
2425
import com.philkes.notallyx.presentation.viewmodel.preference.NotallyXPreferences
2526
import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt
27+
import kotlinx.coroutines.launch
2628

2729
abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
2830

@@ -82,9 +84,12 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
8284
.setMessage(R.string.unlock_with_biometrics_not_setup)
8385
.setPositiveButton(R.string.disable) { _, _ ->
8486
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
85-
baseModel.disableBiometricLock()
87+
lifecycleScope.launch {
88+
baseModel.disableBiometricLock()
89+
showToast(R.string.biometrics_disable_success)
90+
}
8691
}
87-
show()
92+
hide()
8893
}
8994
.setNegativeButton(R.string.tap_to_set_up) { _, _ ->
9095
val intent =
@@ -102,8 +107,10 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
102107

103108
BIOMETRIC_ERROR_HW_NOT_PRESENT -> {
104109
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
105-
baseModel.disableBiometricLock()
106-
showToast(R.string.biometrics_disable_success)
110+
lifecycleScope.launch {
111+
baseModel.disableBiometricLock()
112+
showToast(R.string.biometrics_disable_success)
113+
}
107114
}
108115
show()
109116
}

app/src/main/java/com/philkes/notallyx/presentation/activity/main/fragment/settings/SettingsFragment.kt

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import androidx.core.net.toUri
2323
import androidx.core.view.isVisible
2424
import androidx.fragment.app.Fragment
2525
import androidx.fragment.app.activityViewModels
26+
import androidx.lifecycle.lifecycleScope
2627
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2728
import com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE
2829
import com.philkes.notallyx.NotallyXApplication
@@ -58,11 +59,16 @@ import com.philkes.notallyx.utils.getExtraBooleanFromBundleOrIntent
5859
import com.philkes.notallyx.utils.getLastExceptionLog
5960
import com.philkes.notallyx.utils.getLogFile
6061
import com.philkes.notallyx.utils.getUriForFile
62+
import com.philkes.notallyx.utils.log
6163
import com.philkes.notallyx.utils.reportBug
64+
import com.philkes.notallyx.utils.security.DecryptionException
65+
import com.philkes.notallyx.utils.security.EncryptionException
6266
import com.philkes.notallyx.utils.security.showBiometricOrPinPrompt
67+
import com.philkes.notallyx.utils.showErrorDialog
6368
import com.philkes.notallyx.utils.viewLogs
6469
import com.philkes.notallyx.utils.wrapWithChooser
6570
import java.util.Date
71+
import kotlinx.coroutines.launch
6672

6773
class SettingsFragment : Fragment() {
6874

@@ -663,7 +669,11 @@ class SettingsFragment : Fragment() {
663669
R.string.reset_settings_message,
664670
R.string.reset_settings,
665671
{ _, _ ->
666-
model.resetPreferences { _ -> showToast(R.string.reset_settings_success) }
672+
lifecycleScope.launch {
673+
model.resetPreferences { _ ->
674+
showToast(R.string.reset_settings_success)
675+
}
676+
}
667677
},
668678
)
669679
}
@@ -839,12 +849,27 @@ class SettingsFragment : Fragment() {
839849
R.string.enable_lock_title,
840850
R.string.enable_lock_description,
841851
onSuccess = { cipher ->
852+
val app = (requireActivity().application as NotallyXApplication)
842853
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
843-
model.enableBiometricLock(cipher)
854+
lifecycleScope.launch {
855+
try {
856+
model.enableBiometricLock(cipher)
857+
} catch (e: EncryptionException) {
858+
app.log(TAG, throwable = e)
859+
showErrorDialog(
860+
e,
861+
R.string.biometrics_setup_failure,
862+
getString(
863+
R.string.biometrics_setup_failure_encrypt,
864+
getString(R.string.report_bug),
865+
),
866+
)
867+
return@launch
868+
}
869+
app.locked.value = false
870+
showToast(R.string.biometrics_setup_success)
871+
}
844872
}
845-
val app = (activity?.application as NotallyXApplication)
846-
app.locked.value = false
847-
showToast(R.string.biometrics_setup_success)
848873
},
849874
) {
850875
showBiometricsNotSetupDialog()
@@ -860,9 +885,25 @@ class SettingsFragment : Fragment() {
860885
model.preferences.iv.value!!,
861886
onSuccess = { cipher ->
862887
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
863-
model.disableBiometricLock(cipher)
888+
val app = (requireActivity().application as NotallyXApplication)
889+
lifecycleScope.launch {
890+
try {
891+
model.disableBiometricLock(cipher)
892+
} catch (e: DecryptionException) {
893+
app.log(TAG, throwable = e)
894+
showErrorDialog(
895+
e,
896+
R.string.biometrics_setup_failure,
897+
getString(
898+
R.string.biometrics_setup_failure_decrypt,
899+
getString(R.string.report_bug),
900+
),
901+
)
902+
return@launch
903+
}
904+
showToast(R.string.biometrics_disable_success)
905+
}
864906
}
865-
showToast(R.string.biometrics_disable_success)
866907
},
867908
) {}
868909
}
@@ -906,6 +947,7 @@ class SettingsFragment : Fragment() {
906947
}
907948

908949
companion object {
950+
private const val TAG = "SettingsFragment"
909951
const val EXTRA_SHOW_IMPORT_BACKUPS_FOLDER =
910952
"notallyx.intent.extra.SHOW_IMPORT_BACKUPS_FOLDER"
911953
}

app/src/main/java/com/philkes/notallyx/presentation/viewmodel/BaseNoteModel.kt

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import androidx.room.withTransaction
1919
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2020
import com.philkes.notallyx.R
2121
import com.philkes.notallyx.data.NotallyDatabase
22+
import com.philkes.notallyx.data.NotallyDatabase.Companion.DATABASE_NAME
2223
import com.philkes.notallyx.data.dao.BaseNoteDao
2324
import com.philkes.notallyx.data.dao.CommonDao
2425
import com.philkes.notallyx.data.dao.LabelDao
@@ -60,6 +61,7 @@ import com.philkes.notallyx.utils.Cache
6061
import com.philkes.notallyx.utils.MIME_TYPE_JSON
6162
import com.philkes.notallyx.utils.backup.clearAllFolders
6263
import com.philkes.notallyx.utils.backup.clearAllLabels
64+
import com.philkes.notallyx.utils.backup.copyDatabase
6365
import com.philkes.notallyx.utils.backup.exportAsZip
6466
import com.philkes.notallyx.utils.backup.exportPdfFile
6567
import com.philkes.notallyx.utils.backup.exportPlainTextFile
@@ -71,10 +73,15 @@ import com.philkes.notallyx.utils.cancelNoteReminders
7173
import com.philkes.notallyx.utils.deleteAttachments
7274
import com.philkes.notallyx.utils.getBackupDir
7375
import com.philkes.notallyx.utils.getExternalImagesDirectory
76+
import com.philkes.notallyx.utils.getExternalMediaDirectory
7477
import com.philkes.notallyx.utils.log
7578
import com.philkes.notallyx.utils.scheduleNoteReminders
79+
import com.philkes.notallyx.utils.security.DecryptionException
80+
import com.philkes.notallyx.utils.security.EncryptionException
7681
import com.philkes.notallyx.utils.security.decryptDatabase
7782
import com.philkes.notallyx.utils.security.encryptDatabase
83+
import com.philkes.notallyx.utils.security.isEncryptedDatabase
84+
import com.philkes.notallyx.utils.security.isUnencryptedDatabase
7885
import com.philkes.notallyx.utils.toReadablePath
7986
import java.io.File
8087
import java.util.concurrent.atomic.AtomicInteger
@@ -283,24 +290,55 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
283290
}
284291
}
285292

286-
fun enableBiometricLock(cipher: Cipher) {
293+
suspend fun enableBiometricLock(cipher: Cipher) {
287294
savePreference(preferences.iv, cipher.iv)
288295
val passphrase = preferences.databaseEncryptionKey.init(cipher)
289-
encryptDatabase(app, passphrase)
290-
savePreference(preferences.fallbackDatabaseEncryptionKey, passphrase)
291-
savePreference(preferences.biometricLock, BiometricLock.ENABLED)
296+
withContext(Dispatchers.IO) {
297+
database.close()
298+
val (_, dbFileCopy) = app.copyDatabase(suffix = "-encrypt")
299+
val (_, dbFileBackup) = app.copyDatabase(suffix = "-encrypt-backup")
300+
encryptDatabase(app, dbFileCopy, passphrase)
301+
val originalDbFile = NotallyDatabase.getCurrentDatabaseFile(app)
302+
dbFileCopy.copyTo(originalDbFile, overwrite = true)
303+
if (originalDbFile.isUnencryptedDatabase) {
304+
dbFileBackup.copyTo(originalDbFile, overwrite = true)
305+
val externalBackupFile =
306+
File(app.getExternalMediaDirectory(), "${DATABASE_NAME}_Backup-encrypt")
307+
dbFileBackup.copyTo(externalBackupFile, overwrite = true)
308+
throw EncryptionException(
309+
"Encrypt succeeded but overwritten database is not encrypted"
310+
)
311+
}
312+
savePreference(preferences.fallbackDatabaseEncryptionKey, passphrase)
313+
savePreference(preferences.biometricLock, BiometricLock.ENABLED)
314+
}
292315
}
293316

294317
@RequiresApi(Build.VERSION_CODES.M)
295-
fun disableBiometricLock(cipher: Cipher? = null, callback: (() -> Unit)? = null) {
318+
suspend fun disableBiometricLock(cipher: Cipher? = null, callback: (() -> Unit)? = null) {
296319
val encryptedPassphrase = preferences.databaseEncryptionKey.value
297320
val passphrase =
298321
cipher?.doFinal(encryptedPassphrase)
299322
?: preferences.fallbackDatabaseEncryptionKey.value!!
300-
database.close()
301-
decryptDatabase(app, passphrase)
302-
savePreference(preferences.biometricLock, BiometricLock.DISABLED)
303-
callback?.invoke()
323+
withContext(Dispatchers.IO) {
324+
database.close()
325+
val (_, dbFileCopy) = app.copyDatabase(decrypt = false, suffix = "-decrypt")
326+
val (_, dbFileBackup) = app.copyDatabase(decrypt = false, suffix = "-decrypt-backup")
327+
decryptDatabase(app, dbFileCopy, passphrase)
328+
val originalDbFile = NotallyDatabase.getCurrentDatabaseFile(app)
329+
dbFileCopy.copyTo(originalDbFile, overwrite = true)
330+
if (originalDbFile.isEncryptedDatabase) {
331+
dbFileBackup.copyTo(originalDbFile, overwrite = true)
332+
val externalBackupFile =
333+
File(app.getExternalMediaDirectory(), "${DATABASE_NAME}_Backup-decrypt")
334+
dbFileBackup.copyTo(externalBackupFile, overwrite = true)
335+
throw DecryptionException(
336+
"Decrypt succeeded but overwritten database is still encrypted"
337+
)
338+
}
339+
savePreference(preferences.biometricLock, BiometricLock.DISABLED)
340+
callback?.invoke()
341+
}
304342
}
305343

306344
fun <T> savePreference(preference: BasePreference<T>, value: T) {
@@ -605,7 +643,7 @@ class BaseNoteModel(private val app: Application) : AndroidViewModel(app) {
605643
}
606644
}
607645

608-
fun resetPreferences(callback: (restartRequired: Boolean) -> Unit) {
646+
suspend fun resetPreferences(callback: (restartRequired: Boolean) -> Unit) {
609647
val backupsFolder = preferences.backupsFolder.value
610648
val publicFolder = preferences.dataInPublicFolder.value
611649
val isThemeDefault = preferences.theme.value == Theme.FOLLOW_SYSTEM

app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.philkes.notallyx.R
3535
import com.philkes.notallyx.data.model.BaseNote
3636
import com.philkes.notallyx.data.model.Type
3737
import com.philkes.notallyx.data.model.toText
38+
import com.philkes.notallyx.databinding.DialogErrorBinding
3839
import com.philkes.notallyx.presentation.activity.note.EditActivity.Companion.EXTRA_SELECTED_BASE_NOTE
3940
import com.philkes.notallyx.presentation.activity.note.EditListActivity
4041
import com.philkes.notallyx.presentation.activity.note.EditNoteActivity
@@ -182,6 +183,54 @@ fun ContextWrapper.viewLogs() {
182183
fun ContextWrapper.getLogFileUri() =
183184
getLogFile().let { if (it.exists()) getUriForFile(it) else null }
184185

186+
fun Fragment.showErrorDialog(
187+
throwable: Throwable,
188+
titleResId: Int,
189+
message: String,
190+
originalStacktrace: String? = null,
191+
) {
192+
val stacktrace = throwable.stackTraceToString()
193+
val layout =
194+
DialogErrorBinding.inflate(layoutInflater, null, false).apply {
195+
ExceptionTitle.text = message
196+
ExceptionDetails.text = stacktrace
197+
CopyButton.setOnClickListener { requireContext().copyToClipBoard(stacktrace) }
198+
}
199+
MaterialAlertDialogBuilder(requireContext())
200+
.setTitle(titleResId)
201+
.setView(layout.root)
202+
.setPositiveButton(R.string.report_bug) { dialog, _ ->
203+
dialog.cancel()
204+
reportBug(originalStacktrace ?: throwable.stackTraceToString())
205+
}
206+
.setCancelButton()
207+
.show()
208+
}
209+
210+
fun Activity.showErrorDialog(
211+
throwable: Throwable,
212+
titleResId: Int,
213+
message: String,
214+
originalStacktrace: String? = null,
215+
) {
216+
val stacktrace = throwable.stackTraceToString()
217+
val layout =
218+
DialogErrorBinding.inflate(layoutInflater, null, false).apply {
219+
ExceptionTitle.text = message
220+
ExceptionDetails.text = stacktrace
221+
CopyButton.setOnClickListener { copyToClipBoard(stacktrace) }
222+
}
223+
MaterialAlertDialogBuilder(this)
224+
.setTitle(titleResId)
225+
.setView(layout.root)
226+
.setPositiveButton(R.string.report_bug) { dialog, _ ->
227+
dialog.cancel()
228+
reportBug(originalStacktrace ?: throwable.stackTraceToString())
229+
}
230+
.setCancelButton()
231+
.show()
232+
}
233+
185234
private const val MAX_LOGS_FILE_SIZE_KB: Long = 2048
186235

187236
private fun Context.logToFile(

app/src/main/java/com/philkes/notallyx/utils/ErrorActivity.kt

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import androidx.lifecycle.lifecycleScope
1010
import cat.ereza.customactivityoncrash.CustomActivityOnCrash
1111
import com.google.android.material.dialog.MaterialAlertDialogBuilder
1212
import com.philkes.notallyx.R
13+
import com.philkes.notallyx.R.string.auto_backup_failed
14+
import com.philkes.notallyx.R.string.crash_export_backup_failed
15+
import com.philkes.notallyx.R.string.report_bug
1316
import com.philkes.notallyx.databinding.ActivityErrorBinding
14-
import com.philkes.notallyx.databinding.DialogErrorBinding
1517
import com.philkes.notallyx.presentation.getQuantityString
1618
import com.philkes.notallyx.presentation.setCancelButton
1719
import com.philkes.notallyx.presentation.setupProgressDialog
@@ -92,13 +94,12 @@ class ErrorActivity : AppCompatActivity() {
9294
result.data?.data?.let { uri ->
9395
val preferences = NotallyXPreferences.getInstance(this)
9496
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
95-
// MaterialAlertDialogBuilder(this)
96-
// .setTitle(R.string.auto_backup_failed)
97-
//
98-
// .setMessage(throwable.stackTraceToString())
99-
// .setCancelButton()
100-
// .show()
101-
showErrorDialog(throwable, stacktrace)
97+
showErrorDialog(
98+
throwable,
99+
auto_backup_failed,
100+
getString(crash_export_backup_failed, this.getString(report_bug)),
101+
originalStacktrace = stacktrace,
102+
)
102103
}
103104
lifecycleScope.launch(exceptionHandler) {
104105
val exportedNotes =
@@ -123,26 +124,6 @@ class ErrorActivity : AppCompatActivity() {
123124
exportBackupProgress.setupProgressDialog(this)
124125
}
125126

126-
private fun showErrorDialog(throwable: Throwable, originalStacktrace: String?) {
127-
val stacktrace = throwable.stackTraceToString()
128-
val layout =
129-
DialogErrorBinding.inflate(layoutInflater, null, false).apply {
130-
ExceptionTitle.text =
131-
getString(R.string.crash_export_backup_failed, getString(R.string.report_bug))
132-
ExceptionDetails.text = stacktrace
133-
CopyButton.setOnClickListener { copyToClipBoard(stacktrace) }
134-
}
135-
MaterialAlertDialogBuilder(this)
136-
.setTitle(R.string.auto_backup_failed)
137-
.setView(layout.root)
138-
.setPositiveButton(R.string.report_bug) { dialog, _ ->
139-
dialog.cancel()
140-
reportBug(originalStacktrace)
141-
}
142-
.setCancelButton()
143-
.show()
144-
}
145-
146127
companion object {
147128
private const val TAG = "ErrorActivity"
148129
}

0 commit comments

Comments
 (0)