Skip to content

Commit

Permalink
Added SecureStorageProvider for storing data encrypted (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
Miggets7 authored Jul 26, 2024
1 parent 3442660 commit 46c5935
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package io.openremote.orlib.service

import android.content.Context
import android.content.SharedPreferences
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import androidx.preference.PreferenceManager
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

class SecureStorageProvider(val context: Context) {

private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)
private val keyAlias = context.packageName + ".secure_storage_key"

companion object {
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val TRANSFORMATION =
"${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_GCM}/${KeyProperties.ENCRYPTION_PADDING_NONE}"
}

init {
if (!isKeyExists()) {
generateKey()

// Migrate data from default shared preferences to secure storage
// This is a one-time operation
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val allEntries = defaultSharedPreferences.all
for ((key, value) in allEntries) {
val encryptedData = encryptData(value as String)
storeData(key, encryptedData)
defaultSharedPreferences.edit().remove(key).apply()
}
}
}

private fun getKeyStore(): KeyStore {
return KeyStore.getInstance(ANDROID_KEYSTORE).apply {
load(null)
}
}

private fun generateKey() {
val keyGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
}

private fun isKeyExists(): Boolean {
val keyStore = getKeyStore()
return keyStore.containsAlias(keyAlias)
}

private fun getSecretKey(): SecretKey {
val keyStore = getKeyStore()
return keyStore.getKey(keyAlias, null) as SecretKey
}

private fun encryptData(data: String): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
val iv = cipher.iv
val encryptedData = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
val combined = iv + encryptedData
return Base64.encodeToString(combined, Base64.DEFAULT)
}

fun initialize(): Map<String, Any> {
return hashMapOf(
"action" to "PROVIDER_INIT",
"provider" to "storage",
"version" to "1.0.0",
"enabled" to true,
"requiresPermission" to false,
"hasPermission" to true,
"success" to true,
)
}

fun enable(): Map<String, Any> {
return hashMapOf(
"action" to "PROVIDER_ENABLE",
"provider" to "storage",
"hasPermission" to true,
"success" to true,
)
}

fun storeData(key: String?, data: String?) {
if (key == null) return

val editor = sharedPreferences.edit()
if (data == null) {
editor.remove(key)
} else {
val encryptedData = encryptData(data)
editor.putString(key, encryptedData)
}
editor.apply()
}

fun retrieveData(key: String?): Map<String, Any?> {
val result = hashMapOf(
"action" to "RETRIEVE",
"provider" to "storage",
"key" to key,
"value" to null
)

val encryptedData = sharedPreferences.getString(key, null) ?: return result

val combined = Base64.decode(encryptedData, Base64.DEFAULT)
val iv = combined.copyOfRange(0, 12)
val data = combined.copyOfRange(12, combined.size)

val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
val plainData = cipher.doFinal(data).toString(Charsets.UTF_8)

result["value"] = plainData
return result
}
}
49 changes: 9 additions & 40 deletions ORLib/src/main/java/io/openremote/orlib/ui/OrMainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import io.openremote.orlib.service.BleProvider
import io.openremote.orlib.service.ConnectivityChangeReceiver
import io.openremote.orlib.service.GeofenceProvider
import io.openremote.orlib.service.QrScannerProvider
import io.openremote.orlib.service.SecureStorageProvider
import io.openremote.orlib.shared.SharedData.offlineActivity
import org.json.JSONException
import org.json.JSONObject
Expand Down Expand Up @@ -72,6 +73,7 @@ open class OrMainActivity : Activity() {
private var geofenceProvider: GeofenceProvider? = null
private var qrScannerProvider: QrScannerProvider? = null
private var bleProvider: BleProvider? = null
private var secureStorageProvider: SecureStorageProvider? = null
private var consoleId: String? = null
private var connectFailCount: Int = 0
private var connectFailResetHandler: Handler? = null
Expand Down Expand Up @@ -797,34 +799,26 @@ open class OrMainActivity : Activity() {
@Throws(JSONException::class)
private fun handleStorageProviderMessage(data: JSONObject) {
val action = data.getString("action")
if (secureStorageProvider == null) {
secureStorageProvider = SecureStorageProvider(activity)
}
when {
action.equals("PROVIDER_INIT", ignoreCase = true) -> {
val response: MutableMap<String, Any> = HashMap()
response["action"] = "PROVIDER_INIT"
response["provider"] = "storage"
response["version"] = "1.0.0"
response["enabled"] = true
response["requiresPermission"] = false
response["hasPermission"] = true
response["success"] = true
val response = secureStorageProvider!!.initialize()
notifyClient(response)
}

action.equals("PROVIDER_ENABLE", ignoreCase = true) -> {
// Doesn't require enabling but just in case it gets called lets return a valid response
val response: MutableMap<String, Any> = HashMap()
response["action"] = "PROVIDER_ENABLE"
response["provider"] = "storage"
response["hasPermission"] = true
response["success"] = true
val response = secureStorageProvider!!.enable()
notifyClient(response)
}

action.equals("STORE", ignoreCase = true) -> {
try {
val key = data.getString("key")
val valueJson = data.getString("value")
storeData(key, valueJson)
secureStorageProvider!!.storeData(key, valueJson)
} catch (e: JSONException) {
LOG.log(Level.SEVERE, "Failed to store data", e)
}
Expand All @@ -833,12 +827,7 @@ open class OrMainActivity : Activity() {
action.equals("RETRIEVE", ignoreCase = true) -> {
try {
val key = data.getString("key")
val dataJson = retrieveData(key)
val response: MutableMap<String, Any?> = HashMap()
response["action"] = "RETRIEVE"
response["provider"] = "storage"
response["key"] = key
response["value"] = dataJson
val response = secureStorageProvider!!.retrieveData(key)
notifyClient(response)
} catch (e: JSONException) {
LOG.log(Level.SEVERE, "Failed to retrieve data", e)
Expand Down Expand Up @@ -969,26 +958,6 @@ open class OrMainActivity : Activity() {
}
}

private fun storeData(key: String?, data: String?) {
val editor = sharedPreferences.edit()
if (data == null) {
editor.remove(key)
} else {
editor.putString(key, data)
}
editor.apply()
}

private fun retrieveData(key: String?): Any? {
val str = sharedPreferences.getString(key, null) ?: return null
// Parse data JSON
return try {
mapper.readTree(str)
} catch (e: JsonProcessingException) {
str
}
}

private fun onConnectivityChanged(connectivity: Boolean) {
LOG.info("Connectivity changed: $connectivity")
if (connectivity && !webViewIsLoading && !lastConnectivity) {
Expand Down

0 comments on commit 46c5935

Please sign in to comment.