mdk-core 0.8.0

A simplified interface to build secure messaging apps on nostr with MLS.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
# Encrypted SQLite Storage Implementation Plan

This document outlines the plan for implementing encrypted SQLite storage using SQLCipher, addressing the security audit finding regarding unencrypted MLS state storage.

## Background

### Audit Finding (Issue F)

The MLS state is stored in an unencrypted SQLite database with default file permissions, exposing sensitive data including:
- Messages and message content
- Group metadata
- Exporter secrets (enables retrospective traffic decryption)

### Document Structure

This document is split into two parts:

1. **Part A (MDK-generic)**: The design and implementation plan intended to be useful for any MDK user, regardless of platform.
2. **Part B (whitenoise-rs-specific)**: Non-normative integration notes and examples for `whitenoise-rs` and Flutter/FRB.

---

## Part A: MDK-generic design

### Goals

1. **Encrypt MLS state at rest** in `mdk-sqlite-storage` using SQLCipher.
2. **Keep MDK platform-agnostic**: use the `keyring-core` ecosystem for cross-platform secure credential storage.
3. **Minimize footguns**: explicit keying procedure, clear failure modes, and safe defaults for file placement/permissions.

### Non-goals (for this workstream)

1. **Backups / restore / portability** are not supported yet. (Future work could add explicit export/import tooling, but that changes the threat model and must be opt-in.)
2. **In-memory zeroization / secure buffers** are out of scope here and will be addressed separately.
3. **Defense against a compromised runtime** (root/jailbreak/malware that can read process memory or intercept callbacks) is out of scope. This plan primarily targets offline/exfiltration threats.

### Threat Model

**Assets to protect**

- MLS state stored by `mdk-sqlite-storage`, especially **exporter secrets** (which enable retrospective traffic decryption).
- Group metadata and message content stored in the DB.
- The SQLCipher database encryption key (and any other secrets stored via secure storage).

**Primary attacker we are designing for**

- An attacker who can obtain **a copy of the SQLite database files** (e.g., via device theft, filesystem exfiltration, misconfigured file permissions, developer backups), but who does **not** have access to platform secure storage (Keychain/Keystore/etc.) and does not control the running process.

**Out of scope / explicitly not defended**

- A compromised host application (malicious integration).
- A compromised device / OS (root/jailbreak) or malware that can call secure storage APIs or read process memory.
- Side-channel attacks, hardware attacks, and "evil maid" runtime tampering.

**Trust boundaries**

- MDK trusts the credential store implementations from `keyring-core` to keep secrets confidential.

### Solution Overview (MDK-generic)

1. **Database Encryption**: Use SQLCipher via `rusqlite` `bundled-sqlcipher`.
2. **Secure Storage via `keyring-core`**: Use the [`keyring-core`]https://crates.io/crates/keyring-core ecosystem instead of a custom abstraction:
   - `keyring-core` provides a unified cross-platform API for credential storage
   - Platform-native stores are provided as separate crates (see table below)
   - No custom traits needed—MDK uses `keyring-core::Entry` directly
3. **File Permissions**: Restrict database directories (mode `0700`) and files (mode `0600`) on Unix-like platforms, and apply ACL hardening on Windows to restrict access to the current user.

### Why `keyring-core`?

The [`keyring-core`](https://github.com/open-source-cooperative/keyring-core) ecosystem (v0.7+) provides:

- **Unified API**: Single `Entry` type for all platforms with `set_secret()`, `get_secret()`, `delete_credential()`
- **Native platform stores**: Each platform has a dedicated crate that implements the `CredentialStoreApi` trait
- **Thread-safe by design**: All credentials are `Send + Sync`
- **Android support**: The [`android-native-keyring-store`]https://crates.io/crates/android-native-keyring-store crate provides native Android Keystore integration
- **Active maintenance**: Maintained by the [Open Source Cooperative]https://github.com/open-source-cooperative


---

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                               Host Application                               │
│     (Swift/Kotlin/Flutter/React Native/Desktop/etc.)                         │
│                                   │                                          │
│            (Optional: platform-specific store initialization)                │
│                                   │                                          │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │                               MDK (Rust)                               │  │
│  │                                                                       │  │
│  │  ┌─────────────────────────────────────────────────────────────────┐  │  │
│  │  │                         keyring-core                             │  │  │
│  │  │  - Entry::new(service, user)                                     │  │  │
│  │  │  - entry.set_secret() / get_secret() / delete_credential()       │  │  │
│  │  └─────────────────────────────────────────────────────────────────┘  │  │
│  │                                   │                                    │  │
│  │         ┌─────────────────────────┼─────────────────────────┐         │  │
│  │         │                         │                         │         │  │
│  │         ▼                         ▼                         ▼         │  │
│  │  ┌─────────────┐         ┌─────────────────┐       ┌─────────────────┐│  │
│  │  │ Apple Store │         │ Android Store   │       │ Windows/Linux   ││  │
│  │  │(macOS+iOS)  │         │(native keystore)│       │ stores          ││  │
│  │  └─────────────┘         └─────────────────┘       └─────────────────┘│  │
│  │                                                                       │  │
│  │  ┌─────────────────────────────────────────────────────────────────┐  │  │
│  │  │                       mdk-sqlite-storage                         │  │  │
│  │  │  - SQLCipher-encrypted SQLite                                    │  │  │
│  │  │  - Uses keyring-core Entry to obtain/store DB key                │  │  │
│  │  └─────────────────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Credential Store Crates by Platform

| Platform | Crate | Backend | Initialization |
|----------|-------|---------|----------------|
| macOS | [`apple-native-keyring-store`]https://crates.io/crates/apple-native-keyring-store | Keychain Services | Automatic |
| iOS | [`apple-native-keyring-store`]https://crates.io/crates/apple-native-keyring-store | Keychain Services | Automatic |
| Windows | [`windows-native-keyring-store`]https://crates.io/crates/windows-native-keyring-store | Credential Manager | Automatic |
| Linux | [`linux-keyutils-keyring-store`]https://crates.io/crates/linux-keyutils-keyring-store | Kernel keyutils | Automatic |
| Linux | [`dbus-secret-service-keyring-store`]https://crates.io/crates/dbus-secret-service-keyring-store | D-Bus Secret Service | Automatic |
| Linux | [`zbus-secret-service-keyring-store`]https://crates.io/crates/zbus-secret-service-keyring-store | D-Bus (async zbus) | Automatic |
| Android | [`android-native-keyring-store`]https://crates.io/crates/android-native-keyring-store | Android Keystore | **Requires init** (see below) |

---

## Technical Design

### 1. SQLCipher Integration

#### Crypto Backends by Platform

| Platform | Crypto Backend | Notes |
|----------|---------------|-------|
| iOS | CommonCrypto (Security.framework) | Native, no OpenSSL |
| macOS | CommonCrypto (Security.framework) | Native, no OpenSSL |
| Android | libcrypto (NDK) | Provided by NDK |
| Linux | libcrypto (OpenSSL) | System dependency |
| Windows | OpenSSL | Requires configuration |

#### Cargo.toml Changes

```toml
# Workspace Cargo.toml
[workspace.dependencies]
rusqlite = { version = "0.32", default-features = false }

# mdk-sqlite-storage/Cargo.toml
[dependencies]
rusqlite = { workspace = true, features = ["bundled-sqlcipher"] }
```

**Windows note:** SQLCipher builds typically require OpenSSL headers/libs to be available at build time (or a vendored OpenSSL build strategy).

#### Keying Procedure (precise `PRAGMA key` format)

MDK will use a **random 32-byte (256-bit) key** generated once and stored via `keyring-core` (platform secure credential storage). When opening a database connection:

- `mdk-sqlite-storage` uses multiple SQLite connections to the same database file (e.g., OpenMLS storage and MDK tables). The keying procedure must run **on each connection**.

1. **Call `PRAGMA key` as the first operation** on the database connection.
2. Use **raw key data** (not a passphrase) so we do not depend on passphrase KDF settings:
   - SQLCipher expects a **64 character hex string** inside a blob literal, which it converts to 32 bytes of key data:

```sql
PRAGMA key = "x'2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99'";
```

3. **Immediately after setting the key**, pin SQLCipher defaults and prevent temp spill:

```sql
PRAGMA cipher_compatibility = 4;
PRAGMA temp_store = MEMORY;
```

`PRAGMA cipher_compatibility` is applied through the SQLCipher codec context, so it must run **after** `PRAGMA key` (key first still applies).

4. **Validate the key immediately**: SQLCipher will not always error on `PRAGMA key` alone if the key is wrong. A simple schema read is the recommended check:

```sql
SELECT count(*) FROM sqlite_master;
```

**Alternative:** SQLCipher also exposes `sqlite3_key()` / `sqlite3_key_v2()` as programmatic equivalents to `PRAGMA key`. (The `PRAGMA` interface calls these internally.)

5. Only after the above succeeds should the connection execute other pragmas (e.g., `PRAGMA foreign_keys = ON;`) and run migrations / normal queries.

#### Cipher Parameters (defaults, but pinned intentionally)

SQLCipher’s **major versions have different default settings**, and existing databases can require migration when defaults change. This plan will:

- **Stick to SQLCipher defaults** for the selected compatibility baseline.
- Use `PRAGMA cipher_compatibility = 4;` on **every connection** to pin SQLCipher 4.x defaults so that future SQLCipher upgrades do not silently change parameters.
  - The default Rust SQLCipher bundle used by `rusqlite`/`libsqlite3-sys` is currently SQLCipher **4.5.7 (community)**.
- If we ever need to open databases created under older defaults, use SQLCipher’s supported migration mechanisms (e.g., `PRAGMA cipher_migrate` or `sqlcipher_export`) rather than guessing parameters.

#### SQLite Sidecar Files and Temporary Files

SQLCipher encrypts more than just the `*.db` file, but there are important nuances:

- **Rollback journals (`*-journal`)**: data pages are encrypted with the same key as the main database. The rollback journal includes an **unencrypted header**, but it does not contain data.
- **WAL (`*-wal`)**: page data stored in the WAL file is encrypted using the database key.
- **Statement journals**: encrypted; when file-based temp is disabled, these remain in memory.
- **Master journal**: does not contain data (it contains pathnames for rollback journals).
- **Other transient files are not encrypted**: SQLite can write temporary files for sorts, indexes, etc. To avoid plaintext transient spill to disk, we must disable file-based temporary storage at compile-time **and** enforce in-memory temp storage at runtime as a defense-in-depth measure.

Operational guidance:

- Treat `*.db`, `*-wal`, `*-shm`, and `*-journal` as sensitive and ensure they live in a private directory with restrictive permissions.
- **Compile-time**: Prefer building SQLCipher with `SQLITE_TEMP_STORE=3` (“always use memory for temporary storage”) when feasible.
  - The common Rust SQLCipher bundle uses `SQLITE_TEMP_STORE=2` (temp is in-memory unless `temp_store` is explicitly forced to file), and Android builds commonly use `SQLITE_TEMP_STORE=3`.
- **Runtime**: Always set `PRAGMA temp_store = MEMORY;` on **each connection** as an additional safeguard, even if compile-time settings should prevent file-based temp storage.

### 2. Secure Storage via `keyring-core`

Instead of creating a custom secure-storage abstraction, MDK uses the [`keyring-core`](https://crates.io/crates/keyring-core) ecosystem directly. This provides:

- A well-maintained, community-supported cross-platform credential storage API
- Native platform stores for all major platforms (including Android and iOS)
- Thread-safe `Send + Sync` credentials by design
- Built-in mock store for testing

#### `keyring-core` API Overview

The `keyring-core` crate provides a simple API for credential storage:

```rust
use keyring_core::{Entry, set_default_store, get_default_store, Result};

// Set the default credential store (platform-specific)
set_default_store(my_platform_store);

// Create an entry and manage secrets
// Use a host-provided service identifier (recommend: reverse-DNS / bundle id) to avoid collisions.
let entry = Entry::new("com.example.app", "mdk.db.key.default")?;
entry.set_secret(b"32-byte-encryption-key-here...")?;
let secret: Vec<u8> = entry.get_secret()?;
entry.delete_credential()?;
```

#### Key `keyring-core` Types

| Type | Description |
|------|-------------|
| `Entry` | A named credential in a store (identified by service + user) |
| `CredentialStore` | `Box<dyn CredentialStoreApi + Send + Sync>` — thread-safe store |
| `Credential` | `Box<dyn CredentialApi + Send + Sync>` — thread-safe credential |
| `Error` | Error enum including `NoEntry`, `NoStorageAccess`, etc. |

#### Platform Store Initialization

Each platform has a dedicated store crate. Most initialize automatically, but some (Android, Flutter) require explicit setup:

**Desktop platforms (macOS, Windows, Linux):**

```rust
// macOS / iOS
use apple_native_keyring_store::AppleStore;
keyring_core::set_default_store(AppleStore::new());

// Windows
use windows_native_keyring_store::WindowsStore;
keyring_core::set_default_store(WindowsStore::new());

// Linux (kernel keyutils)
use linux_keyutils_keyring_store::KeyutilsStore;
keyring_core::set_default_store(KeyutilsStore::new());
```

**Android (requires initialization):**

The `android-native-keyring-store` crate uses JNI to interact with Android Keystore. It requires initialization from the Android runtime.

**Option 1: With `ndk-context` feature** (for Dioxus Mobile, Tauri Mobile, android-activity):

```rust
use android_native_keyring_store::AndroidStore;
use keyring_core::set_default_store;

// Call at app startup
set_default_store(AndroidStore::from_ndk_context().unwrap());
```

**Option 2: Manual initialization via Kotlin** (for Flutter/FRB and other frameworks):

Add this Kotlin code to your Android project:

```kotlin
package io.crates.keyring

import android.content.Context

class Keyring {
    companion object {
        init {
            // Load the native library containing android-native-keyring-store
            System.loadLibrary("your_rust_lib")
        }

        external fun setAndroidKeyringCredentialBuilder(context: Context)
    }
}
```

Then call from MainActivity:

```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Keyring.setAndroidKeyringCredentialBuilder(this)
        // Now keyring-core can be used from Rust
    }
}
```

**Flutter:**

Flutter apps can use `android-native-keyring-store` on Android with the same Kotlin initialization approach shown above. On iOS, `apple-native-keyring-store` works automatically. Add the Kotlin initialization code to your Flutter project's `MainActivity.kt`.

### 3. Integration with MDK

#### How MDK Uses `keyring-core`

**Key identifiers (important):**

- MDK uses `Entry::new(service_id, db_key_id)` to store the database encryption key.
- `service_id` should be a **stable, host-defined application identifier** (recommend: reverse-DNS / bundle id like `"com.example.app"`). This prevents collisions between multiple apps on the same OS user account.
- The `db_key_id` should be a **stable, host-defined identifier** (e.g., `"mdk.db.key.default"` or `"mdk.db.key.<profile_id>"`).
- **Do not derive the key identifier from an absolute `db_path`** (hashing paths is fragile across reinstalls, sandbox path changes, migrations, and renames).
- Neither identifier is secret; they are indexes into secure storage.

**Failure modes (expected and user-actionable):**

- If the database file exists but the keyring entry is missing, MDK must return a clear error (do **not** generate a new key and silently “brick” the existing database).
- Distinguish wrong key vs missing key vs plaintext database encountered when encryption is required vs corrupted database vs “secure storage unavailable / not initialized”.

```rust
// mdk-sqlite-storage/src/lib.rs

use keyring_core::{Entry, Error as KeyringError};

impl MdkSqliteStorage {
    /// Creates encrypted storage using the default keyring store.
    ///
    /// This is the primary constructor for production use. The keyring store
    /// must be initialized before calling this (see platform-specific setup).
    pub fn new<P>(file_path: P, service_id: &str, db_key_id: &str) -> Result<Self, Error>
    where
        P: AsRef<Path>,
    {
        // Get or create the 32-byte encryption key
        let key = get_or_create_db_key(service_id, db_key_id)?;
        let key_array: [u8; 32] = key
            .try_into()
            .map_err(|_| Error::InvalidKeyLength)?;

        let config = EncryptionConfig { key: key_array };
        Self::new_internal(file_path, Some(config))
    }

    /// Creates unencrypted storage.
    ///
    /// ⚠️ **WARNING**: This creates an unencrypted database. Only use for testing
    /// or development. Production applications should use `new()` with encrypted
    /// storage.
    pub fn new_unencrypted<P>(file_path: P) -> Result<Self, Error>
    where
        P: AsRef<Path>,
    {
        Self::new_internal(file_path, None)
    }

    fn new_internal<P>(file_path: P, config: Option<EncryptionConfig>) -> Result<Self, Error>
    where
        P: AsRef<Path>,
    {
        // Implementation details...
    }
}

/// Get an existing DB encryption key or generate and store a new one.
///
/// **Concurrency:** This operation must be atomic (at least within the current process).
fn get_or_create_db_key(service_id: &str, db_key_id: &str) -> Result<Vec<u8>, Error> {
    use std::sync::{Mutex, OnceLock};

    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    let _guard = LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();

    let entry = Entry::new(service_id, db_key_id)?;

    // Try to get existing key
    match entry.get_secret() {
        Ok(secret) => return Ok(secret),
        Err(KeyringError::NoEntry) => {
            // Key doesn't exist, generate a new one
        }
        Err(e) => return Err(e.into()),
    }

    // Generate a new 32-byte key
    let mut new_key = vec![0u8; 32];
    getrandom::getrandom(&mut new_key)
        .map_err(|e| Error::KeyGeneration(e.to_string()))?;

    // Store it
    entry.set_secret(&new_key)?;

    Ok(new_key)
}
```

**Note on concurrency:** the in-process mutex only coordinates within a single process. If a host can start multiple processes that open the same profile concurrently, the host should provide higher-level coordination.

#### Cargo.toml Changes

**Implemented configuration** (encryption is always enabled, platform stores are host responsibility):

```toml
# Workspace Cargo.toml
[workspace.dependencies]
keyring-core = "0.7"  # v4 architecture - see https://github.com/open-source-cooperative/keyring-rs/issues/259
getrandom = "0.3"
hex = "0.4"

# mdk-sqlite-storage/Cargo.toml
[dependencies]
keyring-core.workspace = true
getrandom.workspace = true
hex = { workspace = true, features = ["std"] }
rusqlite = { workspace = true, features = ["bundled-sqlcipher"] }
```

**Note:** Platform-specific store crates (`apple-native-keyring-store`, `android-native-keyring-store`, etc.) are **not bundled** with MDK. The host application is responsible for:

1. Adding the appropriate platform store crate to their dependencies
2. Initializing the store at app startup via `keyring_core::set_default_store(...)`
3. Calling `MdkSqliteStorage::new(path, service_id, db_key_id)`

This design keeps MDK platform-agnostic and avoids pulling in unnecessary platform-specific dependencies.

### 4. Project-Specific Usage Examples (see Part B)

Downstream integrations (including `whitenoise-rs`) are documented in **Part B** as non-normative examples.

### 7. File Permission Hardening

**Goal:** prevent other local users/processes from reading the encrypted database files.

#### Unix-like (macOS/Linux/etc.)

Create the database directory with mode `0700` (owner read/write/execute only) and database files with mode `0600` (owner read/write only). Execute permission is not needed for files.

**Footgun avoidance (recommended):**

- Only apply restrictive permissions to directories/files created for the MDK database (or a dedicated MDK subdirectory). Do not `chmod` arbitrary existing parent directories provided by the host.
- To avoid a short window where SQLite creates a new database file with default permissions (umask-dependent), prefer **pre-creating** the database file with mode `0600` before opening it.
- Policy recommendation: if an existing database directory or file is too-permissive, **fail closed** (return an error) rather than silently continuing.

#### iOS/Android

Rely on the application sandbox, but still store databases in app-private directories.

#### Windows

Windows does not have Unix-style chmod permissions. Instead, Windows uses Access Control Lists (ACLs) within security descriptors:

- **DACL (Discretionary Access Control List)**: Specifies which users/groups can access the file and what operations they can perform.
- **SACL (System Access Control List)**: Used for auditing (not required for our use case).

**Implementation approach for Windows (host responsibility):**

1. **Store in per-user locations**: Always store database files in the user's private app data directory (e.g., `%LOCALAPPDATA%\<app_name>\`). This provides baseline isolation since other non-admin users cannot access these directories by default.

2. **Apply explicit ACL restrictions**: Use Windows APIs to set a DACL that grants access only to the current user:
   - Use `SetNamedSecurityInfoW` or `SetSecurityInfo` to modify the file's security descriptor.
   - Create a DACL with a single ACE (Access Control Entry) granting `GENERIC_ALL` to the current user's SID.
   - Disable inheritance from parent directories to prevent inherited permissions from granting broader access.

MDK does not currently implement Windows ACL hardening internally. This is intentionally left to host applications because correct ACL handling is subtle and requires Windows-specific testing (inheritance, effective permissions, and principal selection).

**Reference implementation sketch (conceptual):**

```rust
#[cfg(windows)]
fn set_secure_file_permissions_windows(path: &Path) -> std::io::Result<()> {
    use windows::Win32::Security::{
        SetNamedSecurityInfoW, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
        PROTECTED_DACL_SECURITY_INFORMATION,
    };
    use windows::Win32::Security::Authorization::{
        SetEntriesInAclW, EXPLICIT_ACCESS_W, SET_ACCESS, NO_INHERITANCE,
        TRUSTEE_IS_SID, TRUSTEE_W,
    };
    use windows::Win32::System::Threading::GetCurrentProcess;
    use windows::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_USER};

    // 1. Get current user's SID from process token
    // 2. Build an EXPLICIT_ACCESS entry granting GENERIC_ALL to current user
    // 3. Create a new ACL with SetEntriesInAclW
    // 4. Apply it with SetNamedSecurityInfoW, using:
    //    - DACL_SECURITY_INFORMATION to set the DACL
    //    - PROTECTED_DACL_SECURITY_INFORMATION to disable inheritance

    // Implementation details TBD during development phase
    Ok(())
}
```

**Full implementation example (for reference):**

```rust
#[cfg(windows)]
mod windows_permissions {
    use std::path::Path;
    use std::ptr;
    use windows::core::PCWSTR;
    use windows::Win32::Foundation::{CloseHandle, HANDLE, PSID};
    use windows::Win32::Security::Authorization::{
        SetEntriesInAclW, EXPLICIT_ACCESS_W, SET_ACCESS, NO_INHERITANCE,
        TRUSTEE_IS_SID, TRUSTEE_W, TRUSTEE_FORM, TRUSTEE_TYPE,
    };
    use windows::Win32::Security::{
        GetTokenInformation, SetNamedSecurityInfoW, TokenUser,
        DACL_SECURITY_INFORMATION, PROTECTED_DACL_SECURITY_INFORMATION,
        SE_FILE_OBJECT, TOKEN_QUERY, TOKEN_USER, ACL,
    };
    use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
    use windows::Win32::Storage::FileSystem::FILE_ALL_ACCESS;

    pub fn set_owner_only_permissions(path: &Path) -> std::io::Result<()> {
        // Get current user SID
        let sid = get_current_user_sid()?;

        // Build explicit access for current user only
        let mut explicit_access = EXPLICIT_ACCESS_W {
            grfAccessPermissions: FILE_ALL_ACCESS.0,
            grfAccessMode: SET_ACCESS,
            grfInheritance: NO_INHERITANCE,
            Trustee: TRUSTEE_W {
                TrusteeForm: TRUSTEE_IS_SID,
                TrusteeType: TRUSTEE_TYPE::default(),
                ptstrName: PCWSTR(sid.0 as *const u16),
                ..Default::default()
            },
        };

        // Create new ACL with only this entry
        let mut new_acl: *mut ACL = ptr::null_mut();
        unsafe {
            SetEntriesInAclW(
                Some(&[explicit_access]),
                None,
                &mut new_acl,
            )?;
        }

        // Apply to file (with protected DACL to disable inheritance)
        let path_wide: Vec<u16> = path.as_os_str()
            .encode_wide()
            .chain(std::iter::once(0))
            .collect();

        unsafe {
            SetNamedSecurityInfoW(
                PCWSTR(path_wide.as_ptr()),
                SE_FILE_OBJECT,
                DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
                None,
                None,
                Some(new_acl),
                None,
            )?;
        }

        Ok(())
    }

    fn get_current_user_sid() -> std::io::Result<PSID> {
        // Implementation: OpenProcessToken, GetTokenInformation(TokenUser), extract SID
        // ...
        todo!("Implement SID retrieval")
    }
}
```

#### Combined implementation

```rust
// mdk-sqlite-storage/src/lib.rs

#[cfg(unix)]
fn create_secure_directory(path: &Path) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;

    std::fs::create_dir_all(path)?;
    let perms = std::fs::Permissions::from_mode(0o700);
    std::fs::set_permissions(path, perms)?;
    Ok(())
}

#[cfg(unix)]
fn set_secure_file_permissions(path: &Path) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;

    if path.exists() {
        let perms = std::fs::Permissions::from_mode(0o600);
        std::fs::set_permissions(path, perms)?;
    }
    Ok(())
}

#[cfg(not(unix))]
fn create_secure_directory(path: &Path) -> std::io::Result<()> {
    // On iOS/Android, the app sandbox generally restricts filesystem access.
    std::fs::create_dir_all(path)
}

#[cfg(not(unix))]
fn set_secure_file_permissions(_path: &Path) -> std::io::Result<()> {
    // On mobile platforms, we rely on app sandboxing.
    Ok(())
}
```

---

## Implementation Tasks

### Phase 1: Validate `keyring-core` Ecosystem

- [x] Verify MSRV compatibility (MDK requires Rust 1.90.0) — `keyring-core` 0.7.x compiles with Rust 1.90.0
- [ ] Test `keyring-core` + `apple-native-keyring-store` on macOS
- [ ] Test `keyring-core` + `android-native-keyring-store` on Android emulator
- [x] Document any initialization quirks or platform-specific requirements — documented in `keyring` module and lib.rs docs
- [x] Evaluate `keyring-core` error handling for our use cases — added `Error::Keyring` and `Error::KeyringNotInitialized` variants

### Phase 2: SQLCipher Integration in `mdk-sqlite-storage`

- [x] Update `Cargo.toml` to use `bundled-sqlcipher` feature
- [x] Add `keyring-core` dependency (platform stores are host responsibility, not bundled)
- [x] Add `EncryptionConfig` struct
- [x] Implement `get_or_create_db_key(service_id, db_key_id)` helper using `keyring_core::Entry`
- [x] Rename existing unencrypted constructor to `MdkSqliteStorage::new_unencrypted()`
- [x] Add `MdkSqliteStorage::new(file_path, service_id, db_key_id)` (encrypted) as the primary constructor
- [x] Also added `MdkSqliteStorage::new_with_key(file_path, config)` for direct key injection
- [x] Apply SQLCipher pragmas on **each** new connection before any migrations / foreign key pragmas:
  - [x] `PRAGMA key = "x'...'"` (**must be the first operation**)
  - [x] `PRAGMA cipher_compatibility = 4;`
  - [x] `PRAGMA temp_store = MEMORY;`
  - [x] Validate with a read (e.g., `SELECT count(*) FROM sqlite_master;`) to distinguish wrong key from other failures
- [x] **Compile-time**: Investigate temp-store hardening — **Findings**: Default `SQLITE_TEMP_STORE=2` + runtime `PRAGMA temp_store = MEMORY` is sufficient; Android already uses `=3`. Users can optionally set `LIBSQLITE3_FLAGS="SQLITE_TEMP_STORE=3"` for maximum hardening. See `SECURITY.md` for details.
- [x] Add explicit errors for missing key for an existing DB, wrong key, plaintext DB when encryption is required, corrupted DB, and secure-storage-unavailable/uninitialized
- [x] Add file permission hardening for Unix platforms (0700 for directories, 0600 for files; avoid `chmod` on arbitrary existing directories; pre-create DB file with 0600 to avoid permission races)
- [X] Windows filesystem hardening (ACLs) is left to host applications (see `SECURITY.md`)
- [x] Add unit tests for encrypted storage
- [ ] Test cross-platform compilation (iOS, Android, macOS, Linux, Windows)

### Phase 3: Android Integration Testing

- [ ] Set up test project with `android-native-keyring-store`
- [ ] Test manual Kotlin initialization path (for Flutter compatibility)
- [ ] Test `ndk-context` initialization path (for native Android apps)
- [ ] Document Android-specific setup in README
- [ ] Verify credential persistence across app restarts

### Phase 4: UniFFI Binding Updates

- [x] Add `new_mdk(db_path, service_id, db_key_id)` as the primary constructor (encrypted by default)
- [x] Add `new_mdk_unencrypted(db_path)` for testing/development use with clear warnings
- [x] Add `new_mdk_with_key(db_path, encryption_key)` for direct key injection
- [ ] Export keyring store initialization functions if needed
- [x] Update documentation for Swift, Kotlin, Python, Ruby (updated README.md and language-specific docs)
- [x] Add documentation for platform-specific store setup

### Phase 5: Migration Support

- [ ] Add utility to migrate unencrypted database to encrypted
- [ ] Add utility to re-key encrypted database
- [ ] Handle edge case: app upgrade from unencrypted to encrypted storage
- [ ] Document supported migration paths and failure modes (e.g., missing key vs wrong key vs corrupt DB)

---

## Security Considerations

### Key Storage Best Practices

1. **Never log or expose the encryption key**
2. **Use platform-specific secure storage** - Don't store keys in SharedPreferences, UserDefaults, or files
3. **Use device-bound keys where possible** - Prefer `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` on iOS
4. **Consider biometric protection** - For high-security use cases, require biometric auth to access the key
5. **Use an app-unique `service_id`** (reverse-DNS / bundle id) when storing credentials to avoid cross-application collisions on shared OS key stores

### Android-Specific Security Notes

1. **Use EncryptedSharedPreferences** - This is backed by Android Keystore but allows storing arbitrary secrets
2. **Avoid plain SharedPreferences** - Even with obfuscation, this is not secure
3. **MasterKey.KeyScheme.AES256_GCM** - Use the strongest available key scheme
4. **Android Keystore limitations** - Keys generated in Keystore cannot be exported as raw bytes, which is why we use EncryptedSharedPreferences for SQLCipher keys
5. **Minimum API level** - EncryptedSharedPreferences requires API 23+ (Android 6.0+)

### Database Security

1. **SQLCipher security design** - SQLCipher encrypts pages with 256-bit AES-CBC and authenticates page writes with HMAC-SHA512 (see SQLCipher design docs).
2. **Keying is explicit** - Use `PRAGMA key = "x'...'"` (raw 32-byte key data), then `PRAGMA cipher_compatibility = 4;` and `PRAGMA temp_store = MEMORY;`, and validate with a read (e.g., `SELECT count(*) FROM sqlite_master;`).
3. **Defaults, but pinned** - Use `PRAGMA cipher_compatibility = 4;` on each connection to avoid unexpected default changes across SQLCipher major versions.
4. **Sidecar + temp files** - WAL/journal page data is encrypted, but other transient files are not; ensure in-memory temp store and strict directory permissions.
5. **Optional hardening** - Consider `PRAGMA cipher_memory_security = ON` if the performance impact is acceptable.

### Backup / Restore (Not Supported Yet)

MDK does not currently provide backup/restore/export tooling. Hosts should assume that copying the database file(s) alone is insufficient without a compatible key management strategy.

### Breaking Changes and API Design

As part of the security audit work, MDK is making breaking changes to establish secure defaults:

- **Encrypted storage is the default**: The primary constructor (`new()`, `new_mdk()`) creates encrypted storage.
- **Unencrypted storage is explicitly opt-in**: Use `new_unencrypted()` / `new_unencrypted_mdk()` with clear warnings.
- **No backwards compatibility shims**: We are not maintaining deprecated APIs for unencrypted storage. Existing users must migrate to encrypted storage.

### Trust Boundaries with `keyring-core`

MDK trusts the `keyring-core` ecosystem and its platform-native credential stores. Key trust boundaries:

1. **Native stores are trusted**: We rely on Apple Keychain, Windows Credential Manager, Linux Secret Service, and Android Keystore to protect secrets appropriately.
2. **Store initialization**: On Android, the app must call `setAndroidKeyringCredentialBuilder(context)` before MDK can store secrets. Failure to initialize results in clear errors.

---

## Testing Strategy

### Unit Tests

```rust
#[test]
fn test_encrypted_storage_creation() {
    let temp_dir = tempdir().unwrap();
    let db_path = temp_dir.path().join("encrypted.db");
    let key = [0u8; 32]; // Test key

    let config = EncryptionConfig { key };
    let storage = MdkSqliteStorage::new(&db_path, Some(config));
    assert!(storage.is_ok());
}

#[test]
fn test_encrypted_storage_wrong_key_fails() {
    let temp_dir = tempdir().unwrap();
    let db_path = temp_dir.path().join("encrypted.db");

    // Create with key1
    let key1 = [1u8; 32];
    let config1 = EncryptionConfig { key: key1 };
    let storage1 = MdkSqliteStorage::new(&db_path, Some(config1)).unwrap();
    drop(storage1);

    // Try to open with key2
    let key2 = [2u8; 32];
    let config2 = EncryptionConfig { key: key2 };
    let result = MdkSqliteStorage::new(&db_path, Some(config2));
    assert!(result.is_err());
}

#[test]
fn test_unencrypted_cannot_read_encrypted() {
    let temp_dir = tempdir().unwrap();
    let db_path = temp_dir.path().join("encrypted.db");

    // Create encrypted database
    let key = [0u8; 32];
    let config = EncryptionConfig { key };
    let storage = MdkSqliteStorage::new(&db_path, Some(config)).unwrap();
    drop(storage);

    // Try to open without encryption
    let result = MdkSqliteStorage::new(&db_path, None);
    assert!(result.is_err());
}
```

### Integration Tests

- [ ] Test on iOS Simulator
- [ ] Test on Android Emulator
- [ ] Test host callback integration on both platforms (native iOS + native Android)
- [ ] Performance benchmarks (encryption overhead)

---

## References

### SQLCipher

- [SQLCipher Design]https://www.zetetic.net/sqlcipher/design/
- [SQLCipher Documentation]https://www.zetetic.net/sqlcipher/sqlcipher-api/
- [rusqlite SQLCipher feature]https://github.com/rusqlite/rusqlite#optional-features

### keyring-core Ecosystem

- [keyring-core crate]https://crates.io/crates/keyring-core
- [keyring-core documentation]https://docs.rs/keyring-core/
- [keyring-core GitHub]https://github.com/open-source-cooperative/keyring-core
- [Keyring ecosystem wiki]https://github.com/open-source-cooperative/keyring-rs/wiki/Keyring

### Platform Credential Stores

- [apple-native-keyring-store]https://crates.io/crates/apple-native-keyring-store
- [android-native-keyring-store]https://crates.io/crates/android-native-keyring-store[GitHub]https://github.com/open-source-cooperative/android-native-keyring-store
- [windows-native-keyring-store]https://crates.io/crates/windows-native-keyring-store
- [linux-keyutils-keyring-store]https://crates.io/crates/linux-keyutils-keyring-store
- [dbus-secret-service-keyring-store]https://crates.io/crates/dbus-secret-service-keyring-store
- [zbus-secret-service-keyring-store]https://crates.io/crates/zbus-secret-service-keyring-store

### Platform Documentation

- [iOS Keychain Services]https://developer.apple.com/documentation/security/keychain_services
- [Android Keystore]https://developer.android.com/training/articles/keystore
- [Android EncryptedSharedPreferences]https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences
- [AndroidX Security Crypto Library]https://developer.android.com/jetpack/androidx/releases/security

### UniFFI

- [UniFFI Callback Interfaces]https://mozilla.github.io/uniffi-rs/latest/udl/callback_interfaces.html

---

## Part B: `whitenoise-rs` / Flutter Integration Notes (Non-normative)

This section captures downstream work that is useful for `whitenoise-rs`, but is intentionally separated from the MDK-generic design.

### Background: Current whitenoise-rs Key Storage Problem

`whitenoise-rs` (which depends on MDK) currently handles Nostr key storage using:

- `keyring` crate (v3) for most platforms
- Android: file-based obfuscation (not secure)

With `keyring-core`, `whitenoise-rs` can use the same credential storage for:

- The SQLCipher DB encryption key (MDK storage)
- Nostr secret keys (whitenoise-rs)

### Strategy (Downstream)

With `keyring-core`, the strategy is simpler:

| Platform | Store | Initialization |
|----------|-------|----------------|
| macOS | `apple-native-keyring-store` | Automatic |
| iOS | `apple-native-keyring-store` | Automatic |
| Windows | `windows-native-keyring-store` | Automatic |
| Linux | `linux-keyutils-keyring-store` | Automatic |
| Android (native) | `android-native-keyring-store` | Kotlin init required |
| Flutter (Android) | `android-native-keyring-store` | Kotlin init required |
| Flutter (iOS) | `apple-native-keyring-store` | Automatic |

### `whitenoise-rs` Usage (Sketch)

```rust
// In whitenoise-rs (sketch)

use keyring_core::Entry;

fn open_mdk(db_path: &Path) -> Result<MDK<MdkSqliteStorage>, Error> {
    // keyring-core store must be initialized before this call
    let service_id = "com.whitenoise.app";
    let mdk_storage = MdkSqliteStorage::new(db_path, service_id, "mdk.db.key.default")?;
    Ok(MDK::new(mdk_storage))
}

fn get_or_create_nostr_key() -> Result<Vec<u8>, Error> {
    let service_id = "com.whitenoise.app";
    let entry = Entry::new(service_id, "nostr.secret_key.default")?;

    match entry.get_secret() {
        Ok(secret) => return Ok(secret),
        Err(keyring_core::Error::NoEntry) => {
            // Generate new key
            let mut key = vec![0u8; 32];
            getrandom::getrandom(&mut key)?;
            entry.set_secret(&key)?;
            Ok(key)
        }
        Err(e) => Err(e.into()),
    }
}
```

### Flutter Integration

Flutter apps use the native keyring stores directly:

- **iOS**: `apple-native-keyring-store` works automatically
- **Android**: `android-native-keyring-store` with Kotlin initialization in `MainActivity.kt`

This is simpler than a callback-based approach and uses the same secure storage mechanisms.

### Downstream Tasks (whitenoise / Flutter)

- [ ] Update `whitenoise-rs` to use `keyring-core` instead of `keyring` v3
- [ ] Add platform-specific store initialization for Android (including Flutter)
- [ ] Test on all platforms (macOS, iOS, Windows, Linux, Android)
- [ ] Replace any Android file-obfuscation key storage with `android-native-keyring-store`