Skip to content

Commit

Permalink
feat: secure credentials using credentials-storage-kt (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
y9vad9 authored Jan 7, 2024
1 parent a523a00 commit ded6c0d
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 26 deletions.
2 changes: 2 additions & 0 deletions feature/authorization/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ dependencies {
commonMainImplementation(projects.feature.authorization.domain)
commonMainImplementation(projects.feature.authorization.data.database)

commonMainImplementation(libs.timemates.credentials.manager)

commonMainImplementation(libs.timemates.sdk)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
CREATE TABLE Authorization (
id INTEGER PRIMARY KEY NOT NULL,
accessHashValue TEXT NOT NULL,
accessHashExpiresAt INTEGER NOT NULL,
refreshHashValue TEXT NOT NULL,
refreshHashExpiresAt INTEGER NOT NULL,
generationTime INTEGER NOT NULL,
metadataClientName TEXT,
Expand All @@ -14,9 +12,7 @@ CREATE TABLE Authorization (

update:
UPDATE Authorization
SET accessHashValue = :accessHashValue,
accessHashExpiresAt = :accessHashExpiresAt,
refreshHashValue = :refreshHashValue,
SET accessHashExpiresAt = :accessHashExpiresAt,
refreshHashExpiresAt = :refreshHashExpiresAt,
generationTime = :generationTime,
metadataClientName = :metadataClientName,
Expand Down Expand Up @@ -44,5 +40,5 @@ FROM Authorization
WHERE isCurrent;

add:
INSERT INTO Authorization (id, accessHashValue, accessHashExpiresAt, refreshHashValue, refreshHashExpiresAt, generationTime, metadataClientName, metadataClientVersion, metadataClientIpAddress, userId, isCurrent)
VALUES (:id, :accessHashValue, :accessHashExpiresAt, :refreshHashValue, :refreshHashExpiresAt, :generationTime, :metadataClientName, :metadataClientVersion, :metadataClientIpAddress, :userId, TRUE);
INSERT INTO Authorization (id, accessHashExpiresAt, refreshHashExpiresAt, generationTime, metadataClientName, metadataClientVersion, metadataClientIpAddress, userId, isCurrent)
VALUES (:id, :accessHashExpiresAt, :refreshHashExpiresAt, :generationTime, :metadataClientName, :metadataClientVersion, :metadataClientIpAddress, :userId, TRUE);
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.timemates.app.authorization.data

import io.timemates.app.authorization.data.database.AccountDatabaseQueries
import io.timemates.credentials.CredentialsStorage
import io.timemates.sdk.authorization.email.EmailAuthorizationApi
import io.timemates.sdk.authorization.email.requests.ConfigureNewAccountRequest
import io.timemates.sdk.authorization.email.requests.ConfirmAuthorizationRequest
Expand All @@ -23,6 +24,7 @@ class AuthorizationsRepository(
private val sessionsApi: AuthorizedSessionsApi,
private val localQueries: AccountDatabaseQueries,
private val mapper: DbAuthorizationMapper,
private val credentialsStorage: CredentialsStorage,
) : AuthorizationRepositoryContract {
override suspend fun getCurrentAuthorization(): Authorization? {
return localQueries.getCurrent().executeAsOneOrNull()
Expand All @@ -44,16 +46,19 @@ class AuthorizationsRepository(
val auth = it.authorization!!
localQueries.add(
userId = auth.userId.long,
accessHashValue = auth.accessHash!!.value.string,
accessHashExpiresAt = auth.accessHash!!.expiresAt.toEpochMilliseconds(),
refreshHashValue = auth.refreshHash!!.value.string,
refreshHashExpiresAt = auth.refreshHash!!.expiresAt.toEpochMilliseconds(),
generationTime = auth.generationTime.toEpochMilliseconds(),
metadataClientName = auth.metadata?.applicationName?.string,
metadataClientIpAddress = auth.metadata?.clientIpAddress?.string,
metadataClientVersion = auth.metadata?.clientVersion?.double ?: 1.0,
id = null,
)

val id = localQueries.getCurrent().executeAsOne().id

credentialsStorage.setString("access_hash_$id", auth.accessHash!!.value.string)
credentialsStorage.setString("refresh_hash_$id", auth.refreshHash!!.value.string)
}
}
}
Expand All @@ -68,16 +73,19 @@ class AuthorizationsRepository(
with(result.authorization) {
localQueries.add(
null,
accessHashValue = accessHash!!.value.string,
accessHashExpiresAt = accessHash!!.expiresAt.toEpochMilliseconds(),
refreshHashValue = refreshHash!!.value.string,
refreshHashExpiresAt = refreshHash!!.expiresAt.toEpochMilliseconds(),
generationTime = generationTime.toEpochMilliseconds(),
metadataClientName = metadata?.applicationName?.string,
metadataClientIpAddress = metadata?.clientIpAddress?.string,
metadataClientVersion = metadata?.clientVersion!!.double,
userId = userId.long,
)

val id = localQueries.getCurrent().executeAsOne().id

credentialsStorage.setString("access_hash_$id", accessHash!!.value.string)
credentialsStorage.setString("refresh_hash_$id", refreshHash!!.value.string)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package io.timemates.app.authorization.data

import io.timemates.app.authorization.data.database.AccountDatabaseQueries
import io.timemates.credentials.CredentialsStorage
import io.timemates.sdk.authorization.types.value.AccessHash
import io.timemates.sdk.common.constructor.createOrThrow
import io.timemates.sdk.common.exceptions.UnauthorizedException
import io.timemates.sdk.common.providers.AccessHashProvider

class DatabaseAccessHashProvider(private val localQueries: AccountDatabaseQueries) : AccessHashProvider {
class DatabaseAccessHashProvider(
private val localQueries: AccountDatabaseQueries,
private val credentialsStorage: CredentialsStorage,
) : AccessHashProvider {
override suspend fun getOrNull(): AccessHash? {
// TODO cache in memory
return localQueries.getCurrent().executeAsOneOrNull()
?.let { AccessHash.createOrThrow(it.accessHashValue) }
?.let {
AccessHash.createOrThrow(
credentialsStorage.getString("access_hash_${it.id}")
?: throw UnauthorizedException("Authorization wasn't saved to system credentials.")
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
package io.timemates.app.authorization.data

import io.timemates.credentials.CredentialsStorage
import io.timemates.sdk.authorization.sessions.types.Authorization
import io.timemates.sdk.authorization.types.value.HashValue
import io.timemates.sdk.common.constructor.createOrThrow
import io.timemates.sdk.common.exceptions.UnauthorizedException
import io.timemates.sdk.users.profile.types.value.UserId
import kotlinx.datetime.Instant
import io.timemates.app.authorization.data.database.Authorization as DbAuthorization

class DbAuthorizationMapper {
fun dbToSdkAuthorization(dbAuthorization: DbAuthorization): Authorization = with(dbAuthorization) {
class DbAuthorizationMapper(
private val credentialsStorage: CredentialsStorage,
) {
fun dbToSdkAuthorization(
dbAuthorization: DbAuthorization,
): Authorization = with(dbAuthorization) {
return@with Authorization(
accessHash = Authorization.Hash(
HashValue.createOrThrow(accessHashValue),
HashValue.createOrThrow(credentialsStorage.getString("access_hash_$id")
?: throw UnauthorizedException("Authorization wasn't saved to system credentials.")),
Instant.fromEpochMilliseconds(accessHashExpiresAt),
),
refreshHash = Authorization.Hash(
HashValue.createOrThrow(accessHashValue),
HashValue.createOrThrow(credentialsStorage.getString("refresh_hash_$id")
?: throw UnauthorizedException("Authorization wasn't saved to system credentials.")),
Instant.fromEpochMilliseconds(accessHashExpiresAt),
),
generationTime = Instant.fromEpochMilliseconds(generationTime),
Expand Down
2 changes: 2 additions & 0 deletions feature/authorization/dependencies/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ dependencies {
commonMainImplementation(projects.feature.authorization.data.database)

commonMainImplementation(projects.feature.common.domain)

commonMainImplementation(libs.timemates.credentials.manager)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import app.cash.sqldelight.db.SqlDriver
import io.timemates.app.authorization.data.DatabaseAccessHashProvider
import io.timemates.app.authorization.data.DbAuthorizationMapper
import io.timemates.app.authorization.repositories.AuthorizationsRepository
import io.timemates.credentials.CredentialsStorage
import io.timemates.data.database.TimeMatesAuthorizations
import io.timemates.sdk.authorization.email.EmailAuthorizationApi
import io.timemates.sdk.authorization.sessions.AuthorizedSessionsApi
Expand All @@ -23,21 +24,26 @@ class AuthorizationDataModule {
}

@Factory
fun accessHashProvider(dbAuthorizations: TimeMatesAuthorizations): AccessHashProvider {
return DatabaseAccessHashProvider(dbAuthorizations.accountDatabaseQueries)
fun accessHashProvider(
dbAuthorizations: TimeMatesAuthorizations,
credentialsStorage: CredentialsStorage,
): AccessHashProvider {
return DatabaseAccessHashProvider(dbAuthorizations.accountDatabaseQueries, credentialsStorage)
}

@Factory
fun authorizationRepository(
requestsEngine: TimeMatesRequestsEngine,
accessHashProvider: AccessHashProvider,
dbAuthorizations: TimeMatesAuthorizations,
credentialsStorage: CredentialsStorage,
): AuthorizationsRepository {
return AuthorizationsRepositoryImpl(
emailAuthApi = EmailAuthorizationApi(requestsEngine),
sessionsApi = AuthorizedSessionsApi(requestsEngine, accessHashProvider),
localQueries = dbAuthorizations.accountDatabaseQueries,
mapper = DbAuthorizationMapper(),
mapper = DbAuthorizationMapper(credentialsStorage),
credentialsStorage = credentialsStorage,
)
}
}
5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
material = "1.11.0"
credentials = "1.0.0"
securityCryptoKtx = "1.1.0-alpha06"

[libraries]
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
Expand Down Expand Up @@ -75,6 +77,8 @@ androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", ve

timemates-sdk = { module = "io.timemates:sdk", version.ref = "timeMatesSdk" }
timemates-engine-rsocket = { module = "io.timemates:rsocket-engine", version.ref = "timeMatesSdk" }
timemates-credentials-manager = { module = "io.timemates.credentials:credentials-manager", version.ref = "credentials" }
timemates-credentials-manager-android = { module = "io.timemates.credentials:credentials-manager-android", version.ref = "credentials" }

koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-annotations" }
Expand Down Expand Up @@ -110,6 +114,7 @@ material = { group = "com.google.android.material", name = "material", version.r
mockk = { group = "io.mockk", name = "mockk", version.require = "1.13.5" }

libres-compose = { module = "io.github.skeptick.libres:libres-compose", version.require = "1.2.2" }
androidx-security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" }

[plugins]
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Expand Down
1 change: 1 addition & 0 deletions platforms/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ dependencies {
implementation(libs.androidx.compose.activity)

implementation(projects.platforms.common)
implementation(libs.androidx.security.crypto.ktx)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import androidx.multidex.MultiDexApplication
import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import io.timemates.app.common.initializeAppDependencies
import io.timemates.app.credentials.AndroidEncryptedPrefsCredentials
import io.timemates.app.foundation.time.SystemUTCTimeProvider
import io.timemates.app.users.data.database.TimeMatesUsers
import io.timemates.credentials.CredentialsStorage
import io.timemates.data.database.TimeMatesAuthorizations
import io.timemates.sdk.common.exceptions.UnauthorizedException
import kotlinx.coroutines.channels.Channel
Expand All @@ -33,11 +35,14 @@ class TimeMatesApplication : MultiDexApplication() {
)
}

val credentialsStorage: CredentialsStorage = AndroidEncryptedPrefsCredentials(applicationContext)

initializeAppDependencies(
SystemUTCTimeProvider(),
onAuthFailedChannel,
authDriver,
usersDriver,
credentialsStorage,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.timemates.app.credentials

import android.content.Context
import android.content.SharedPreferences
import io.timemates.credentials.CredentialsStorage

/**
* Android encrypted shared preferences implementation of credentials manager
*
* TODO: remove when https://github.com/vanniktech/gradle-maven-publish-plugin/issues/705 is fixed and we will be able to use library for android target.
*
* @param context Application context.
*
* @suppress Warning! In robolectric test encrypting is disabled
* @see createSharedPreferences
*/
class AndroidEncryptedPrefsCredentials(
context: Context,
fileName: String = DEFAULT_ENCRYPTED_PREFS_FILE,
) : CredentialsStorage {
private val prefs: SharedPreferences = createSharedPreferences(context, fileName)

override fun getString(key: String): String? {
return prefs.getString(key, null)
}

override fun getInt(key: String): Int? {
return prefs.getString(key, null)?.toIntOrNull()
}

override fun getLong(key: String): Long? {
return prefs.getString(key, null)?.toLongOrNull()
}

override fun getBoolean(key: String): Boolean? {
return prefs.getString(key, null)?.toBooleanStrictOrNull()
}

override fun setString(key: String, value: String) {
prefs.edit().apply { putString(key, value) }.apply()
}

override fun setInt(key: String, value: Int) {
prefs.edit().apply { putString(key, value.toString()) }.apply()
}

override fun setLong(key: String, value: Long) {
prefs.edit().apply { putString(key, value.toString()) }.apply()
}

override fun setBoolean(key: String, value: Boolean) {
prefs.edit().apply { putString(key, value.toString()) }.apply()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.timemates.app.credentials

import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys

internal const val DEFAULT_ENCRYPTED_PREFS_FILE = "credentials_storage.txt"

/**
* Create an encrypted SharedPreferences.
*
* @param context Application context.
*
* @suppress Warning! In robolectric test encrypting is disabled
*/
internal fun createSharedPreferences(context: Context, fileName: String = DEFAULT_ENCRYPTED_PREFS_FILE): SharedPreferences {
return if(Build.FINGERPRINT.lowercase() == "robolectric") // For tests
context.getSharedPreferences("test", Context.MODE_PRIVATE)
else EncryptedSharedPreferences.create( // For app
fileName,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
2 changes: 2 additions & 0 deletions platforms/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ dependencies {
commonMainApi(projects.navigation)
commonMainApi(projects.styleSystem)

commonMainApi(libs.timemates.credentials.manager)

commonMainImplementation(projects.foundation.mvi)
commonMainImplementation(projects.foundation.mvi.koinCompose)

Expand Down
Loading

0 comments on commit ded6c0d

Please sign in to comment.