Skip to content

Commit 53709fc

Browse files
authored
PIR: Add dev setting to export db (#7011)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1211766094952757?focus=true ### Description Adds a dev setting to export the PIR encrypted database as unencrypted ### Steps to test this PR See https://app.asana.com/1/137249556945/task/1211754086792538?focus=true ### UI changes No UI changes
1 parent d962ad3 commit 53709fc

File tree

5 files changed

+135
-1
lines changed

5 files changed

+135
-1
lines changed

pir/pir-internal/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ dependencies {
4141
implementation AndroidX.webkit
4242
implementation Google.android.material
4343
implementation Google.dagger
44+
implementation AndroidX.room.runtime
4445
}

pir/pir-internal/src/main/java/com/duckduckgo/pir/internal/settings/PirDevScanActivity.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.duckduckgo.pir.impl.store.PirRepository
4444
import com.duckduckgo.pir.impl.store.PirSchedulingRepository
4545
import com.duckduckgo.pir.internal.R
4646
import com.duckduckgo.pir.internal.databinding.ActivityPirInternalScanBinding
47+
import com.duckduckgo.pir.internal.settings.store.secure.PirDatabaseExporter
4748
import kotlinx.coroutines.flow.launchIn
4849
import kotlinx.coroutines.flow.onEach
4950
import kotlinx.coroutines.launch
@@ -77,6 +78,9 @@ class PirDevScanActivity : DuckDuckGoActivity() {
7778
@Inject
7879
lateinit var currentTimeProvider: CurrentTimeProvider
7980

81+
@Inject
82+
lateinit var pirDatabaseExporter: PirDatabaseExporter
83+
8084
private val binding: ActivityPirInternalScanBinding by viewBinding()
8185
private val recordStringBuilder = StringBuilder()
8286

@@ -197,6 +201,12 @@ class PirDevScanActivity : DuckDuckGoActivity() {
197201
pirScanScheduler.scheduleScans()
198202
Toast.makeText(this, getString(R.string.pirMessageSchedule), Toast.LENGTH_SHORT).show()
199203
}
204+
205+
binding.debugExportDb.setOnClickListener {
206+
lifecycleScope.launch(dispatcherProvider.io()) {
207+
pirDatabaseExporter.exportToPlaintext()
208+
}
209+
}
200210
}
201211

202212
private fun killRunningWork() {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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.internal.settings.store.secure
18+
19+
import android.content.Context
20+
import com.duckduckgo.common.utils.DispatcherProvider
21+
import com.duckduckgo.di.scopes.AppScope
22+
import com.duckduckgo.pir.impl.store.PirDatabase
23+
import com.squareup.anvil.annotations.ContributesBinding
24+
import dagger.SingleInstanceIn
25+
import kotlinx.coroutines.withContext
26+
import logcat.LogPriority.ERROR
27+
import logcat.asLog
28+
import logcat.logcat
29+
import java.io.File
30+
import javax.inject.Inject
31+
32+
/**
33+
* Utility interface for exporting the encrypted PIR database to a plaintext format.
34+
* Useful for debugging and data inspection.
35+
*/
36+
interface PirDatabaseExporter {
37+
/**
38+
* Exports the encrypted database to a plaintext (unencrypted) SQLite database
39+
* on the device's external storage.
40+
*
41+
* You can pull it using "adb pull /storage/emulated/0/Android/data/com.duckduckgo.mobile.android.debug/files/pir_decrypted.db ~/Desktop/"
42+
* and open it in a SQLite viewer for inspection.
43+
*/
44+
suspend fun exportToPlaintext()
45+
}
46+
47+
@SingleInstanceIn(AppScope::class)
48+
@ContributesBinding(
49+
scope = AppScope::class,
50+
boundType = PirDatabaseExporter::class,
51+
)
52+
class PirRealDatabaseExporter @Inject constructor(
53+
private val pirDatabase: PirDatabase,
54+
private val dispatcherProvider: DispatcherProvider,
55+
private val context: Context,
56+
) : PirDatabaseExporter {
57+
58+
override suspend fun exportToPlaintext() = withContext(dispatcherProvider.io()) {
59+
exportDecryptedDatabase()
60+
}
61+
62+
private fun exportDecryptedDatabase() {
63+
try {
64+
// Get the writable database instance
65+
val db = pirDatabase.openHelper.writableDatabase
66+
67+
// Define output path - using external files directory for accessibility
68+
val outputFile = File(context.getExternalFilesDir(null), "pir_decrypted.db")
69+
70+
// Delete existing file if present
71+
if (outputFile.exists()) {
72+
outputFile.delete()
73+
logcat { "PIR-DB: Deleted existing decrypted database file" }
74+
}
75+
76+
val outputPath = outputFile.absolutePath
77+
logcat { "PIR-DB: Exporting decrypted database to: $outputPath" }
78+
79+
// Attach a plaintext database (no encryption key)
80+
db.query("ATTACH DATABASE ? AS plaintext KEY ''", arrayOf(outputPath)).use { cursor ->
81+
cursor.moveToFirst()
82+
}
83+
84+
logcat { "PIR-DB: Attached plaintext database" }
85+
86+
// Export the encrypted database to the plaintext one
87+
db.query("SELECT sqlcipher_export('plaintext')").use { cursor ->
88+
if (cursor.moveToFirst()) {
89+
val result = cursor.getString(0)
90+
logcat { "PIR-DB: Export result: $result" }
91+
}
92+
}
93+
94+
logcat { "PIR-DB: Database exported successfully" }
95+
96+
// Detach the plaintext database
97+
db.query("DETACH DATABASE plaintext").use { cursor ->
98+
cursor.moveToFirst()
99+
}
100+
101+
logcat { "PIR-DB: Detached plaintext database" }
102+
103+
// Verify the file was created and has content
104+
if (outputFile.exists() && outputFile.length() > 0) {
105+
logcat { "PIR-DB: Successfully exported decrypted database (${outputFile.length()} bytes)" }
106+
logcat { "PIR-DB: File location: ${outputFile.absolutePath}" }
107+
} else {
108+
logcat(ERROR) { "PIR-DB: Export file was not created or is empty" }
109+
}
110+
} catch (e: Exception) {
111+
logcat(ERROR) { "PIR-DB: Error exporting database: ${e.asLog()}" }
112+
}
113+
}
114+
}

pir/pir-internal/src/main/res/layout/activity_pir_internal_scan.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@
166166
app:editable="true"
167167
app:type="single_line" />
168168

169+
<com.duckduckgo.common.ui.view.button.DaxButtonPrimary
170+
android:id="@+id/debugExportDb"
171+
android:layout_width="180dp"
172+
android:layout_height="wrap_content"
173+
android:layout_gravity="center_horizontal"
174+
android:text="@string/pirDevExportDb"
175+
app:editable="true"
176+
app:type="single_line" />
169177

170178
<com.duckduckgo.common.ui.view.listitem.SectionHeaderListItem
171179
android:layout_width="wrap_content"
@@ -204,4 +212,4 @@
204212
android:text="@string/pirDevViewScanResults" />
205213
</LinearLayout>
206214
</ScrollView>
207-
</LinearLayout>
215+
</LinearLayout>

pir/pir-internal/src/main/res/values/donottranslate.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<string name="pirDevViewOptOutResults">View All OptOut Results</string>
3131
<string name="pirDevViewEmailResults">View Email Confirmation Results</string>
3232
<string name="pirDevViewRunEvents">View PIR Run events</string>
33+
<string name="pirDevExportDb">Export database</string>
3334
<string name="pirDevViewEmailEvents">PIR Email</string>
3435
<string name="pirDevSchedule">Schedule Scan</string>
3536
<string name="pirDevSimpleScanHeader">Simple Scan Results</string>

0 commit comments

Comments
 (0)