mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2025-12-23 23:17:50 -05:00
Experimenting with Keystore
Signed-off-by: Arnau Mora <arnyminerz@proton.me>
This commit is contained in:
@@ -7,6 +7,7 @@ package at.bitfire.davdroid.webdav
|
|||||||
import at.bitfire.davdroid.db.Credentials
|
import at.bitfire.davdroid.db.Credentials
|
||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@@ -29,7 +30,7 @@ class CredentialsStoreTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSetGetDelete() {
|
fun testSetGetDelete() = runTest {
|
||||||
store.setCredentials(0, Credentials(username = "myname", password = "12345"))
|
store.setCredentials(0, Credentials(username = "myname", password = "12345"))
|
||||||
assertEquals(Credentials(username = "myname", password = "12345"), store.getCredentials(0))
|
assertEquals(Credentials(username = "myname", password = "12345"), store.getCredentials(0))
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,182 @@
|
|||||||
package at.bitfire.davdroid.webdav
|
package at.bitfire.davdroid.webdav
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
import androidx.annotation.StringDef
|
import androidx.annotation.StringDef
|
||||||
import androidx.core.content.edit
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.datastore.preferences.core.MutablePreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import at.bitfire.davdroid.db.Credentials
|
import at.bitfire.davdroid.db.Credentials
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import java.security.Key
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
class CredentialsStore @Inject constructor(
|
class CredentialsStore @Inject constructor(
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext context: Context
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "credentials")
|
||||||
|
private val dataStore by lazy { context.dataStore }
|
||||||
|
|
||||||
|
val ks: KeyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generate a new EC key pair entry in the Android Keystore by
|
||||||
|
* using the KeyPairGenerator API. The private key can only be
|
||||||
|
* used for signing or verification and only with SHA-256 or
|
||||||
|
* SHA-512 as the message digest.
|
||||||
|
*/
|
||||||
|
private fun generateSecretKey(alias: String): SecretKey {
|
||||||
|
val keyEntry = ks.getEntry(alias, null)
|
||||||
|
if (keyEntry == null) {
|
||||||
|
val kg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
|
||||||
|
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
|
||||||
|
alias,
|
||||||
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||||
|
).run {
|
||||||
|
setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||||
|
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
kg.init(parameterSpec)
|
||||||
|
|
||||||
|
ks.setKeyEntry(alias, kg.generateKey(), null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSecretKey(alias) ?: error("There was an error while generating the key")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSecretKey(alias: String): SecretKey? {
|
||||||
|
return ks.getKey(alias, null) as SecretKey?
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encrypt(key: Key, data: String): Pair<ByteArray, ByteArray> {
|
||||||
|
return Cipher.getInstance(AES_MODE).apply {
|
||||||
|
init(Cipher.ENCRYPT_MODE, key)
|
||||||
|
}.let { it.doFinal(data.encodeToByteArray()) to it.iv }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decrypt(key: Key, encryptedData: ByteArray, iv: ByteArray): String {
|
||||||
|
return Cipher.getInstance(AES_MODE).apply {
|
||||||
|
init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))
|
||||||
|
}.doFinal(encryptedData).decodeToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCredentials(mountId: Long): Credentials? {
|
||||||
|
val hasCredentialsKey = preferenceKey<Boolean>(mountId, HAS_CREDENTIALS)
|
||||||
|
val userNameKey = preferenceKey<String>(mountId, USER_NAME)
|
||||||
|
val passwordKey = preferenceKey<String>(mountId, PASSWORD)
|
||||||
|
val cerAliasKey = preferenceKey<String>(mountId, CERTIFICATE_ALIAS)
|
||||||
|
|
||||||
|
val data = dataStore.data.first()
|
||||||
|
if (data[hasCredentialsKey] != true) return null
|
||||||
|
|
||||||
|
val key = getSecretKey(mountId.toString())
|
||||||
|
checkNotNull(key) { "Could not find any key for mount $mountId" }
|
||||||
|
|
||||||
|
val username = data.getWithIV(userNameKey)?.let { (value, iv) -> decrypt(key, value.encodeToByteArray(), iv) }
|
||||||
|
val password = data.getWithIV(passwordKey)?.let { (value, iv) -> decrypt(key, value.encodeToByteArray(), iv) }
|
||||||
|
val cerAlias = data.getWithIV(cerAliasKey)?.let { (value, iv) -> decrypt(key, value.encodeToByteArray(), iv) }
|
||||||
|
|
||||||
|
return Credentials(username, password, cerAlias)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setCredentials(mountId: Long, credentials: Credentials?) {
|
||||||
|
val alias = mountId.toString()
|
||||||
|
val key = getSecretKey(alias) ?: generateSecretKey(alias)
|
||||||
|
|
||||||
|
dataStore.edit { pref ->
|
||||||
|
val hasCredentials = preferenceKey<Boolean>(mountId, HAS_CREDENTIALS)
|
||||||
|
val userNameKey = preferenceKey<String>(mountId, USER_NAME)
|
||||||
|
val passwordKey = preferenceKey<String>(mountId, PASSWORD)
|
||||||
|
val cerAliasKey = preferenceKey<String>(mountId, CERTIFICATE_ALIAS)
|
||||||
|
|
||||||
|
if (credentials == null) {
|
||||||
|
pref.remove(hasCredentials)
|
||||||
|
pref.remove(userNameKey)
|
||||||
|
pref.remove(userNameKey.iv())
|
||||||
|
pref.remove(passwordKey)
|
||||||
|
pref.remove(passwordKey.iv())
|
||||||
|
pref.remove(cerAliasKey)
|
||||||
|
pref.remove(cerAliasKey.iv())
|
||||||
|
} else {
|
||||||
|
check(credentials.username != null || credentials.password != null || credentials.certificateAlias != null) {
|
||||||
|
"Credentials given are all-null"
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials.username?.let { username ->
|
||||||
|
val (data, iv) = encrypt(key, username)
|
||||||
|
pref.setWithIV(userNameKey, data.decodeToString(), iv)
|
||||||
|
}
|
||||||
|
credentials.password?.let { password ->
|
||||||
|
val (data, iv) = encrypt(key, password)
|
||||||
|
pref.setWithIV(passwordKey, data.decodeToString(), iv)
|
||||||
|
}
|
||||||
|
credentials.certificateAlias?.let { certificateAlias ->
|
||||||
|
val (data, iv) = encrypt(key, certificateAlias)
|
||||||
|
pref.setWithIV(cerAliasKey, data.decodeToString(), iv)
|
||||||
|
}
|
||||||
|
|
||||||
|
pref[hasCredentials] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <Type : Any> preferenceKeyFromGeneric(keyName: String, type: KClass<Type>): Preferences.Key<Type> {
|
||||||
|
val key = when (type) {
|
||||||
|
String::class -> stringPreferencesKey(keyName)
|
||||||
|
Boolean::class -> booleanPreferencesKey(keyName)
|
||||||
|
else -> error("Got unsupported type ${type.simpleName}")
|
||||||
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return key as Preferences.Key<Type>
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <Type : Any> preferenceKey(
|
||||||
|
mountId: Long,
|
||||||
|
@KeyName name: String,
|
||||||
|
suffix: String = "",
|
||||||
|
type: KClass<Type>,
|
||||||
|
): Preferences.Key<Type> {
|
||||||
|
val keyName = "$mountId.$name$suffix"
|
||||||
|
return preferenceKeyFromGeneric(keyName, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified Type : Any> preferenceKey(
|
||||||
|
mountId: Long,
|
||||||
|
@KeyName name: String,
|
||||||
|
suffix: String = "",
|
||||||
|
): Preferences.Key<Type> = preferenceKey(mountId, name, suffix, Type::class)
|
||||||
|
|
||||||
|
private fun <Type : Any> Preferences.Key<Type>.iv(): Preferences.Key<String> {
|
||||||
|
return preferenceKeyFromGeneric("$name.iv", String::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified Type : Any> MutablePreferences.setWithIV(key: Preferences.Key<Type>, value: Type, iv: ByteArray) {
|
||||||
|
set(key, value)
|
||||||
|
set(key.iv(), iv.decodeToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified Type : Any> Preferences.getWithIV(key: Preferences.Key<Type>): Pair<Type, ByteArray>? {
|
||||||
|
val value = get(key)
|
||||||
|
val iv = get(key.iv())?.encodeToByteArray()
|
||||||
|
return if (value != null && iv != null) value to iv
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
@StringDef(
|
@StringDef(
|
||||||
HAS_CREDENTIALS,
|
HAS_CREDENTIALS,
|
||||||
@@ -31,44 +195,10 @@ class CredentialsStore @Inject constructor(
|
|||||||
const val USER_NAME = "user_name"
|
const val USER_NAME = "user_name"
|
||||||
const val PASSWORD = "password"
|
const val PASSWORD = "password"
|
||||||
const val CERTIFICATE_ALIAS = "certificate_alias"
|
const val CERTIFICATE_ALIAS = "certificate_alias"
|
||||||
|
|
||||||
|
const val KEYSTORE_PROVIDER = "AndroidKeyStore"
|
||||||
|
|
||||||
|
const val AES_MODE = KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
}
|
}
|
||||||
|
|
||||||
private val masterKey = MasterKey.Builder(context)
|
}
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
||||||
.build()
|
|
||||||
private val prefs = EncryptedSharedPreferences.create(context, "webdav_credentials", masterKey,
|
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)
|
|
||||||
|
|
||||||
|
|
||||||
fun getCredentials(mountId: Long): Credentials? {
|
|
||||||
if (!prefs.getBoolean(keyName(mountId, HAS_CREDENTIALS), false))
|
|
||||||
return null
|
|
||||||
|
|
||||||
return Credentials(
|
|
||||||
prefs.getString(keyName(mountId, USER_NAME), null),
|
|
||||||
prefs.getString(keyName(mountId, PASSWORD), null),
|
|
||||||
prefs.getString(keyName(mountId, CERTIFICATE_ALIAS), null)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCredentials(mountId: Long, credentials: Credentials?) {
|
|
||||||
prefs.edit {
|
|
||||||
if (credentials != null)
|
|
||||||
putBoolean(keyName(mountId, HAS_CREDENTIALS), true)
|
|
||||||
.putString(keyName(mountId, USER_NAME), credentials.username)
|
|
||||||
.putString(keyName(mountId, PASSWORD), credentials.password)
|
|
||||||
.putString(keyName(mountId, CERTIFICATE_ALIAS), credentials.certificateAlias)
|
|
||||||
else
|
|
||||||
remove(keyName(mountId, HAS_CREDENTIALS))
|
|
||||||
.remove(keyName(mountId, USER_NAME))
|
|
||||||
.remove(keyName(mountId, PASSWORD))
|
|
||||||
.remove(keyName(mountId, CERTIFICATE_ALIAS))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun keyName(mountId: Long, @KeyName name: String) =
|
|
||||||
"$mountId.$name"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
@@ -765,7 +766,7 @@ class DavDocumentsProvider(
|
|||||||
* @param mountId ID of the mount to access
|
* @param mountId ID of the mount to access
|
||||||
* @param logBody whether to log the body of HTTP requests (disable for potentially large files)
|
* @param logBody whether to log the body of HTTP requests (disable for potentially large files)
|
||||||
*/
|
*/
|
||||||
internal fun httpClient(mountId: Long, logBody: Boolean = true): HttpClient {
|
internal suspend fun httpClient(mountId: Long, logBody: Boolean = true): HttpClient = withContext(Dispatchers.IO) {
|
||||||
val builder = httpClientBuilder.get()
|
val builder = httpClientBuilder.get()
|
||||||
.loggerInterceptorLevel(if (logBody) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.HEADERS)
|
.loggerInterceptorLevel(if (logBody) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.HEADERS)
|
||||||
.setCookieStore(
|
.setCookieStore(
|
||||||
@@ -776,7 +777,7 @@ class DavDocumentsProvider(
|
|||||||
builder.authenticate(host = null, credentials = credentials)
|
builder.authenticate(host = null, credentials = credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.build()
|
builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun notifyFolderChanged(parentDocumentId: Long?) {
|
internal fun notifyFolderChanged(parentDocumentId: Long?) {
|
||||||
|
|||||||
Reference in New Issue
Block a user