mirror of
https://github.com/wgtunnel/Android-Room-Database-Backup.git
synced 2026-10-09 14:40:29 -07:00
improved en- and decryption with aes
-> Saved encryption key in EncryptedSharedPreferences
This commit is contained in:
1 parent
2e9e9081b2
commit
2e6209f79a
6 files changed
+288
-115
No files matched your search
+4
-1
@@ -11,7 +11,7 @@ android {
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "0.3.0"
|
||||
versionName "0.4.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
@@ -58,4 +58,7 @@ dependencies {
|
||||
//Material Design Implementation
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
|
||||
//Androidx Security
|
||||
implementation "androidx.security:security-crypto:1.1.0-alpha01"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.ebner.roomdatabasebackup.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import java.io.*
|
||||
import java.nio.file.Path
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.spec.InvalidKeySpecException
|
||||
import java.security.spec.KeySpec
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* Copyright 2020 Raphael Ebner
|
||||
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
class AESEncryptionHelper {
|
||||
|
||||
companion object {
|
||||
private const val BACKUP_SECRET_KEY = "backupsecretkey"
|
||||
private const val TAG = "debug_AESEncryptionHelper"
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will convert a file to ByteArray
|
||||
* @param filePath : the Path where the file is located
|
||||
* @return ByteArray of the file
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun readFile(filePath: Path): ByteArray {
|
||||
val file = File(filePath.toUri())
|
||||
val fileContents = file.readBytes()
|
||||
val inputBuffer = BufferedInputStream(
|
||||
FileInputStream(file)
|
||||
)
|
||||
inputBuffer.read(fileContents)
|
||||
inputBuffer.close()
|
||||
return fileContents
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will convert a ByteArray to a file, and saves it to the path
|
||||
* @param fileData : the ByteArray
|
||||
* @param path : the path where the ByteArray should be saved
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun saveFile(fileData: ByteArray, path: Path) {
|
||||
val file = File(path.toUri())
|
||||
val bos = BufferedOutputStream(FileOutputStream(file, false))
|
||||
bos.write(fileData)
|
||||
bos.flush()
|
||||
bos.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will convert a file to ByteArray
|
||||
* @param sharedPref : the sharedPref, to fetch the key
|
||||
* @param iv : the encryption nonce
|
||||
* @return SecretKey
|
||||
*/
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun getSecretKey(sharedPref: SharedPreferences, iv: ByteArray): SecretKey {
|
||||
|
||||
Log.d(TAG, "sharedprefs helper: $sharedPref")
|
||||
|
||||
//get key: String from sharedpref
|
||||
var password = sharedPref.getString(BACKUP_SECRET_KEY, null)
|
||||
|
||||
//If no key is stored in shared pref, create one and save it
|
||||
if (password == null) {
|
||||
//generate random string
|
||||
val stringLength = 15
|
||||
val charset = ('a'..'z') + ('A'..'Z') + ('1'..'9')
|
||||
password = (1..stringLength)
|
||||
.map { charset.random() }
|
||||
.joinToString("")
|
||||
|
||||
val secretKey = generateSecretKey(password, iv)
|
||||
//the key can be saved plain, because i am using EncryptedSharedPreferences
|
||||
val editor = sharedPref.edit()
|
||||
editor.putString(BACKUP_SECRET_KEY, password)
|
||||
//I use .commit because when using .apply the needed app restart is faster then apply and the preferences wont be saved
|
||||
editor.commit()
|
||||
|
||||
return secretKey
|
||||
}
|
||||
|
||||
//generate secretKey, and return it
|
||||
return generateSecretKey(password, iv)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to generate a 128 bit key from the given password and iv
|
||||
* @param password
|
||||
* @param iv
|
||||
* @return Secret key
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws InvalidKeySpecException
|
||||
*/
|
||||
@Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class)
|
||||
private fun generateSecretKey(password: String, iv: ByteArray?): SecretKey {
|
||||
//convert random string to secretKey
|
||||
val spec: KeySpec = PBEKeySpec(password.toCharArray(), iv, 65536, 128) // AES-128
|
||||
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
val key = secretKeyFactory.generateSecret(spec).encoded
|
||||
return SecretKeySpec(key, "AES")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.ebner.roomdatabasebackup.core
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.InvalidKeySpecException
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
/**
|
||||
* Encryption / Decryption service using the AES algorithm
|
||||
* example for nullbeans.com
|
||||
* https://nullbeans.com/how-to-encrypt-decrypt-files-byte-arrays-in-java-using-aes-gcm/#Generating_an_AES_key
|
||||
*/
|
||||
class AESEncryptionManager {
|
||||
|
||||
private val aesEncryptionHelper = AESEncryptionHelper()
|
||||
|
||||
/**
|
||||
* This method will encrypt the given data
|
||||
* @param sharedPref : the sharedPref, to fetch the key
|
||||
* @param data : the data that will be encrypted
|
||||
* @return Encrypted data in a byte array
|
||||
*/
|
||||
@Throws(
|
||||
NoSuchPaddingException::class,
|
||||
NoSuchAlgorithmException::class,
|
||||
InvalidAlgorithmParameterException::class,
|
||||
InvalidKeyException::class,
|
||||
BadPaddingException::class,
|
||||
IllegalBlockSizeException::class,
|
||||
InvalidKeySpecException::class
|
||||
)
|
||||
fun encryptData(sharedPref: SharedPreferences, data: ByteArray): ByteArray {
|
||||
|
||||
//Prepare the nonce
|
||||
val secureRandom = SecureRandom()
|
||||
|
||||
//Noonce should be 12 bytes
|
||||
val iv = ByteArray(12)
|
||||
secureRandom.nextBytes(iv)
|
||||
|
||||
//Prepare your key/password
|
||||
val secretKey = aesEncryptionHelper.getSecretKey(sharedPref, iv)
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
val parameterSpec = GCMParameterSpec(128, iv)
|
||||
|
||||
//Encryption mode on!
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec)
|
||||
|
||||
//Encrypt the data
|
||||
val encryptedData = cipher.doFinal(data)
|
||||
|
||||
//Concatenate everything and return the final data
|
||||
val byteBuffer = ByteBuffer.allocate(4 + iv.size + encryptedData.size)
|
||||
byteBuffer.putInt(iv.size)
|
||||
byteBuffer.put(iv)
|
||||
byteBuffer.put(encryptedData)
|
||||
return byteBuffer.array()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will decrypt the given data
|
||||
* @param sharedPref : the sharedPref, to fetch the key
|
||||
* @param encryptedData : the data that will be decrypted
|
||||
* @return decrypted data in a byte array
|
||||
*/
|
||||
@Throws(
|
||||
NoSuchPaddingException::class,
|
||||
NoSuchAlgorithmException::class,
|
||||
InvalidAlgorithmParameterException::class,
|
||||
InvalidKeyException::class,
|
||||
BadPaddingException::class,
|
||||
IllegalBlockSizeException::class,
|
||||
InvalidKeySpecException::class
|
||||
)
|
||||
fun decryptData(sharedPref: SharedPreferences, encryptedData: ByteArray): ByteArray {
|
||||
|
||||
|
||||
//Wrap the data into a byte buffer to ease the reading process
|
||||
val byteBuffer = ByteBuffer.wrap(encryptedData)
|
||||
val noonceSize = byteBuffer.int
|
||||
|
||||
//Make sure that the file was encrypted properly
|
||||
require(!(noonceSize < 12 || noonceSize >= 16)) { "Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file." }
|
||||
val iv = ByteArray(noonceSize)
|
||||
byteBuffer[iv]
|
||||
|
||||
//Prepare your key/password
|
||||
val secretKey = aesEncryptionHelper.getSecretKey(sharedPref, iv)
|
||||
|
||||
//get the rest of encrypted data
|
||||
val cipherBytes = ByteArray(byteBuffer.remaining())
|
||||
byteBuffer[cipherBytes]
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
val parameterSpec = GCMParameterSpec(128, iv)
|
||||
|
||||
//Encryption mode on!
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec)
|
||||
|
||||
//Encrypt the data
|
||||
return cipher.doFinal(cipherBytes)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package com.ebner.roomdatabasebackup.core
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import java.io.*
|
||||
import java.nio.file.Path
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* https://medium.com/@oshanm1/how-to-encrypt-and-decrypt-a-file-in-android-kotlin-80c8e9168513#:~:text=We%20can%20use%20the%20Java,case%20to%20encrypt%20the%20file.
|
||||
*/
|
||||
class EncryptDecryptBackup {
|
||||
|
||||
private val BACKUP_SECRET_KEY = "backupsecretkey"
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun readFile(filePath: Path): ByteArray {
|
||||
val file = File(filePath.toUri())
|
||||
val fileContents = file.readBytes()
|
||||
val inputBuffer = BufferedInputStream(
|
||||
FileInputStream(file)
|
||||
)
|
||||
inputBuffer.read(fileContents)
|
||||
inputBuffer.close()
|
||||
return fileContents
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun saveFile(fileData: ByteArray, path: String) {
|
||||
val file = File(path)
|
||||
val bos = BufferedOutputStream(FileOutputStream(file, false))
|
||||
bos.write(fileData)
|
||||
bos.flush()
|
||||
bos.close()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun encrypt(yourKey: SecretKey, fileData: ByteArray): ByteArray {
|
||||
val data = yourKey.encoded
|
||||
val skeySpec = SecretKeySpec(data, 0, data.size, "AES")
|
||||
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, IvParameterSpec(ByteArray(cipher.blockSize)))
|
||||
return cipher.doFinal(fileData)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun decrypt(yourKey: SecretKey, fileData: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, yourKey, IvParameterSpec(ByteArray(cipher.blockSize)))
|
||||
return cipher.doFinal(fileData)
|
||||
}
|
||||
|
||||
fun getSecretKey(sharedPref: SharedPreferences): SecretKey {
|
||||
|
||||
val key = sharedPref.getString(BACKUP_SECRET_KEY, null)
|
||||
|
||||
if (key == null) {
|
||||
//generate secure random
|
||||
val secretKey = generateSecretKey()
|
||||
saveSecretKey(sharedPref, secretKey!!)
|
||||
return secretKey
|
||||
}
|
||||
|
||||
val decodedKey = Base64.decode(key, Base64.NO_WRAP)
|
||||
val originalKey = SecretKeySpec(decodedKey, 0, decodedKey.size, "AES")
|
||||
|
||||
return originalKey
|
||||
}
|
||||
|
||||
private fun saveSecretKey(sharedPref: SharedPreferences, secretKey: SecretKey): String {
|
||||
val encodedKey = Base64.encodeToString(secretKey.encoded, Base64.NO_WRAP)
|
||||
sharedPref.edit().putString(BACKUP_SECRET_KEY, encodedKey).apply()
|
||||
return encodedKey
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun generateSecretKey(): SecretKey? {
|
||||
val secureRandom = SecureRandom()
|
||||
val keyGenerator = KeyGenerator.getInstance("AES")
|
||||
//generate a key with secure random
|
||||
keyGenerator?.init(128, secureRandom)
|
||||
return keyGenerator?.generateKey()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,19 @@
|
||||
package com.ebner.roomdatabasebackup.core
|
||||
|
||||
/**
|
||||
* Created by raphael on 15.06.2020.
|
||||
* Android Room Database Backup Created in com.ebner.roomdatabasebackup.core
|
||||
* Copyright 2020 Raphael Ebner
|
||||
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
interface OnCompleteListener {
|
||||
fun onComplete(success: Boolean, message: String)
|
||||
|
||||
@@ -6,8 +6,9 @@ import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -37,15 +38,18 @@ import kotlin.Comparator
|
||||
*/
|
||||
class RoomBackup {
|
||||
|
||||
private val SHARED_PREFS = "ardbbackup"
|
||||
companion object {
|
||||
private const val SHARED_PREFS = "com.ebner.roomdatabasebackup"
|
||||
private var TAG = "debug_RoomBackup"
|
||||
private lateinit var INTERNAL_BACKUP_PATH: Path
|
||||
private lateinit var TEMP_BACKUP_PATH: Path
|
||||
private lateinit var TEMP_BACKUP_FILE: Path
|
||||
private lateinit var EXTERNAL_BACKUP_PATH: Path
|
||||
private lateinit var DATABASE_FILE: Path
|
||||
}
|
||||
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private var TAG = "debug_RoomBackup"
|
||||
private lateinit var dbName: String
|
||||
private lateinit var INTERNAL_BACKUP_PATH: Path
|
||||
private lateinit var TEMP_BACKUP_PATH: Path
|
||||
private lateinit var TEMP_BACKUP_FILE: Path
|
||||
private lateinit var EXTERNAL_BACKUP_PATH: Path
|
||||
private lateinit var DATABASE_FILE: Path
|
||||
|
||||
private var context: Context? = null
|
||||
private var roomDatabase: RoomDatabase? = null
|
||||
@@ -203,7 +207,20 @@ class RoomBackup {
|
||||
return false
|
||||
}
|
||||
|
||||
sharedPreferences = context!!.getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE)
|
||||
//Create or retrieve the Master Key for encryption/decryption
|
||||
val masterKeyAlias = MasterKey.Builder(context!!)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
//Initialize/open an instance of EncryptedSharedPreferences
|
||||
//Encryption key is stored in plain text in an EncryptedSharedPreferences --> it is saved encrypted
|
||||
sharedPreferences = EncryptedSharedPreferences.create(
|
||||
context!!,
|
||||
SHARED_PREFS,
|
||||
masterKeyAlias,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
|
||||
dbName = roomDatabase!!.openHelper.databaseName
|
||||
INTERNAL_BACKUP_PATH = Paths.get("${context!!.filesDir}/databasebackup/")
|
||||
@@ -282,14 +299,13 @@ class RoomBackup {
|
||||
Files.copy(DATABASE_FILE, TEMP_BACKUP_FILE, StandardCopyOption.REPLACE_EXISTING)
|
||||
|
||||
//encrypt temp file, and save it to backup location
|
||||
val encryptDecryptBackup = EncryptDecryptBackup()
|
||||
val encryptDecryptBackup = AESEncryptionHelper()
|
||||
val fileData = encryptDecryptBackup.readFile(TEMP_BACKUP_FILE)
|
||||
//get secret key
|
||||
val secretKey = encryptDecryptBackup.getSecretKey(sharedPreferences)
|
||||
//encrypt file
|
||||
val encodedData = encryptDecryptBackup.encrypt(secretKey, fileData)
|
||||
|
||||
encryptDecryptBackup.saveFile(encodedData, backuppath.toString())
|
||||
val aesEncryptionManager = AESEncryptionManager()
|
||||
val encryptedBytes = aesEncryptionManager.encryptData(sharedPreferences, fileData)
|
||||
|
||||
encryptDecryptBackup.saveFile(encryptedBytes, backuppath)
|
||||
|
||||
//Delete temp file
|
||||
Files.delete(TEMP_BACKUP_FILE)
|
||||
@@ -315,14 +331,13 @@ class RoomBackup {
|
||||
Files.copy(backuppath, TEMP_BACKUP_FILE, StandardCopyOption.REPLACE_EXISTING)
|
||||
|
||||
//Decrypt temp file, and save it to database location
|
||||
val encryptDecryptBackup = EncryptDecryptBackup()
|
||||
val encryptDecryptBackup = AESEncryptionHelper()
|
||||
val fileData = encryptDecryptBackup.readFile(TEMP_BACKUP_FILE)
|
||||
val secretKey = encryptDecryptBackup.getSecretKey(sharedPreferences)
|
||||
var decodedData: ByteArray? = null
|
||||
runBlocking(Dispatchers.IO) {
|
||||
decodedData = encryptDecryptBackup.decrypt(secretKey, fileData)
|
||||
}
|
||||
encryptDecryptBackup.saveFile(decodedData!!, DATABASE_FILE.toString())
|
||||
|
||||
val aesEncryptionManager = AESEncryptionManager()
|
||||
val decryptedBytes = aesEncryptionManager.decryptData(sharedPreferences, fileData)
|
||||
|
||||
encryptDecryptBackup.saveFile(decryptedBytes, DATABASE_FILE)
|
||||
|
||||
//Delete tem file
|
||||
Files.delete(TEMP_BACKUP_FILE)
|
||||
|
||||
Reference in new issue
Block a user