Skip to content

Commit 0770edf

Browse files
authored
[PIR] Handle DB encryption related errors (#7054)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211766964402360?focus=true ### Description Adds error handling for when PIR encrypted database fails to decrypt and we can't access the data. ### Steps to test this PR See https://app.asana.com/1/137249556945/task/1211824590968188?focus=true ### UI changes A new error dialog when opening PIR. See https://app.asana.com/1/137249556945/task/1211824590968188?focus=true
1 parent 7efc550 commit 0770edf

File tree

24 files changed

+560
-277
lines changed

24 files changed

+560
-277
lines changed

pir/pir-api/src/main/java/com/duckduckgo/pir/api/PirFeature.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616

1717
package com.duckduckgo.pir.api
1818

19+
import com.duckduckgo.pir.api.dashboard.PirFeatureState
20+
1921
interface PirFeature {
2022

2123
/**
2224
* Runs on the IO thread by default.
2325
*
24-
* @return true if the PIR beta is enabled, false otherwise
26+
* @return [PirFeatureState] that represents the current state of the feature
2527
*/
26-
suspend fun isPirBetaEnabled(): Boolean
28+
suspend fun getPirFeatureState(): PirFeatureState
2729
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.api.dashboard
18+
19+
/**
20+
* Represents the state of the PIR (Private Information Retrieval) feature.
21+
*/
22+
enum class PirFeatureState {
23+
/**
24+
* The PIR feature is enabled and available for use.
25+
*/
26+
ENABLED,
27+
28+
/**
29+
* The PIR feature is disabled and cannot be used.
30+
*/
31+
DISABLED,
32+
33+
/**
34+
* The PIR feature is enabled but not available.
35+
*/
36+
NOT_AVAILABLE,
37+
}

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/PirRemoteFeatures.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import com.duckduckgo.feature.toggles.api.Toggle
2323
import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
2424
import com.duckduckgo.feature.toggles.api.Toggle.DefaultValue
2525
import com.duckduckgo.pir.api.PirFeature
26+
import com.duckduckgo.pir.api.dashboard.PirFeatureState
27+
import com.duckduckgo.pir.impl.store.PirRepository
2628
import com.squareup.anvil.annotations.ContributesBinding
2729
import dagger.SingleInstanceIn
2830
import kotlinx.coroutines.withContext
@@ -48,9 +50,20 @@ interface PirRemoteFeatures {
4850
class PirRemoteFeatureImpl @Inject constructor(
4951
private val pirRemoteFeatures: PirRemoteFeatures,
5052
private val dispatcherProvider: DispatcherProvider,
53+
private val pirRepository: PirRepository,
5154
) : PirFeature {
5255

53-
override suspend fun isPirBetaEnabled(): Boolean = withContext(dispatcherProvider.io()) {
54-
pirRemoteFeatures.pirBeta().isEnabled()
56+
override suspend fun getPirFeatureState(): PirFeatureState = withContext(dispatcherProvider.io()) {
57+
val isEnabled = pirRemoteFeatures.pirBeta().isEnabled()
58+
if (!isEnabled) {
59+
return@withContext PirFeatureState.DISABLED
60+
}
61+
62+
val isRepositoryAvailable = pirRepository.isRepositoryAvailable()
63+
if (!isRepositoryAvailable) {
64+
return@withContext PirFeatureState.NOT_AVAILABLE
65+
}
66+
67+
return@withContext PirFeatureState.ENABLED
5568
}
5669
}

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/brokers/BrokerJsonUpdater.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ class RealBrokerJsonUpdater @Inject constructor(
5656
*/
5757
override suspend fun update(): Boolean = withContext(dispatcherProvider.io()) {
5858
return@withContext kotlin.runCatching {
59+
if (!pirRepository.isRepositoryAvailable()) {
60+
return@withContext false
61+
}
62+
5963
confirmEtagIntegrity()
6064
dbpService.getMainConfig(pirRepository.getCurrentMainEtag()).also {
6165
logcat { "PIR-update: Main config result $it." }

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/checker/PirWorkHandler.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.duckduckgo.pir.impl.PirRemoteFeatures
2424
import com.duckduckgo.pir.impl.optout.PirForegroundOptOutService
2525
import com.duckduckgo.pir.impl.scan.PirForegroundScanService
2626
import com.duckduckgo.pir.impl.scan.PirScanScheduler
27+
import com.duckduckgo.pir.impl.store.PirRepository
2728
import com.duckduckgo.subscriptions.api.Product.PIR
2829
import com.duckduckgo.subscriptions.api.SubscriptionStatus
2930
import com.duckduckgo.subscriptions.api.Subscriptions
@@ -65,6 +66,7 @@ class RealPirWorkHandler @Inject constructor(
6566
private val subscriptions: Subscriptions,
6667
private val context: Context,
6768
private val pirScanScheduler: PirScanScheduler,
69+
private val pirRepository: PirRepository,
6870
) : PirWorkHandler {
6971

7072
override suspend fun canRunPir(): Flow<Boolean> {
@@ -81,7 +83,7 @@ class RealPirWorkHandler @Inject constructor(
8183
}
8284
.distinctUntilChanged(),
8385
) { subscriptionStatus, hasValidEntitlement ->
84-
isPirEnabled(hasValidEntitlement, subscriptionStatus)
86+
isPirEnabled(hasValidEntitlement, subscriptionStatus) && pirRepository.isRepositoryAvailable()
8587
}
8688
} else {
8789
flowOf(false)

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

Lines changed: 7 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.duckduckgo.pir.impl.common.RealNativeBrokerActionHandler
3131
import com.duckduckgo.pir.impl.common.actions.EventHandler
3232
import com.duckduckgo.pir.impl.common.actions.PirActionsRunnerStateEngineFactory
3333
import com.duckduckgo.pir.impl.common.actions.RealPirActionsRunnerStateEngineFactory
34+
import com.duckduckgo.pir.impl.pixels.PirPixelSender
3435
import com.duckduckgo.pir.impl.scripts.BrokerActionProcessor
3536
import com.duckduckgo.pir.impl.scripts.PirMessagingInterface
3637
import com.duckduckgo.pir.impl.scripts.RealBrokerActionProcessor
@@ -48,19 +49,9 @@ import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.GetCaptchaInfoR
4849
import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.NavigateResponse
4950
import com.duckduckgo.pir.impl.scripts.models.PirSuccessResponse.SolveCaptchaResponse
5051
import com.duckduckgo.pir.impl.service.DbpService
51-
import com.duckduckgo.pir.impl.store.PirDatabase
5252
import com.duckduckgo.pir.impl.store.PirRepository
5353
import com.duckduckgo.pir.impl.store.RealPirDataStore
5454
import com.duckduckgo.pir.impl.store.RealPirRepository
55-
import com.duckduckgo.pir.impl.store.db.BrokerDao
56-
import com.duckduckgo.pir.impl.store.db.BrokerJsonDao
57-
import com.duckduckgo.pir.impl.store.db.EmailConfirmationLogDao
58-
import com.duckduckgo.pir.impl.store.db.ExtractedProfileDao
59-
import com.duckduckgo.pir.impl.store.db.JobSchedulingDao
60-
import com.duckduckgo.pir.impl.store.db.OptOutResultsDao
61-
import com.duckduckgo.pir.impl.store.db.ScanLogDao
62-
import com.duckduckgo.pir.impl.store.db.ScanResultsDao
63-
import com.duckduckgo.pir.impl.store.db.UserProfileDao
6455
import com.duckduckgo.pir.impl.store.secure.PirSecureStorageDatabaseFactory
6556
import com.squareup.anvil.annotations.ContributesTo
6657
import com.squareup.moshi.Moshi
@@ -70,97 +61,30 @@ import dagger.Module
7061
import dagger.Provides
7162
import dagger.SingleInstanceIn
7263
import kotlinx.coroutines.CoroutineScope
73-
import kotlinx.coroutines.runBlocking
7464
import javax.inject.Named
7565

7666
@Module
7767
@ContributesTo(AppScope::class)
7868
class PirModule {
7969

80-
@SingleInstanceIn(AppScope::class)
81-
@Provides
82-
fun bindPirDatabase(
83-
databaseFactory: PirSecureStorageDatabaseFactory,
84-
): PirDatabase {
85-
return runBlocking {
86-
databaseFactory.getDatabase()
87-
} ?: throw IllegalStateException("Failed to create PIR encrypted database")
88-
}
89-
90-
@SingleInstanceIn(AppScope::class)
91-
@Provides
92-
fun provideBrokerJsonDao(database: PirDatabase): BrokerJsonDao {
93-
return database.brokerJsonDao()
94-
}
95-
96-
@SingleInstanceIn(AppScope::class)
97-
@Provides
98-
fun provideBrokerDao(database: PirDatabase): BrokerDao {
99-
return database.brokerDao()
100-
}
101-
102-
@SingleInstanceIn(AppScope::class)
103-
@Provides
104-
fun provideScanResultsDao(database: PirDatabase): ScanResultsDao {
105-
return database.scanResultsDao()
106-
}
107-
108-
@SingleInstanceIn(AppScope::class)
109-
@Provides
110-
fun provideUserProfileDao(database: PirDatabase): UserProfileDao {
111-
return database.userProfileDao()
112-
}
113-
114-
@SingleInstanceIn(AppScope::class)
115-
@Provides
116-
fun provideScanLogDao(database: PirDatabase): ScanLogDao {
117-
return database.scanLogDao()
118-
}
119-
120-
@SingleInstanceIn(AppScope::class)
121-
@Provides
122-
fun provideOptOutResultsDao(database: PirDatabase): OptOutResultsDao {
123-
return database.optOutResultsDao()
124-
}
125-
126-
@SingleInstanceIn(AppScope::class)
127-
@Provides
128-
fun provideJobSchedulingDao(database: PirDatabase): JobSchedulingDao {
129-
return database.jobSchedulingDao()
130-
}
131-
132-
@SingleInstanceIn(AppScope::class)
133-
@Provides
134-
fun provideExtractedProfileDao(database: PirDatabase): ExtractedProfileDao {
135-
return database.extractedProfileDao()
136-
}
137-
138-
@SingleInstanceIn(AppScope::class)
139-
@Provides
140-
fun provideEmailConfirmationLogDao(database: PirDatabase): EmailConfirmationLogDao {
141-
return database.emailConfirmationLogDao()
142-
}
143-
14470
@Provides
14571
@SingleInstanceIn(AppScope::class)
14672
fun providePirRepository(
14773
sharedPreferencesProvider: SharedPreferencesProvider,
14874
dispatcherProvider: DispatcherProvider,
149-
brokerJsonDao: BrokerJsonDao,
150-
brokerDao: BrokerDao,
15175
currentTimeProvider: CurrentTimeProvider,
152-
userProfileDao: UserProfileDao,
15376
dbpService: DbpService,
154-
extractedProfileDao: ExtractedProfileDao,
77+
@AppCoroutineScope appCoroutineScope: CoroutineScope,
78+
databaseFactory: PirSecureStorageDatabaseFactory,
79+
pixelSender: PirPixelSender,
15580
): PirRepository = RealPirRepository(
15681
dispatcherProvider,
15782
RealPirDataStore(sharedPreferencesProvider),
15883
currentTimeProvider,
159-
brokerJsonDao,
160-
brokerDao,
161-
userProfileDao,
84+
databaseFactory,
16285
dbpService,
163-
extractedProfileDao,
86+
pixelSender,
87+
appCoroutineScope,
16488
)
16589

16690
@Provides

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/pixels/PirPixel.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ enum class PirPixel(
125125
PIR_EMAIL_CONFIRMATION_RUN_COMPLETED(
126126
baseName = "pir_email-confirmation_completed",
127127
type = Count,
128+
),
129+
130+
PIR_INTERNAL_SECURE_STORAGE_UNAVAILABLE(
131+
baseName = "pir_internal_secure-storage_unavailable",
132+
types = setOf(Count, Daily()),
128133
), ;
129134

130135
constructor(

pir/pir-impl/src/main/java/com/duckduckgo/pir/impl/pixels/PirPixelSender.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.duckduckgo.pir.impl.pixels.PirPixel.PIR_INTERNAL_SCAN_STATS
3939
import com.duckduckgo.pir.impl.pixels.PirPixel.PIR_INTERNAL_SCHEDULED_SCAN_COMPLETED
4040
import com.duckduckgo.pir.impl.pixels.PirPixel.PIR_INTERNAL_SCHEDULED_SCAN_SCHEDULED
4141
import com.duckduckgo.pir.impl.pixels.PirPixel.PIR_INTERNAL_SCHEDULED_SCAN_STARTED
42+
import com.duckduckgo.pir.impl.pixels.PirPixel.PIR_INTERNAL_SECURE_STORAGE_UNAVAILABLE
4243
import com.duckduckgo.pir.impl.pixels.PirPixel.PIR_OPTOUT_STAGE_PENDING_EMAIL_CONFIRMATION
4344
import com.squareup.anvil.annotations.ContributesBinding
4445
import logcat.logcat
@@ -282,6 +283,11 @@ interface PirPixelSender {
282283
totalFetchAttempts: Int,
283284
totalEmailConfirmationJobs: Int,
284285
)
286+
287+
/**
288+
* Emits a pixel to signal that PIR encrypted database is unavailable.
289+
*/
290+
fun reportSecureStorageUnavailable()
285291
}
286292

287293
@ContributesBinding(AppScope::class)
@@ -526,6 +532,10 @@ class RealPirPixelSender @Inject constructor(
526532
fire(PIR_EMAIL_CONFIRMATION_RUN_COMPLETED, params)
527533
}
528534

535+
override fun reportSecureStorageUnavailable() {
536+
fire(PIR_INTERNAL_SECURE_STORAGE_UNAVAILABLE)
537+
}
538+
529539
private fun fire(
530540
pixel: PirPixel,
531541
params: Map<String, String> = emptyMap(),

0 commit comments

Comments
 (0)