Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: secure credentials using credentials-storage-kt #115

Merged
merged 1 commit into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading