Skip to content

Commit 2b6ce0a

Browse files
committed
ADD backup restore validation if file extension and encryption state matches #37
1 parent eb16161 commit 2b6ce0a

File tree

3 files changed

+103
-41
lines changed

3 files changed

+103
-41
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ The following options are optional and the default options
175175
- Set your custom name to the Backup files
176176

177177
**Attention**\
178-
If a backup file with the same name already exists, it will be replaced
178+
If a backup file with the same name already exists, it will be replaced
179+
customBackupFileName should not contain file extension. File extension(s) will be added automatically
180+
".sqlite3" is the default file extension and for encrypted backups ".aes" will be added
179181

180182
```kotlin
181183
.customBackupFileName(*DatabaseName* + *currentTime* + ".sqlite3")

app/src/main/java/de/raphaelebner/roomdatabasebackup/sample/MainActivity.kt

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import de.raphaelebner.roomdatabasebackup.sample.database.table.fruit.Fruit
2828
import de.raphaelebner.roomdatabasebackup.sample.database.table.fruit.FruitListAdapter
2929
import de.raphaelebner.roomdatabasebackup.sample.database.table.fruit.FruitViewModel
3030
import java.io.File
31+
import androidx.core.content.edit
3132

3233
/**
3334
* MIT License
@@ -135,20 +136,20 @@ class MainActivity : AppCompatActivity(), FruitListAdapter.OnItemClickListener {
135136
0 -> {
136137
encryptBackup = checked
137138
sharedPreferences
138-
.edit()
139-
.putBoolean(spEncryptBackup, encryptBackup)
140-
.apply()
139+
.edit {
140+
putBoolean(spEncryptBackup, encryptBackup)
141+
}
141142
}
142143
1 -> {
143144
enableLog = checked
144-
sharedPreferences.edit().putBoolean(spEnableLog, enableLog).apply()
145+
sharedPreferences.edit { putBoolean(spEnableLog, enableLog) }
145146
}
146147
2 -> {
147148
useMaxFileCount = checked
148149
sharedPreferences
149-
.edit()
150-
.putBoolean(spUseMaxFileCount, useMaxFileCount)
151-
.apply()
150+
.edit {
151+
putBoolean(spUseMaxFileCount, useMaxFileCount)
152+
}
152153
}
153154
}
154155
}
@@ -166,30 +167,30 @@ class MainActivity : AppCompatActivity(), FruitListAdapter.OnItemClickListener {
166167
0 -> {
167168
storageLocation = RoomBackup.BACKUP_FILE_LOCATION_INTERNAL
168169
sharedPreferences
169-
.edit()
170-
.putInt(spStorageLocation, storageLocation)
171-
.apply()
170+
.edit {
171+
putInt(spStorageLocation, storageLocation)
172+
}
172173
}
173174
1 -> {
174175
storageLocation = RoomBackup.BACKUP_FILE_LOCATION_EXTERNAL
175176
sharedPreferences
176-
.edit()
177-
.putInt(spStorageLocation, storageLocation)
178-
.apply()
177+
.edit {
178+
putInt(spStorageLocation, storageLocation)
179+
}
179180
}
180181
2 -> {
181182
storageLocation = RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG
182183
sharedPreferences
183-
.edit()
184-
.putInt(spStorageLocation, storageLocation)
185-
.apply()
184+
.edit {
185+
putInt(spStorageLocation, storageLocation)
186+
}
186187
}
187188
3 -> {
188189
storageLocation = RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_FILE
189190
sharedPreferences
190-
.edit()
191-
.putInt(spStorageLocation, storageLocation)
192-
.apply()
191+
.edit {
192+
putInt(spStorageLocation, storageLocation)
193+
}
193194
}
194195
}
195196
}

core/src/main/java/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import android.app.Activity
44
import android.content.Context
55
import android.content.Intent
66
import android.content.SharedPreferences
7+
import android.net.Uri
78
import android.os.Build
9+
import android.provider.OpenableColumns
810
import android.util.Log
911
import android.widget.Toast
1012
import androidx.activity.ComponentActivity
@@ -63,6 +65,8 @@ class RoomBackup(var context: Context) {
6365
private var currentProcess: Int? = null
6466
private const val PROCESS_BACKUP = 1
6567
private const val PROCESS_RESTORE = 2
68+
private const val BACKUP_EXTENSION_BASE = "sqlite3"
69+
private const val BACKUP_EXTENSION_ENCRYPTED = "aes"
6670
private var backupFilename: String? = null
6771

6872
/** Code for internal backup location, used for [backupLocation] */
@@ -173,7 +177,7 @@ class RoomBackup(var context: Context) {
173177

174178
/**
175179
* Set custom Backup File Name, default = "$dbName-$currentTime.sqlite3"
176-
*
180+
* customBackupFileName should not contain file extension. File extension(s) will be added automatically
177181
* @param customBackupFileName String
178182
*/
179183
fun customBackupFileName(customBackupFileName: String): RoomBackup {
@@ -347,13 +351,15 @@ class RoomBackup(var context: Context) {
347351
// Needed for storage permissions request
348352
currentProcess = PROCESS_BACKUP
349353

350-
// Create name for backup file, if no custom name is set: Database name + currentTime +
351-
// .sqlite3
354+
// Create name for backup file
355+
// if no custom name is set: Database name + currentTime + .sqlite3
356+
// for custom name: customBackupFileName + .sqlite3
357+
// if backup is encrypted: .aes will be added to filename
352358
var filename =
353-
if (customBackupFileName == null) "$dbName-${getTime()}.sqlite3"
354-
else customBackupFileName as String
359+
if (customBackupFileName == null) "$dbName-${getTime()}.$BACKUP_EXTENSION_BASE"
360+
else "$customBackupFileName.$BACKUP_EXTENSION_BASE"
355361
// Add .aes extension to filename, if file is encrypted
356-
if (backupIsEncrypted) filename += ".aes"
362+
if (backupIsEncrypted) filename += ".$BACKUP_EXTENSION_ENCRYPTED"
357363
if (enableLogDebug) Log.d(TAG, "backupFilename: $filename")
358364

359365
when (backupLocation) {
@@ -572,10 +578,11 @@ class RoomBackup(var context: Context) {
572578
* @param source File
573579
*/
574580
private fun doRestore(source: File) {
581+
val fileExtension = source.extension
582+
if (!isChosenFileValidForRestore(fileExtension)) return
575583
// Close the database
576584
roomDatabase!!.close()
577585
roomDatabase = null
578-
val fileExtension = source.extension
579586
if (backupIsEncrypted) {
580587
copy(source, TEMP_BACKUP_FILE)
581588
val decryptedBytes = decryptBackup() ?: return
@@ -584,23 +591,9 @@ class RoomBackup(var context: Context) {
584591
bos.flush()
585592
bos.close()
586593
} else {
587-
if (fileExtension == "aes") {
588-
if (enableLogDebug)
589-
Log.d(
590-
TAG,
591-
"Cannot restore database, it is encrypted. Maybe you forgot to add the property .fileIsEncrypted(true)"
592-
)
593-
onCompleteListener?.onComplete(
594-
false,
595-
"cannot restore database, see Log for more details (if enabled)",
596-
OnCompleteListener.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
597-
)
598-
return
599-
}
600594
// Copy back database and replace current database
601595
copy(source, DATABASE_FILE)
602596
}
603-
604597
if (enableLogDebug)
605598
Log.d(TAG, "Restore done, decrypted($backupIsEncrypted) and restored from $source")
606599
onCompleteListener?.onComplete(true, "success", OnCompleteListener.EXIT_CODE_SUCCESS)
@@ -741,6 +734,8 @@ class RoomBackup(var context: Context) {
741734
ActivityResultContracts.OpenDocument()
742735
) { result ->
743736
if (result != null) {
737+
val fileExtension = getFileExtension(result)
738+
if (fileExtension == null || !isChosenFileValidForRestore(fileExtension)) return@registerForActivityResult
744739
val inputstream = context.contentResolver.openInputStream(result)!!
745740
doRestore(inputstream)
746741
return@registerForActivityResult
@@ -840,4 +835,68 @@ class RoomBackup(var context: Context) {
840835
}
841836
return true
842837
}
838+
839+
/**
840+
* Gets the file extension of the [Uri] using ContentResolver.
841+
*/
842+
private fun getFileExtension(uri: Uri): String? {
843+
var fileName: String? = null
844+
val cursor = context.contentResolver.query(uri, null, null, null, null)
845+
cursor?.use {
846+
if (it.moveToFirst()) {
847+
val indexOfNameColumn = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
848+
if (indexOfNameColumn >= 0) fileName = it.getString(indexOfNameColumn)
849+
}
850+
}
851+
return fileName?.substringAfterLast(".")
852+
}
853+
854+
/**
855+
* Validating the chosen file with it's [File.extension]
856+
* if [backupIsEncrypted] state matches file extension return true, else false
857+
*/
858+
private fun isChosenFileValidForRestore(fileExtension:String): Boolean {
859+
return when {
860+
backupIsEncrypted && fileExtension == BACKUP_EXTENSION_ENCRYPTED -> true
861+
!backupIsEncrypted && fileExtension == BACKUP_EXTENSION_BASE -> true
862+
backupIsEncrypted && fileExtension == BACKUP_EXTENSION_BASE -> {
863+
if (enableLogDebug) Log.d(TAG,
864+
"isChosenFileValid: chosen file is unencrypted while encrypted file is expected"
865+
)
866+
onCompleteListener?.onComplete(
867+
false,
868+
"chosen file is unencrypted while encrypted file is expected",
869+
OnCompleteListener.EXIT_CODE_ERROR_DECRYPTION_ERROR
870+
)
871+
false
872+
}
873+
!backupIsEncrypted && fileExtension == BACKUP_EXTENSION_ENCRYPTED -> {
874+
if (enableLogDebug)
875+
Log.d(
876+
TAG,
877+
"isChosenFileValid: chosen file is encrypted while unencrypted file is expected"
878+
)
879+
onCompleteListener?.onComplete(
880+
false,
881+
"chosen file is encrypted while unencrypted file is expected",
882+
OnCompleteListener.EXIT_CODE_ERROR_RESTORE_BACKUP_IS_ENCRYPTED
883+
)
884+
false
885+
}
886+
else -> {
887+
if (enableLogDebug)
888+
Log.d(
889+
TAG,
890+
"isChosenFileValid: chosen file is of wrong extension: $fileExtension"
891+
)
892+
onCompleteListener?.onComplete(
893+
false,
894+
"failed to verify the chosen file extension",
895+
OnCompleteListener.EXIT_CODE_ERROR
896+
)
897+
false
898+
}
899+
}
900+
}
901+
843902
}

0 commit comments

Comments
 (0)