Skip to content

Commit d962ad3

Browse files
authored
PIR: Add database encryption (#7010)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211635200412099?focus=true ### Description Encrypts the PIR database. ### Steps to test this PR https://app.asana.com/1/137249556945/project/488551667048375/task/1211766506550756?focus=true ### UI changes No UI changes
1 parent d33b17c commit d962ad3

File tree

9 files changed

+729
-7
lines changed

9 files changed

+729
-7
lines changed

pir/pir-impl/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ dependencies {
5858
implementation AndroidX.work.runtimeKtx
5959
implementation "androidx.work:work-multiprocess:_"
6060

61+
// Encryption
62+
implementation "net.zetetic:sqlcipher-android:_"
63+
implementation project(':library-loader-api')
64+
implementation AndroidX.security.crypto
65+
6166
testImplementation Testing.junit4
6267
testImplementation "org.mockito.kotlin:mockito-kotlin:_"
6368
testImplementation project(path: ':common-test')

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/di/PirModule.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package com.duckduckgo.pir.impl.di
1818

19-
import android.content.Context
20-
import androidx.room.Room
2119
import com.duckduckgo.app.di.AppCoroutineScope
2220
import com.duckduckgo.common.utils.CurrentTimeProvider
2321
import com.duckduckgo.common.utils.DispatcherProvider
@@ -63,6 +61,7 @@ import com.duckduckgo.pir.impl.store.db.OptOutResultsDao
6361
import com.duckduckgo.pir.impl.store.db.ScanLogDao
6462
import com.duckduckgo.pir.impl.store.db.ScanResultsDao
6563
import com.duckduckgo.pir.impl.store.db.UserProfileDao
64+
import com.duckduckgo.pir.impl.store.secure.PirSecureStorageDatabaseFactory
6665
import com.squareup.anvil.annotations.ContributesTo
6766
import com.squareup.moshi.Moshi
6867
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
@@ -71,6 +70,7 @@ import dagger.Module
7170
import dagger.Provides
7271
import dagger.SingleInstanceIn
7372
import kotlinx.coroutines.CoroutineScope
73+
import kotlinx.coroutines.runBlocking
7474
import javax.inject.Named
7575

7676
@Module
@@ -79,11 +79,12 @@ class PirModule {
7979

8080
@SingleInstanceIn(AppScope::class)
8181
@Provides
82-
fun bindPirDatabase(context: Context): PirDatabase {
83-
return Room.databaseBuilder(context, PirDatabase::class.java, "pir.db")
84-
.enableMultiInstanceInvalidation()
85-
.fallbackToDestructiveMigration()
86-
.build()
82+
fun bindPirDatabase(
83+
databaseFactory: PirSecureStorageDatabaseFactory,
84+
): PirDatabase {
85+
return runBlocking {
86+
databaseFactory.getDatabase()
87+
} ?: throw IllegalStateException("Failed to create PIR encrypted database")
8788
}
8889

8990
@SingleInstanceIn(AppScope::class)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.pir.impl.store.secure
18+
19+
import android.content.Context
20+
import androidx.room.Room
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.duckduckgo.library.loader.LibraryLoader
23+
import com.duckduckgo.pir.impl.store.PirDatabase
24+
import com.squareup.anvil.annotations.ContributesBinding
25+
import dagger.SingleInstanceIn
26+
import kotlinx.coroutines.sync.Mutex
27+
import kotlinx.coroutines.sync.withLock
28+
import logcat.LogPriority.ERROR
29+
import logcat.asLog
30+
import logcat.logcat
31+
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
32+
import javax.inject.Inject
33+
34+
interface PirSecureStorageDatabaseFactory {
35+
suspend fun getDatabase(): PirDatabase?
36+
}
37+
38+
@SingleInstanceIn(AppScope::class)
39+
@ContributesBinding(
40+
scope = AppScope::class,
41+
boundType = PirSecureStorageDatabaseFactory::class,
42+
)
43+
class RealPirSecureStorageDatabaseFactory @Inject constructor(
44+
private val context: Context,
45+
private val keyProvider: PirSecureStorageKeyProvider,
46+
) : PirSecureStorageDatabaseFactory {
47+
private var _database: PirDatabase? = null
48+
49+
private val mutex = Mutex()
50+
51+
init {
52+
logcat { "PIR-DB: Loading the sqlcipher native library" }
53+
try {
54+
LibraryLoader.loadLibrary(context, "sqlcipher")
55+
logcat { "PIR-DB: sqlcipher native library loaded ok" }
56+
} catch (t: Throwable) {
57+
// error loading the library
58+
logcat(ERROR) { "PIR-DB: Error loading sqlcipher library: ${t.asLog()}" }
59+
}
60+
}
61+
62+
override suspend fun getDatabase(): PirDatabase? {
63+
_database?.let { return it }
64+
return mutex.withLock {
65+
getInnerDatabase()
66+
}
67+
}
68+
69+
@OptIn(ExperimentalStdlibApi::class)
70+
private suspend fun getInnerDatabase(): PirDatabase? {
71+
// If we have already the DB instance then let's use it
72+
if (_database != null) {
73+
return _database
74+
}
75+
76+
// If we can't access the keystore, it means that L1Key will be null. We don't want to encrypt the db with a null key.
77+
return if (keyProvider.canAccessKeyStore()) {
78+
// At this point, we are guaranteed that if L1key is null, it's because it hasn't been generated yet. Else, we always use the one stored.
79+
_database = Room.databaseBuilder(
80+
context,
81+
PirDatabase::class.java,
82+
"pir_encrypted.db",
83+
)
84+
.openHelperFactory(
85+
SupportOpenHelperFactory(
86+
keyProvider.getl1Key(),
87+
),
88+
)
89+
.enableMultiInstanceInvalidation()
90+
.fallbackToDestructiveMigration()
91+
.build()
92+
_database
93+
} else {
94+
logcat(ERROR) { "PIR-DB: Cannot access key store!" }
95+
null
96+
}
97+
}
98+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.pir.impl.store.secure
18+
19+
import com.duckduckgo.di.scopes.AppScope
20+
import com.squareup.anvil.annotations.ContributesBinding
21+
import kotlinx.coroutines.sync.Mutex
22+
import kotlinx.coroutines.sync.withLock
23+
import javax.inject.Inject
24+
25+
/**
26+
* This class provides the usable decrypted keys to be used for encryption
27+
*/
28+
interface PirSecureStorageKeyProvider {
29+
suspend fun canAccessKeyStore(): Boolean
30+
31+
/**
32+
* Ready to use key for L1 encryption
33+
*/
34+
suspend fun getl1Key(): ByteArray
35+
}
36+
37+
@ContributesBinding(AppScope::class)
38+
class RealPirSecureStorageKeyProvider @Inject constructor(
39+
private val randomBytesGenerator: PirRandomBytesGenerator,
40+
private val secureStorageKeyRepository: PirSecureStorageKeyRepository,
41+
) : PirSecureStorageKeyProvider {
42+
43+
override suspend fun canAccessKeyStore(): Boolean =
44+
secureStorageKeyRepository.canUseEncryption()
45+
46+
private val l1KeyMutex = Mutex()
47+
48+
override suspend fun getl1Key(): ByteArray {
49+
l1KeyMutex.withLock {
50+
return innerGetL1Key()
51+
}
52+
}
53+
54+
private suspend fun innerGetL1Key(): ByteArray {
55+
// If no key exists in the keystore, we generate a new one and store it
56+
return if (secureStorageKeyRepository.getL1Key() == null) {
57+
randomBytesGenerator.generateBytes(L1_PASSPHRASE_SIZE).also {
58+
secureStorageKeyRepository.setL1Key(it)
59+
}
60+
} else {
61+
secureStorageKeyRepository.getL1Key()!!
62+
}
63+
}
64+
65+
companion object {
66+
private const val L1_PASSPHRASE_SIZE = 32
67+
}
68+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.pir.impl.store.secure
18+
19+
import com.duckduckgo.di.scopes.AppScope
20+
import com.squareup.anvil.annotations.ContributesBinding
21+
import dagger.SingleInstanceIn
22+
import javax.inject.Inject
23+
24+
interface PirSecureStorageKeyRepository {
25+
/**
26+
* Key used for L1 encryption
27+
*/
28+
suspend fun getL1Key(): ByteArray?
29+
suspend fun setL1Key(value: ByteArray?)
30+
31+
/**
32+
* This method can be checked if the keystore has support for encryption
33+
*
34+
* @return `true` if keystore encryption is supported and `false` otherwise
35+
*/
36+
suspend fun canUseEncryption(): Boolean
37+
}
38+
39+
@SingleInstanceIn(AppScope::class)
40+
@ContributesBinding(
41+
scope = AppScope::class,
42+
boundType = PirSecureStorageKeyRepository::class,
43+
)
44+
class RealPirSecureStorageKeyRepository @Inject constructor(
45+
private val keyStore: PirSecureStorageKeyStore,
46+
) : PirSecureStorageKeyRepository {
47+
override suspend fun getL1Key(): ByteArray? = keyStore.getKey(KEY_L1KEY)
48+
override suspend fun setL1Key(value: ByteArray?) {
49+
keyStore.updateKey(KEY_L1KEY, value)
50+
}
51+
52+
override suspend fun canUseEncryption(): Boolean = keyStore.canUseEncryption()
53+
54+
companion object {
55+
private const val KEY_L1KEY = "KEY_L1KEY"
56+
}
57+
}

0 commit comments

Comments
 (0)