improved en- and decryption with aes

-> Saved encryption key in EncryptedSharedPreferences
This commit is contained in:
Raphael Ebner authored and rafi0101 committed 2020-06-22 19:46:28 +02:00
1 parent 2e9e9081b2
commit 2e6209f79a
6 files changed
+288 -115

No files matched your search

+4 -1
View File
@@ -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)