Skip to content

Commit 3e526d6

Browse files
committed
feat: add web database encryption and app version display
- Enable web database encryption using SQLite3MultipleCiphers WASM with OPFS storage. - Implement `AppVersionUseCase` across all platforms (Android, iOS, JVM, WasmJs) to display the application version in the Info settings. - Rename "Theme" settings category to "Appearance" and update related UI components and test tags. - Refactor `SettingsViewModel` and state handling to use a unified `SettingsResult` model. - Extract shared build logic into reusable Gradle convention extensions for Karma tests, SQLite3MC resources, and dependency management. - Remove the vendored `androidx.paging.compose` fork. - Expand WasmJs UI test coverage and update shared test infrastructure. - Bump project version to `8.5.3` and update dependencies (Gradle 9.4.0, Compose 1.10.2). - Update documentation for web encryption, OPFS, and database export/inspection.
1 parent fe2ce29 commit 3e526d6

66 files changed

Lines changed: 1081 additions & 690 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/kmp.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
5555
android_ui_test_job:
5656
name: Android UI-tests on emulator
57-
runs-on: ubuntu-latest
57+
runs-on: ubuntu-22.04
5858
continue-on-error: true
5959
strategy:
6060
fail-fast: false

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [8.5.3] - 2026-03-10
9+
10+
### Features
11+
- Enable web database encryption support with SQLite3MultipleCiphers WASM, including browser-side encrypt/decrypt/rekey flows backed by OPFS storage
12+
- Improve Settings by renaming the Theme category to Appearance and showing the application version in the Info section on Android, iOS, and Desktop
13+
14+
### Bug Fixes
15+
- Fix encrypted database compatibility across JVM and WasmJs by aligning SQLCipher v4 settings and validating database readability when opening protected databases
16+
- Fix Desktop SQLCipher query/version checks by using the generated JDBC URL directly for inspection queries
17+
18+
### Refactoring
19+
- Extract shared Gradle convention helpers for Karma Chrome detection, SQLite3MultipleCiphers resource setup, Android dependency forcing, and Desktop version metadata generation
20+
- Remove the vendored `androidx.paging.compose` fork and simplify Settings state handling with the new `SettingsResult` model
21+
22+
### Tests
23+
- Add WasmJs smoke tests and expand web UI coverage for Settings/navigation flows
24+
- Update shared UI test infrastructure and settings test helpers for the renamed Appearance category and version row
25+
26+
### Documentation
27+
- Add a guide for opening exported databases and refresh the web/OPFS/build-logic docs to reflect encrypted SQLite3MultipleCiphers usage
28+
29+
### Chores
30+
- Update build tooling and dependencies, including Gradle `9.4.0`, AGP `9.1.0`, Compose `1.10.2`, Firebase BOM `34.10.0`, and CI runner settings
31+
832
## [8.5.2] - 2026-02-06
933

1034
### Features

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ Supported platforms:
3434

3535
![Architecture blueprint for this project](docs/diagrams/architecture.png)
3636

37-
## WORK IN PROGRESS 🛠
37+
## FEATURES ✨
3838

3939
| feature \ platform | Android | iOS | Desktop Java | Web |
4040
|:------------------:|:-------:|:---:|:------------:|:---:|
4141
| database |||||
42-
| encryption |||| |
42+
| encryption |||| |
4343
| backup |||||
4444

45-
Check out [CONTRIBUTING.md](/CONTRIBUTING.md) if you want to develop missing features.
45+
Interested in contributing new features or fixes? Check out [CONTRIBUTING.md](/CONTRIBUTING.md).
4646

4747
## CONTINUOUS INTEGRATION / DELIVERY ♻️
4848

app/android/build.gradle.kts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
@file:Suppress("UnstableApiUsage")
22

3+
import com.softartdev.notedelight.forceAndroidXDependencyVersions
34
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
45
import com.google.firebase.crashlytics.buildtools.gradle.tasks.UploadMappingFileTask
56
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
67
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
78

89
plugins {
10+
alias(libs.plugins.gradle.convention)
911
alias(libs.plugins.android.application)
1012
alias(libs.plugins.compose)
1113
alias(libs.plugins.compose.compiler)
@@ -23,8 +25,8 @@ android {
2325
applicationId = "com.softartdev.noteroom"
2426
minSdk = libs.versions.minSdk.get().toInt()
2527
targetSdk = libs.versions.targetSdk.get().toInt()
26-
versionCode = 852
27-
versionName = "8.5.2"
28+
versionCode = 853
29+
versionName = "8.5.3"
2830
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2931
testInstrumentationRunnerArguments["clearPackageData"] = "true"
3032
vectorDrawables.useSupportLibrary = true
@@ -119,18 +121,4 @@ tasks.withType<UploadMappingFileTask> {
119121
dependsOn("processDebugGoogleServices")
120122
}
121123

122-
configurations.all {
123-
resolutionStrategy {
124-
sequenceOf(
125-
"common", "common-java8", "runtime", "runtime-ktx", "runtime-compose", "viewmodel", "viewmodel-ktx", "viewmodel-compose", "viewmodel-savedstate", "livedata", "livedata-core", "livedata-core-ktx", "process"
126-
).forEach { depName: String ->
127-
force("androidx.lifecycle:lifecycle-$depName:${libs.versions.androidxLifecycle.get()}")
128-
}
129-
force("androidx.savedstate:savedstate:1.4.0")
130-
force("androidx.savedstate:savedstate-ktx:1.4.0")
131-
force("androidx.savedstate:savedstate-compose:1.4.0")
132-
133-
force("androidx.concurrent:concurrent-futures:1.2.0")
134-
force("com.google.errorprone:error_prone_annotations:2.30.0")
135-
}
136-
}
124+
project.forceAndroidXDependencyVersions()

app/desktop/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import com.softartdev.notedelight.configureJvmVersionProps
12
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
23
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
34

@@ -55,7 +56,7 @@ compose.desktop {
5556
nativeDistributions {
5657
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
5758
packageName = "Note Delight"
58-
packageVersion = "8.5.2"
59+
packageVersion = "8.5.3"
5960
description = "Note app with encryption"
6061
copyright = "© 2023 SoftArtDev"
6162
macOS.iconFile.set(project.file("src/jvmMain/resources/app_icon.icns"))
@@ -69,3 +70,5 @@ compose.desktop {
6970
}
7071
}
7172
}
73+
74+
project.configureJvmVersionProps(compose.desktop.application.nativeDistributions.packageVersion)

app/iosApp/iosApp.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@
475475
"$(inherited)",
476476
"@executable_path/Frameworks",
477477
);
478-
MARKETING_VERSION = 8.5.2;
478+
MARKETING_VERSION = 8.5.3;
479479
OTHER_LDFLAGS = (
480480
"$(inherited)",
481481
"-ObjC",
@@ -517,7 +517,7 @@
517517
"$(inherited)",
518518
"@executable_path/Frameworks",
519519
);
520-
MARKETING_VERSION = 8.5.2;
520+
MARKETING_VERSION = 8.5.3;
521521
OTHER_LDFLAGS = (
522522
"$(inherited)",
523523
"-ObjC",
Binary file not shown.

app/iosApp/iosApp/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<key>CFBundlePackageType</key>
2323
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
2424
<key>CFBundleShortVersionString</key>
25-
<string>8.5.2</string>
25+
<string>8.5.3</string>
2626
<key>CFBundleVersion</key>
2727
<string>19</string>
2828
<key>LSRequiresIPhoneOS</key>

app/web/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ HTML entry point:
8484

8585
### Database
8686

87-
- **Official SQLite WASM**: Native SQLite compiled to WebAssembly
87+
- **SQLite3MultipleCiphers WASM**: SQLite with encryption support compiled to WebAssembly
8888
- **OPFS Storage**: Origin-Private FileSystem for persistent database storage
8989
- **Web Worker**: Database operations run off the main thread
90-
- **No encryption**: SQLCipher not available in browsers
90+
- **Encryption ready**: [SQLite3MultipleCiphers](https://github.com/utelle/SQLite3MultipleCiphers) provides cipher support in the browser
9191

9292
### Webpack
9393

@@ -124,7 +124,7 @@ build/dist/wasmJs/productionExecutable/
124124
├── composeApp.wasm # Application WebAssembly binary
125125
├── skiko.wasm # Skia graphics engine
126126
├── sqlite3.js # SQLite JavaScript
127-
├── sqlite3.wasm # Official SQLite WASM
127+
├── sqlite3.wasm # SQLite3MultipleCiphers WASM (with encryption)
128128
├── sqlite.worker.js # Custom OPFS worker
129129
├── coi-serviceworker.js # Service worker for headers
130130
└── sql-wasm.wasm # Legacy SQL.js (fallback)
@@ -280,7 +280,7 @@ fun isWasmSupported(): Boolean = js("""
280280

281281
### Current Limitations
282282

283-
1. **No encryption**: SQLCipher not available in browsers
283+
1. **Encryption**: [SQLite3MultipleCiphers](https://github.com/utelle/SQLite3MultipleCiphers) WASM provides encrypt/decrypt/rekey via `PRAGMA key`/`PRAGMA rekey`
284284
2. ✅ **Storage**: OPFS provides persistent database storage
285285
3. ⚠️ **File access**: Restricted browser file API
286286
4. ⚠️ **Performance**: Slower than native (improving)
@@ -309,7 +309,7 @@ The web app now uses OPFS (Origin-Private FileSystem) for persistent database st
309309
- ✅ **Persistent storage**: Survives browser sessions
310310
- ✅ **Better performance**: Direct file system access
311311
- ✅ **Larger capacity**: Not limited by IndexedDB quotas
312-
- ✅ **Real SQLite**: Uses official SQLite WASM build
312+
- ✅ **Real SQLite with encryption**: Uses SQLite3MultipleCiphers WASM build
313313

314314
### Browser Support
315315

@@ -506,7 +506,7 @@ When working with this module:
506506
3. **Size matters**: Minimize bundle size
507507
4. **Progressive enhancement**: Detect and use modern APIs gracefully
508508
5. **Testing**: Test in multiple browsers
509-
6. **Security**: No sensitive data without encryption
509+
6. **Security**: Encryption available via SQLite3MultipleCiphers
510510
7. **Performance**: Profile and optimize load time
511511
8. **Responsive**: Support mobile and desktop browsers
512512
9. **Accessibility**: Follow WCAG guidelines

app/web/build.gradle.kts

Lines changed: 13 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
@file:OptIn(ExperimentalWasmDsl::class)
22

3-
import de.undercouch.gradle.tasks.download.Download
3+
import com.softartdev.notedelight.configureWasmJsChromeForKarmaTests
4+
import com.softartdev.notedelight.configureWebSqlite3mcWasmResources
45
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
5-
import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest
66
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
77

88
plugins {
9+
alias(libs.plugins.gradle.convention)
910
alias(libs.plugins.kotlin.multiplatform)
1011
alias(libs.plugins.compose)
1112
alias(libs.plugins.compose.compiler)
@@ -16,27 +17,22 @@ kotlin {
1617
wasmJs {
1718
outputModuleName.set("composeApp")
1819
browser {
19-
val rootDirPath = project.rootDir.path
20-
val projectDirPath = project.projectDir.path
21-
testTask {
22-
useKarma {
23-
useChrome()
24-
useChromeHeadless()
25-
}
26-
}
20+
val rootDirPath: String = project.rootDir.path
21+
val projectDirPath: String = project.projectDir.path
2722
commonWebpackConfig {
2823
outputFileName = "composeApp.js"
2924
devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
30-
// Serve sources to debug inside browser
25+
// Serve sources to debug inside the browser
3126
static(rootDirPath)
3227
static(projectDirPath)
3328
}
3429
}
30+
testTask { useKarma { useChromeHeadless() } }
3531
}
3632
binaries.executable()
3733
}
3834
sourceSets {
39-
val wasmJsMain by getting {
35+
wasmJsMain {
4036
dependencies {
4137
implementation(projects.core.domain)
4238
implementation(projects.core.presentation)
@@ -49,54 +45,21 @@ kotlin {
4945
}
5046
resources.srcDir(layout.buildDirectory.dir("sqlite"))
5147
}
52-
val wasmJsTest by getting {
48+
wasmJsTest {
5349
dependencies {
5450
implementation(kotlin("test"))
5551
implementation(projects.ui.test)
5652
implementation(libs.compose.ui.test)
53+
implementation(libs.compose.material3)
5754
implementation(libs.compose.material.icons.extended)
5855
implementation(libs.androidx.lifecycle.runtime.compose)
5956
implementation(libs.androidx.lifecycle.runtime.testing)
57+
implementation(libs.androidx.paging.common)
6058
}
6159
resources.srcDir(layout.buildDirectory.dir("sqlite"))
6260
}
6361
}
6462
}
6563

66-
val chromeBinaryFromEnv = providers.environmentVariable("CHROME_BIN").orNull
67-
val hasChromeForTests = chromeBinaryFromEnv?.let { file(it).exists() } == true
68-
69-
tasks.named<KotlinJsTest>("wasmJsBrowserTest").configure {
70-
enabled = hasChromeForTests
71-
}
72-
73-
val sqliteVersion = 3500400 // See https://sqlite.org/download.html for the latest wasm build version
74-
val sqliteDownload = tasks.register("sqliteDownload", Download::class.java) {
75-
src("https://sqlite.org/2025/sqlite-wasm-$sqliteVersion.zip")
76-
dest(layout.buildDirectory.dir("tmp"))
77-
onlyIfModified(true)
78-
}
79-
val sqliteUnzip = tasks.register("sqliteUnzip", Copy::class.java) {
80-
dependsOn(sqliteDownload)
81-
from(zipTree(layout.buildDirectory.dir("tmp/sqlite-wasm-$sqliteVersion.zip"))) {
82-
include("sqlite-wasm-$sqliteVersion/jswasm/**")
83-
exclude("**/*worker1*") // We use our own worker
84-
eachFile {
85-
relativePath = RelativePath(true, *relativePath.segments.drop(2).toTypedArray())
86-
}
87-
}
88-
into(layout.buildDirectory.dir("sqlite"))
89-
includeEmptyDirs = false
90-
}
91-
// Hook the unzip task into the wasmJs resource processing
92-
tasks.named("wasmJsProcessResources").configure {
93-
dependsOn(sqliteUnzip)
94-
}
95-
tasks.named<Copy>("wasmJsTestProcessResources").configure {
96-
dependsOn(sqliteUnzip)
97-
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
98-
}
99-
100-
tasks.named<Sync>("wasmJsBrowserDistribution").configure {
101-
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
102-
}
64+
project.configureWasmJsChromeForKarmaTests()
65+
project.configureWebSqlite3mcWasmResources()

0 commit comments

Comments
 (0)