Working on backup and restore

This commit is contained in:
Yogesh Paliyal
2021-02-21 21:01:40 +05:30
parent 67a319d299
commit e9ea508a5d
17 changed files with 218 additions and 107 deletions

View File

@@ -3,6 +3,7 @@ package com.yogeshpaliyal.keypass
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.yogeshpaliyal.keypass.data.AccountModel
import com.yogeshpaliyal.keypass.db.DbDao
@@ -31,8 +32,6 @@ abstract class AppDatabase : RoomDatabase() {
if (!this::_instance.isInitialized)
synchronized(this) {
@@ -43,18 +42,25 @@ abstract class AppDatabase : RoomDatabase() {
).addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
super.onDestructiveMigration(db)
onCreate(db)
}
})
.fallbackToDestructiveMigration()
/* .addMigrations(object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
val cursor = database.query("SELECT * FROM account")
while(cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndex("id"))
val password = cursor.getLong(cursor.getColumnIndex("password"))
//-- Hash your password --//
database.execSQL("UPDATE account SET password = hashedPassword WHERE id = $id;")
}
}
})*/
.build()
}
@@ -62,4 +68,6 @@ abstract class AppDatabase : RoomDatabase() {
return _instance
}
}
}

View File

@@ -16,7 +16,7 @@ import com.yogeshpaliyal.universal_adapter.model.BaseDiffUtil
class AccountModel(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
val id: Long? = null,
var id: Long? = null,
@ColumnInfo(name = "title")
var title: String? = null,
@ColumnInfo(name = "username")

View File

@@ -21,6 +21,9 @@ abstract class DbDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insertOrUpdateAccount(accountModel: AccountModel)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insertOrUpdateAccount(accountModel: List<AccountModel>)
@Query("SELECT * from account")
abstract fun getAllAccounts() : Flow<List<AccountModel>>

View File

@@ -2,6 +2,9 @@ package com.yogeshpaliyal.keypass.db_helper
import android.content.ContentResolver
import android.net.Uri
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.room.withTransaction
import com.google.gson.Gson
import com.yogeshpaliyal.keypass.AppDatabase
import com.yogeshpaliyal.keypass.data.AccountModel
@@ -12,6 +15,8 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import java.io.File
import java.io.OutputStream
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
/*
@@ -36,7 +41,14 @@ suspend fun AppDatabase.createBackup(contentResolver : ContentResolver,fileUri:
suspend fun AppDatabase.restoreBackup(contentResolver : ContentResolver,fileUri: Uri?) = withContext(Dispatchers.IO){
fileUri ?: return@withContext false
EncryptionHelper.doCryptoDecrypt(getOrCreateBackupKey(), contentResolver.openInputStream(fileUri)).logD("DecryptedFile")
val restoredFile = EncryptionHelper.doCryptoDecrypt(getOrCreateBackupKey(), contentResolver.openInputStream(fileUri))
val data = Gson().fromJson(restoredFile, Array<AccountModel>::class.java).toList()
data.forEach {
it.id = null
}
withTransaction {
getDao().insertOrUpdateAccount(data.toList())
}
return@withContext true
}
}

View File

@@ -1,13 +1,17 @@
package com.yogeshpaliyal.keypass.db_helper
import java.io.*
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.UnsupportedEncodingException
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.Key
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.security.spec.InvalidKeySpecException
import java.security.spec.InvalidParameterSpecException
import javax.crypto.*
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@@ -20,20 +24,28 @@ import javax.crypto.spec.SecretKeySpec
*/
object EncryptionHelper {
private const val ALGORITHM = "AES"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
// private const val TRANSFORMATION = "AES/GCM/PKCS5Padding"
private const val TRANSFORMATION = "AES"
// private const val TRANSFORMATION = "DES/CBC/PKCS5Padding"
private const val TAG_LENGTH_BIT = 128
private const val IV_LENGTH_BYTE = 12
@Throws(CryptoException::class)
fun doCryptoEncrypt(key: String, data: String,
fun doCryptoEncrypt(
key: String, data: String,
outputFile: OutputStream?
) {
try {
val iv = ByteArray(IV_LENGTH_BYTE)
val secretKey: Key =
SecretKeySpec(key.toByteArray(), ALGORITHM)
val cipher = Cipher.getInstance(TRANSFORMATION)
// val spec = GCMParameterSpec(TAG_LENGTH_BIT, iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
data.byteInputStream().use {
@@ -76,6 +88,8 @@ object EncryptionHelper {
val secretKey: Key =
SecretKeySpec(key.toByteArray(), ALGORITHM)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, secretKey)
inputFile.use {
@@ -112,4 +126,37 @@ object EncryptionHelper {
fun encryptPassword(message: String, password: String): String{
/* Encrypt the message. */
var cipher: Cipher? = null
cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, generateKey(password))
return String(cipher.doFinal(message.encodeToByteArray()))
}
@Throws(
NoSuchPaddingException::class,
NoSuchAlgorithmException::class,
InvalidParameterSpecException::class,
InvalidAlgorithmParameterException::class,
InvalidKeyException::class,
BadPaddingException::class,
IllegalBlockSizeException::class,
UnsupportedEncodingException::class
)
fun decryptMsg(encryptedMessage: String, password: String): String {
/* Decrypt the message, given derived encContentValues and initialization vector. */
var cipher: Cipher? = null
cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, generateKey(password))
return String(cipher.doFinal(encryptedMessage.toByteArray()))
}
@Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class)
fun generateKey(passLockKey: String): SecretKey {
return SecretKeySpec(passLockKey.encodeToByteArray(), "AES")
}
}

View File

@@ -1,7 +1,5 @@
package com.yogeshpaliyal.keypass.ui.auth
import android.R.attr.description
import android.app.KeyguardManager
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
@@ -11,7 +9,6 @@ import androidx.core.content.ContextCompat
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.databinding.ActivityAuthenticationBinding
import com.yogeshpaliyal.keypass.ui.nav.DashboardActivity
import com.yogeshpaliyal.keypass.utils.SharedPrefHelper
import java.util.concurrent.Executor
@@ -23,9 +20,6 @@ class AuthenticationActivity : AppCompatActivity() {
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo
private val userPin by lazy {
SharedPrefHelper.getString(SharedPrefHelper.SharedPrefKeys.USER_PIN)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -78,7 +72,6 @@ class AuthenticationActivity : AppCompatActivity() {
// Consider integrating with the keystore to unlock cryptographic operations,
// if needed by your app.
biometricPrompt.authenticate(promptInfo)
binding.btnRetry.setOnClickListener {

View File

@@ -11,9 +11,11 @@ import androidx.navigation.fragment.navArgs
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.data.AccountModel
import com.yogeshpaliyal.keypass.databinding.FragmentHomeBinding
import com.yogeshpaliyal.keypass.db_helper.EncryptionHelper
import com.yogeshpaliyal.keypass.listener.UniversalClickListener
import com.yogeshpaliyal.keypass.ui.detail.DetailActivity
import com.yogeshpaliyal.keypass.utils.initViewModel
import com.yogeshpaliyal.keypass.utils.logD
import com.yogeshpaliyal.universal_adapter.adapter.UniversalAdapterViewType
import com.yogeshpaliyal.universal_adapter.adapter.UniversalRecyclerAdapter
import com.yogeshpaliyal.universal_adapter.utils.Resource

View File

@@ -2,18 +2,22 @@ package com.yogeshpaliyal.keypass.ui.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.DocumentsContract
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.yogeshpaliyal.keypass.AppDatabase
import com.yogeshpaliyal.keypass.BuildConfig
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.db_helper.createBackup
import com.yogeshpaliyal.keypass.db_helper.restoreBackup
import com.yogeshpaliyal.keypass.utils.canUserAccessBackupDirectory
import com.yogeshpaliyal.keypass.utils.email
import com.yogeshpaliyal.keypass.utils.getBackupDirectory
import com.yogeshpaliyal.keypass.utils.setBackupDirectory
import kotlinx.coroutines.launch
class MySettingsFragment : PreferenceFragmentCompat() {
@@ -40,31 +44,45 @@ class MySettingsFragment : PreferenceFragmentCompat() {
selectRestoreFile()
return true
}
"share" -> {
val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(
Intent.EXTRA_TEXT,
"KeyPass Password Manager\n Offline, Secure, Open Source https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID
)
sendIntent.type = "text/plain"
startActivity(Intent.createChooser(sendIntent, "Share KeyPass"))
return true
}
}
return super.onPreferenceTreeClick(preference)
}
private fun selectBackupDirectory(){
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
val selectedDirectory = Uri.parse(getBackupDirectory())
/*if (Build.VERSION.SDK_INT >= 26) {
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
SignalStore.settings().getLatestSignalBackupDirectory()
)
}*/
context?.let {
if(it.canUserAccessBackupDirectory()){
backup(selectedDirectory)
}else{
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
intent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
try {
startActivityForResult(intent, CHOOSE_BACKUPS_LOCATION_REQUEST_CODE)
}catch (e: Exception){
e.printStackTrace()
try {
startActivityForResult(intent, CHOOSE_BACKUPS_LOCATION_REQUEST_CODE)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
@@ -73,15 +91,8 @@ class MySettingsFragment : PreferenceFragmentCompat() {
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
/*if (Build.VERSION.SDK_INT >= 26) {
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
SignalStore.settings().getLatestSignalBackupDirectory()
)
}*/
intent.addFlags(
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
@@ -103,12 +114,8 @@ class MySettingsFragment : PreferenceFragmentCompat() {
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
val tempFile = DocumentFile.fromTreeUri(requireContext(), selectedDirectory)?.createFile("*/*","key_pass_backup_${System.currentTimeMillis()}.keypass")
lifecycleScope.launch {
AppDatabase.getInstance().createBackup(contentResolver, tempFile?.uri)
Toast.makeText(context, "File saved", Toast.LENGTH_SHORT).show()
}
setBackupDirectory(selectedDirectory.toString())
backup(selectedDirectory)
}
} else if (requestCode == CHOOSE_RESTORE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK){
val contentResolver = context?.contentResolver
@@ -120,10 +127,30 @@ class MySettingsFragment : PreferenceFragmentCompat() {
lifecycleScope.launch {
AppDatabase.getInstance().restoreBackup(contentResolver, selectedFile)
Toast.makeText(context, "File saved", Toast.LENGTH_SHORT).show()
Toast.makeText(context, getString(R.string.backup_restored), Toast.LENGTH_SHORT).show()
}
}
}
}
fun backup(selectedDirectory: Uri){
val tempFile = DocumentFile.fromTreeUri(requireContext(), selectedDirectory)?.createFile(
"*/*",
"key_pass_backup_${System.currentTimeMillis()}.keypass"
)
lifecycleScope.launch {
context?.contentResolver?.let { AppDatabase.getInstance().createBackup(
it,
tempFile?.uri
)
Toast.makeText(context, getString(R.string.backup_completed), Toast.LENGTH_SHORT).show()
}
}
}
}

View File

@@ -1,7 +1,10 @@
package com.yogeshpaliyal.keypass.utils
import android.content.Context
import android.net.Uri
import android.text.TextUtils
import androidx.documentfile.provider.DocumentFile
import java.security.SecureRandom
import java.util.*
/*
@@ -12,7 +15,8 @@ import java.util.*
*/
fun getRandomString(sizeOfRandomString: Int): String {
val ALLOWED_CHARACTERS = "0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM!@#$%&*_+"
val ALLOWED_CHARACTERS =
"0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM!@#$%&*_+"
val random = SecureRandom()
val sb = StringBuilder(sizeOfRandomString)
for (i in 0 until sizeOfRandomString) sb.append(
@@ -22,3 +26,20 @@ fun getRandomString(sizeOfRandomString: Int): String {
)
return sb.toString()
}
fun Context.canUserAccessBackupDirectory(): Boolean {
val backupDirectoryUri = getUri(getBackupDirectory()) ?: return false
val backupDirectory = DocumentFile.fromTreeUri(this, backupDirectoryUri)
return backupDirectory != null && backupDirectory.exists() && backupDirectory.canRead() && backupDirectory.canWrite()
}
private fun getUri(string: String?): Uri? {
val uri = string
return if (TextUtils.isEmpty(uri)) {
null
} else {
Uri.parse(uri)
}
}

View File

@@ -1,47 +0,0 @@
package com.yogeshpaliyal.keypass.utils
import android.content.Context
import androidx.annotation.StringDef
import androidx.core.content.edit
import com.yogeshpaliyal.keypass.MyApplication
import com.yogeshpaliyal.keypass.R
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
/*
* @author Yogesh Paliyal
* techpaliyal@gmail.com
* https://techpaliyal.com
* created on 22-01-2021 22:40
*/
object SharedPrefHelper {
@StringDef(open = false,value = [
SharedPrefKeys.USER_PIN
])
annotation class SharedPrefKeys{
companion object{
const val USER_PIN = "USER_PIN"
}
}
private val sp by lazy {
MyApplication.instance.getSharedPreferences(
MyApplication.instance.getString(R.string.app_name),
Context.MODE_PRIVATE
);
}
fun setString(@SharedPrefKeys key: String,value : String?){
sp.edit { putString(key,value)}
}
fun getString(@SharedPrefKeys key: String, default : String = "") = sp.getString(key,default)
}

View File

@@ -59,4 +59,18 @@ fun getOrCreateBackupKey(): String{
}
}
private const val BACKUP_KEY = "backup_key"
fun setBackupDirectory(string: String){
getSharedPreferences().edit {
putString(BACKUP_DIRECTORY, string)
}
}
fun getBackupDirectory(): String{
val sp = getSharedPreferences()
return sp.getString(BACKUP_DIRECTORY,"") ?: ""
}
private const val BACKUP_KEY = "backup_key"
private const val BACKUP_DIRECTORY = "backup_directory"

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z"/>
</vector>

View File

@@ -18,6 +18,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/illustrator"
app:layout_constraintVertical_bias="0.7"
android:gravity="center"
android:text="No Accounts added, please add from below button"
app:layout_constrainedWidth="true"
style="@style/TextStyle.Heading"

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textSize="14sp"
android:text="Copy This Key Phrase this will be used to recover this backup"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.KeyPass" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.KeyPass" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!--Color-->
<item name="colorPrimary">@color/keypass_blue_200</item>
<item name="colorPrimaryVariant">@color/keypass_blue_300</item>

View File

@@ -9,4 +9,8 @@
<string name="generate_shortcut_short_label">Generate Password</string>
<string name="retry">Retry</string>
<string name="backup_restored">Backup Restored</string>
<string name="backup_completed">Backup Completed</string>
</resources>

View File

@@ -8,13 +8,11 @@
<Preference
app:key="backup"
app:icon="@drawable/ic_baseline_feedback_24"
app:title="Backup"
app:summary="Backup your credentials in encrypted file" />
<Preference
app:key="restore"
app:icon="@drawable/ic_baseline_share_24"
app:title="Restore"
app:summary="Restore your backup" />
</PreferenceCategory>
@@ -34,5 +32,6 @@
app:icon="@drawable/ic_baseline_share_24"
app:title="Share"
app:summary="Share app with others" />
</PreferenceCategory>
</PreferenceScreen>