Skip to content

Commit

Permalink
Implement L2 encryption on secure storage (#1980)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1202210732792279/f

Description

This PR adds the L2 encryption for passwords in the secure storage.

What's not yet included:

expiring sessions / passwords
user authentication on secure storage

Steps to test this PR

Use autofill [Make sure to clear storage before you install this changes)

  Open a website with login
  Login and save password in autofill
  Open the autofill manage screen
  Check if password shown is correct
  • Loading branch information
karlenDimla authored Jun 17, 2022
1 parent dc8803c commit c11bc96
Show file tree
Hide file tree
Showing 15 changed files with 564 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import dagger.SingleInstanceIn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import timber.log.Timber

class SecureStoreBackedAutofillStore(val secureStorage: SecureStorage) : AutofillStore {
Expand All @@ -43,7 +41,10 @@ class SecureStoreBackedAutofillStore(val secureStorage: SecureStorage) : Autofil
return storedCredentials.map { it.toLoginCredentials() }
}

override suspend fun saveCredentials(rawUrl: String, credentials: LoginCredentials) {
override suspend fun saveCredentials(
rawUrl: String,
credentials: LoginCredentials
) {
val url = rawUrl.extractSchemeAndDomain()
if (url == null) {
Timber.w("Cannot save credentials as given url was in an unexpected format. Original url: %s", rawUrl)
Expand All @@ -55,30 +56,22 @@ class SecureStoreBackedAutofillStore(val secureStorage: SecureStorage) : Autofil
val loginDetails = WebsiteLoginDetails(domain = url, username = credentials.username)
val webSiteLoginCredentials = WebsiteLoginCredentials(loginDetails, password = credentials.password)

// todo this should be handled internally by secure storage
withContext(Dispatchers.IO) {
secureStorage.addWebsiteLoginCredential(webSiteLoginCredentials)
}
secureStorage.addWebsiteLoginCredential(webSiteLoginCredentials)
}

override suspend fun getAllCredentials(): Flow<List<LoginCredentials>> = withContext(Dispatchers.IO) {
val savedCredentials = secureStorage.websiteLoginCredentials()
override suspend fun getAllCredentials(): Flow<List<LoginCredentials>> {
return secureStorage.websiteLoginCredentials()
.map { list ->
list.map { it.toLoginCredentials() }
}
return@withContext savedCredentials
}

override suspend fun deleteCredentials(id: Int) {
withContext(Dispatchers.IO) {
secureStorage.deleteWebsiteLoginCredentials(id)
}
secureStorage.deleteWebsiteLoginCredentials(id)
}

override suspend fun updateCredentials(credentials: LoginCredentials) {
withContext(Dispatchers.IO) {
secureStorage.updateWebsiteLoginCredentials(credentials.toWebsiteLoginCredentials())
}
secureStorage.updateWebsiteLoginCredentials(credentials.toWebsiteLoginCredentials())
}

private fun WebsiteLoginCredentials.toLoginCredentials(): LoginCredentials {
Expand All @@ -96,7 +89,6 @@ class SecureStoreBackedAutofillStore(val secureStorage: SecureStorage) : Autofil
password = password
)
}

}

@Module
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ interface SecureStorage {
* This method adds a raw plaintext [WebsiteLoginCredentials] into the [SecureStorage]. This requires the user to be authenticated
* first see [authenticateUser].
*
* @throws UserNotAuthenticatedException if the user is not authenticated when calling this method
* @throws [SecureStorageException] if something went wrong while trying to perform the action. See type to get more info on the cause.
*/
@Throws(UserNotAuthenticatedException::class)
@Throws(SecureStorageException::class)
suspend fun addWebsiteLoginCredential(websiteLoginCredentials: WebsiteLoginCredentials)

/**
Expand All @@ -74,9 +74,9 @@ interface SecureStorage {
* This requires the user to be authenticated.
*
* @return [WebsiteLoginCredentials] containing the plaintext password
* @throws [UserNotAuthenticatedException] if the user is not authenticated when calling this method
* @throws [SecureStorageException] if something went wrong while trying to perform the action. See type to get more info on the cause.
*/
@Throws(UserNotAuthenticatedException::class)
@Throws(SecureStorageException::class)
suspend fun getWebsiteLoginCredentials(id: Int): WebsiteLoginCredentials

/**
Expand All @@ -85,9 +85,9 @@ interface SecureStorage {
*
* @return Flow<List<WebsiteLoginCredentials>> a flow emitting a List of plain text WebsiteLoginCredentials stored in SecureStorage
* containing the plaintext password
* @throws [UserNotAuthenticatedException] if the user is not authenticated when calling this method
* @throws [SecureStorageException] if something went wrong while trying to perform the action. See type to get more info on the cause.
*/
@Throws(UserNotAuthenticatedException::class)
@Throws(SecureStorageException::class)
suspend fun websiteLoginCredentialsForDomain(domain: String): Flow<List<WebsiteLoginCredentials>>

/**
Expand All @@ -96,18 +96,18 @@ interface SecureStorage {
*
* @return Flow<List<WebsiteLoginCredentials>> a flow emitting a List of plain text WebsiteLoginCredentials stored in SecureStorage
* containing the plaintext password
* @throws [UserNotAuthenticatedException] if the user is not authenticated when calling this method
* @throws [SecureStorageException] if something went wrong while trying to perform the action. See type to get more info on the cause.
*/
@Throws(UserNotAuthenticatedException::class)
@Throws(SecureStorageException::class)
suspend fun websiteLoginCredentials(): Flow<List<WebsiteLoginCredentials>>

/**
* This method updates an existing [WebsiteLoginCredentials] in the [SecureStorage].
* This requires the user to be authenticated.
*
* @throws [UserNotAuthenticatedException] if the user is not authenticated when calling this method
* @throws [SecureStorageException] if something went wrong while trying to perform the action. See type to get more info on the cause.
*/
@Throws(UserNotAuthenticatedException::class)
@Throws(SecureStorageException::class)
suspend fun updateWebsiteLoginCredentials(websiteLoginCredentials: WebsiteLoginCredentials)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@

package com.duckduckgo.securestorage.api

import java.lang.RuntimeException
import java.lang.Exception

/**
* Public data class exception that is thrown when a method that requires user authentication is accessed by a
* non authenticated user.
*/
data class UserNotAuthenticatedException(override val message: String?) : RuntimeException()
sealed class SecureStorageException : Exception() {
/**
* Public data class exception that is thrown when a method that requires user authentication is accessed by a
* non authenticated user.
*/
data class UserNotAuthenticatedException(override val message: String) : SecureStorageException()
data class InternalSecureStorageException(
override val message: String,
override val cause: Throwable? = null
) : SecureStorageException()
}
2 changes: 2 additions & 0 deletions secure-storage/secure-storage-impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ apply from: "$rootProject.projectDir/gradle/android-library.gradle"

dependencies {
implementation project(path: ':appbuildconfig-api')
implementation project(path: ':common')
implementation project(path: ':di')
implementation project(path: ':secure-storage-api')
implementation project(path: ':secure-storage-store')

implementation AndroidX.room.ktx
implementation Google.dagger
implementation KotlinX.coroutines.core
implementation Square.okio

implementation "net.zetetic:android-database-sqlcipher:_"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2022 DuckDuckGo
*
* 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.
*/

package com.duckduckgo.securestorage.impl

import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.securestorage.impl.encryption.EncryptionHelper
import com.duckduckgo.securestorage.impl.encryption.EncryptionHelper.EncryptedString
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject

interface L2DataTransformer {
fun canProcessData(): Boolean

fun encrypt(data: String): EncryptedString

fun decrypt(
data: String,
iv: String
): String
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
class RealL2DataTransformer @Inject constructor(
private val encryptionHelper: EncryptionHelper,
private val secureStorageKeyProvider: SecureStorageKeyProvider
) : L2DataTransformer {
private val l2Key by lazy {
secureStorageKeyProvider.getl2Key()
}

override fun canProcessData(): Boolean = secureStorageKeyProvider.canAccessKeyStore()

override fun encrypt(data: String): EncryptedString = encryptionHelper.encrypt(data, l2Key)

override fun decrypt(
data: String,
iv: String
): String = encryptionHelper.decrypt(
EncryptedString(
data = data,
iv = iv
),
l2Key
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,92 +16,116 @@

package com.duckduckgo.securestorage.impl

import com.duckduckgo.app.global.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.securestorage.api.Result
import com.duckduckgo.securestorage.api.SecureStorage
import com.duckduckgo.securestorage.api.WebsiteLoginCredentials
import com.duckduckgo.securestorage.api.WebsiteLoginDetails
import com.duckduckgo.securestorage.impl.encryption.EncryptionHelper.EncryptedString
import com.duckduckgo.securestorage.store.SecureStorageRepository
import com.duckduckgo.securestorage.store.db.WebsiteLoginCredentialsEntity
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import javax.inject.Inject

@ContributesBinding(AppScope::class)
class RealSecureStorage @Inject constructor(
private val secureStorageRepository: SecureStorageRepository
private val secureStorageRepository: SecureStorageRepository,
private val dispatchers: DispatcherProvider,
private val l2DataTransformer: L2DataTransformer
) : SecureStorage {

override fun canAccessSecureStorage(): Boolean = true
override fun canAccessSecureStorage(): Boolean = l2DataTransformer.canProcessData()

override suspend fun authenticateUser(): Result {
// TODO (karl) Implement authentication. This is only relevant for L2. Note the expiry here will change once implemented.
return Result.Success(expiryInMillis = System.currentTimeMillis() + DEFAULT_EXPIRY_IN_MILLIS)
return withContext(dispatchers.io()) {
Result.Success(expiryInMillis = System.currentTimeMillis() + DEFAULT_EXPIRY_IN_MILLIS)
}
}

override suspend fun authenticateUser(password: String): Result {
// TODO (karl) Implement authentication. This is only relevant for L2. Note the expiry here will change once implemented.
return Result.Success(expiryInMillis = System.currentTimeMillis() + DEFAULT_EXPIRY_IN_MILLIS)
return withContext(dispatchers.io()) {
Result.Success(expiryInMillis = System.currentTimeMillis() + DEFAULT_EXPIRY_IN_MILLIS)
}
}

override suspend fun addWebsiteLoginCredential(websiteLoginCredentials: WebsiteLoginCredentials) {
// TODO (karl) Integrate L2 encryption
secureStorageRepository.addWebsiteLoginCredential(websiteLoginCredentials.toDataEntity())
withContext(dispatchers.io()) {
secureStorageRepository.addWebsiteLoginCredential(websiteLoginCredentials.toDataEntity())
}
}

override suspend fun websiteLoginDetailsForDomain(domain: String): Flow<List<WebsiteLoginDetails>> =
secureStorageRepository.websiteLoginCredentialsForDomain(domain).map { list ->
list.map {
it.toDetails()
withContext(dispatchers.io()) {
secureStorageRepository.websiteLoginCredentialsForDomain(domain).map { list ->
list.map {
it.toDetails()
}
}
}

override suspend fun websiteLoginDetails(): Flow<List<WebsiteLoginDetails>> =
secureStorageRepository.websiteLoginCredentials().map { list ->
list.map {
it.toDetails()
withContext(dispatchers.io()) {
secureStorageRepository.websiteLoginCredentials().map { list ->
list.map {
it.toDetails()
}
}
}

override suspend fun getWebsiteLoginCredentials(id: Int): WebsiteLoginCredentials =
secureStorageRepository.getWebsiteLoginCredentialsForId(id).toCredentials()
withContext(dispatchers.io()) {
secureStorageRepository.getWebsiteLoginCredentialsForId(id).toCredentials()
}

override suspend fun websiteLoginCredentialsForDomain(domain: String): Flow<List<WebsiteLoginCredentials>> =
// TODO (karl) Integrate L2 encryption
secureStorageRepository.websiteLoginCredentialsForDomain(domain).map { list ->
list.map {
it.toCredentials()
withContext(dispatchers.io()) {
secureStorageRepository.websiteLoginCredentialsForDomain(domain).map { list ->
list.map {
it.toCredentials()
}
}
}

override suspend fun websiteLoginCredentials(): Flow<List<WebsiteLoginCredentials>> =
// TODO (karl) Integrate L2 encryption
secureStorageRepository.websiteLoginCredentials().map { list ->
list.map {
it.toCredentials()
withContext(dispatchers.io()) {
secureStorageRepository.websiteLoginCredentials().map { list ->
list.map {
it.toCredentials()
}
}
}

override suspend fun updateWebsiteLoginCredentials(websiteLoginCredentials: WebsiteLoginCredentials) =
// TODO (karl) Integrate L2 encryption
secureStorageRepository.updateWebsiteLoginCredentials(websiteLoginCredentials.toDataEntity())
withContext(dispatchers.io()) {
secureStorageRepository.updateWebsiteLoginCredentials(websiteLoginCredentials.toDataEntity())
}

override suspend fun deleteWebsiteLoginCredentials(id: Int) =
secureStorageRepository.deleteWebsiteLoginCredentials(id)
withContext(dispatchers.io()) {
secureStorageRepository.deleteWebsiteLoginCredentials(id)
}

private fun WebsiteLoginCredentials.toDataEntity(): WebsiteLoginCredentialsEntity =
WebsiteLoginCredentialsEntity(
private fun WebsiteLoginCredentials.toDataEntity(): WebsiteLoginCredentialsEntity {
val encryptedData = encryptData(password)
return WebsiteLoginCredentialsEntity(
id = details.id ?: 0,
domain = details.domain,
username = details.username,
password = password
password = encryptedData?.data,
iv = encryptedData?.iv
)
}

private fun WebsiteLoginCredentialsEntity.toCredentials(): WebsiteLoginCredentials =
WebsiteLoginCredentials(
details = toDetails(),
password = password
password = decryptData(password, iv)
)

private fun WebsiteLoginCredentialsEntity.toDetails(): WebsiteLoginDetails =
Expand All @@ -111,6 +135,21 @@ class RealSecureStorage @Inject constructor(
id = id
)

// only encrypt when there's data
private fun encryptData(data: String?): EncryptedString? = data?.let { l2DataTransformer.encrypt(it) }

private fun decryptData(
data: String?,
iv: String?
): String? {
// only decrypt when there's data and iv
return data?.let { _data ->
iv?.let { _iv ->
l2DataTransformer.decrypt(_data, _iv)
}
}
}

companion object {
private const val DEFAULT_EXPIRY_IN_MILLIS = 30 * 60 * 1000
}
Expand Down
Loading

0 comments on commit c11bc96

Please sign in to comment.