Skip to content

Add storage adapter nostr-mls-slqcipher-storage #932

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"mls/nostr-mls",
"mls/nostr-mls-memory-storage",
"mls/nostr-mls-sqlite-storage",
"mls/nostr-mls-sqlcipher-storage",
"mls/nostr-mls-storage",
]
default-members = ["crates/*"]
Expand Down Expand Up @@ -40,6 +41,7 @@ nostr-indexeddb = { version = "0.42", path = "./database/nostr-indexeddb", defau
nostr-lmdb = { version = "0.42", path = "./database/nostr-lmdb", default-features = false }
nostr-mls-memory-storage = { version = "0.42", path = "./mls/nostr-mls-memory-storage", default-features = false }
nostr-mls-sqlite-storage = { version = "0.42", path = "./mls/nostr-mls-sqlite-storage", default-features = false }
nostr-mls-sqlcipher-storage = { version = "0.1", path = "./mls/nostr-mls-sqlcipher-storage", default-features = false }
nostr-mls-storage = { version = "0.42", path = "./mls/nostr-mls-storage", default-features = false }
nostr-mls = { version = "0.42", path = "./mls/nostr-mls", default-features = false }
nostr-ndb = { version = "0.42", path = "./database/nostr-ndb", default-features = false }
Expand Down
31 changes: 31 additions & 0 deletions mls/nostr-mls-sqlcipher-storage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "nostr-mls-sqlcipher-storage"
version = "0.1.0"
edition = "2021"
description = "SQLCipher-encrypted SQLite database for nostr-mls that implements the NostrMlsStorageProvider Trait"
authors = ["Jeff Gardner <j@jeffg.me>", "Yuki Kishimoto <yukikishimoto@protonmail.com>", "Rust Nostr Developers"]
homepage.workspace = true
repository.workspace = true
license.workspace = true
readme = "README.md"
rust-version = "1.74.0"
keywords = ["nostr", "mls", "openmls", "sqlite"]

[dependencies]
nostr = { workspace = true, features = ["std"] }
nostr-mls-storage.workspace = true
openmls = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false }
openmls_sqlite_storage = { git = "https://github.com/openmls/openmls", rev = "4cc0f594b11262083ad9827b3b2033052c6ef99f", default-features = false }
refinery = { version = "0.8", features = ["rusqlite"] } # MSRV is 1.75.0
rusqlite = { version = "0.32", default-features = false }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
tracing = { workspace = true, features = ["std"] }

[features]
default = ["bundled-sqlcipher-vendored-openssl"]
bundled-sqlcipher = ["rusqlite/bundled-sqlcipher"]
bundled-sqlcipher-vendored-openssl = ["rusqlite/bundled-sqlcipher-vendored-openssl"]

[dev-dependencies]
tempfile.workspace = true
34 changes: 34 additions & 0 deletions mls/nostr-mls-sqlcipher-storage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Nostr MLS SQLCipher Storage

SQLCipher MLS storage backend for nostr apps

## State

**This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways.

## Features

This crate provides two cargo features to control how SQLCipher and its crypto backend are packaged:

| Feature | Description |
| ------- | ----------- |
| default (`bundled-sqlcipher-vendored-openssl`) | Builds SQLCipher from source and statically links it together with a vendored OpenSSL via the `openssl-sys` crate. Ideal for fully self-contained binaries without system dependencies. |
| `bundled-sqlcipher` | Builds SQLCipher from source but relies on the system OpenSSL / LibreSSL / Security.framework for the crypto implementation. |

### Build examples

```bash
# 1. Default build: SQLCipher + vendored (statically linked) OpenSSL
cargo build

# 2. Use system crypto library, still bundle SQLCipher
cargo build --no-default-features --features bundled-sqlcipher
```

## Donations

`rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate).

## License

This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details
85 changes: 85 additions & 0 deletions mls/nostr-mls-sqlcipher-storage/examples/encrypted_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Example demonstrating SQLCipher encryption features
//!
//! This example shows how to:
//! - Create an encrypted database
//! - Open an encrypted database with a password
//! - Change the password of an encrypted database
//! - Handle encryption-related errors

use std::env;
use nostr_mls_sqlcipher_storage::{NostrMlsSqliteStorage, error::Error};

fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("SQLCipher Encryption Example");
println!("============================");

// Example 1: Create an encrypted database
println!("\n1. Creating an encrypted database...");
let password = "my_secret_password_123";
let storage = NostrMlsSqliteStorage::new_with_password("encrypted_example.db", Some(password))?;
println!("✓ Encrypted database created successfully");

// Example 2: Check if database is encrypted (this will always return false for an open connection with correct password)
match storage.is_encrypted() {
Ok(encrypted) => println!("✓ Database encryption status: {}", if encrypted { "encrypted" } else { "accessible (correct password or unencrypted)" }),
Err(e) => println!("⚠ Could not check encryption status: {}", e),
}

// Example 3: Change password
println!("\n2. Changing database password...");
let new_password = "new_secret_password_456";
storage.change_password(Some(new_password))?;
println!("✓ Password changed successfully");

// Close the current connection
drop(storage);

// Example 4: Try to open with old password (should fail)
println!("\n3. Testing old password (should fail)...");
match NostrMlsSqliteStorage::new_with_password("encrypted_example.db", Some(password)) {
Ok(_) => println!("⚠ Unexpected: Old password still works"),
Err(e) => println!("✓ Expected: Old password rejected - {}", e),
}

// Example 5: Open with new password (should work)
println!("\n4. Opening with new password...");
let storage = NostrMlsSqliteStorage::new_with_password("encrypted_example.db", Some(new_password))?;
println!("✓ Successfully opened with new password");

// Example 6: Try to remove encryption (will fail - not supported)
println!("\n5. Attempting to remove encryption (should fail)...");
match storage.change_password(None) {
Ok(_) => println!("⚠ Unexpected: Encryption removal succeeded"),
Err(Error::Database(msg)) if msg.contains("not currently supported") => {
println!("✓ Expected: Encryption removal not supported - {}", msg);
}
Err(e) => println!("⚠ Unexpected error: {}", e),
}

// Example 7: Environment-based password
println!("\n6. Environment-based password example...");
env::set_var("DATABASE_PASSWORD", "env_password_789");
let env_password = env::var("DATABASE_PASSWORD").ok();
let env_storage = NostrMlsSqliteStorage::new_with_password(
"env_encrypted_example.db",
env_password.as_deref()
)?;
println!("✓ Database created with environment-based password");

// Clean up
drop(storage);
drop(env_storage);

// Clean up files
let _ = std::fs::remove_file("encrypted_example.db");
let _ = std::fs::remove_file("env_encrypted_example.db");

println!("\n✓ Example completed successfully!");
println!("\nKey takeaways:");
println!("- Use `new_with_password()` to create encrypted databases");
println!("- Use `change_password()` to change encryption passwords");
println!("- Store passwords securely (environment variables, secure vaults, etc.)");
println!("- Removing encryption is not currently supported");

Ok(())
}
117 changes: 117 additions & 0 deletions mls/nostr-mls-sqlcipher-storage/migrations/V100__initial.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
-- Initial database schema for nostr-mls-sqlite-storage

-- Groups table
CREATE TABLE IF NOT EXISTS groups (
mls_group_id BLOB PRIMARY KEY,
nostr_group_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
admin_pubkeys JSONB NOT NULL,
last_message_id BLOB, -- Event ID as byte array
last_message_at INTEGER,
group_type TEXT NOT NULL,
epoch INTEGER NOT NULL,
state TEXT NOT NULL
);

-- Create unique index on nostr_group_id
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_nostr_group_id ON groups(nostr_group_id);

-- Group Relays table
CREATE TABLE IF NOT EXISTS group_relays (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mls_group_id BLOB NOT NULL,
relay_url TEXT NOT NULL,
FOREIGN KEY (mls_group_id) REFERENCES groups(mls_group_id) ON DELETE CASCADE,
UNIQUE(mls_group_id, relay_url)
);

-- Create index on mls_group_id for faster lookups
CREATE INDEX IF NOT EXISTS idx_group_relays_mls_group_id ON group_relays(mls_group_id);

-- Group Exporter Secrets table
CREATE TABLE IF NOT EXISTS group_exporter_secrets (
mls_group_id BLOB NOT NULL,
epoch INTEGER NOT NULL,
secret BLOB NOT NULL,
PRIMARY KEY (mls_group_id, epoch)
);

-- Create index on mls_group_id for faster lookups
CREATE INDEX IF NOT EXISTS idx_group_exporter_secrets_mls_group_id ON group_exporter_secrets(mls_group_id);

-- Messages table
CREATE TABLE IF NOT EXISTS messages (
id BLOB PRIMARY KEY, -- Event ID as byte array
pubkey BLOB NOT NULL, -- Pubkey as byte array
kind INTEGER NOT NULL,
mls_group_id BLOB NOT NULL,
created_at INTEGER NOT NULL,
content TEXT NOT NULL,
tags JSONB NOT NULL,
event JSONB NOT NULL,
wrapper_event_id BLOB NOT NULL, -- Wrapper event ID as byte array
state TEXT NOT NULL,
FOREIGN KEY (mls_group_id) REFERENCES groups(mls_group_id) ON DELETE CASCADE
);

-- Create indexes on messages table
CREATE INDEX IF NOT EXISTS idx_messages_mls_group_id ON messages(mls_group_id);
CREATE INDEX IF NOT EXISTS idx_messages_wrapper_event_id ON messages(wrapper_event_id);
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
CREATE INDEX IF NOT EXISTS idx_messages_pubkey ON messages(pubkey);
CREATE INDEX IF NOT EXISTS idx_messages_kind ON messages(kind);
CREATE INDEX IF NOT EXISTS idx_messages_state ON messages(state);

-- Processed Messages table
CREATE TABLE IF NOT EXISTS processed_messages (
wrapper_event_id BLOB PRIMARY KEY, -- Wrapper event ID as byte array
message_event_id BLOB, -- Message event ID as byte array
processed_at INTEGER NOT NULL,
state TEXT NOT NULL,
failure_reason TEXT
);

-- Create index on message_event_id for faster lookups
CREATE INDEX IF NOT EXISTS idx_processed_messages_message_event_id ON processed_messages(message_event_id);
CREATE INDEX IF NOT EXISTS idx_processed_messages_state ON processed_messages(state);
CREATE INDEX IF NOT EXISTS idx_processed_messages_processed_at ON processed_messages(processed_at);
CREATE INDEX IF NOT EXISTS idx_processed_messages_wrapper_event_id ON processed_messages(wrapper_event_id);

-- Welcome messages table
CREATE TABLE IF NOT EXISTS welcomes (
id BLOB PRIMARY KEY, -- Event ID as byte array
event JSONB NOT NULL,
mls_group_id BLOB NOT NULL,
nostr_group_id TEXT NOT NULL,
group_name TEXT NOT NULL,
group_description TEXT NOT NULL,
group_admin_pubkeys JSONB NOT NULL,
group_relays JSONB NOT NULL,
welcomer BLOB NOT NULL, -- pubkey as byte array
member_count INTEGER NOT NULL,
state TEXT NOT NULL,
wrapper_event_id BLOB NOT NULL, -- Wrapper event ID as byte array
FOREIGN KEY (mls_group_id) REFERENCES groups(mls_group_id) ON DELETE CASCADE
);

-- Create indexes on welcomes table
CREATE INDEX IF NOT EXISTS idx_welcomes_mls_group_id ON welcomes(mls_group_id);
CREATE INDEX IF NOT EXISTS idx_welcomes_wrapper_event_id ON welcomes(wrapper_event_id);
CREATE INDEX IF NOT EXISTS idx_welcomes_state ON welcomes(state);
CREATE INDEX IF NOT EXISTS idx_welcomes_nostr_group_id ON welcomes(nostr_group_id);

-- Processed Welcome messages table
CREATE TABLE IF NOT EXISTS processed_welcomes (
wrapper_event_id BLOB PRIMARY KEY, -- Wrapper event ID as byte array
welcome_event_id BLOB, -- Welcome event ID as byte array
processed_at INTEGER NOT NULL,
state TEXT NOT NULL,
failure_reason TEXT
);

-- Create index on welcome_event_id for faster lookups
CREATE INDEX IF NOT EXISTS idx_processed_welcomes_welcome_event_id ON processed_welcomes(welcome_event_id);
CREATE INDEX IF NOT EXISTS idx_processed_welcomes_state ON processed_welcomes(state);
CREATE INDEX IF NOT EXISTS idx_processed_welcomes_processed_at ON processed_welcomes(processed_at);
CREATE INDEX IF NOT EXISTS idx_processed_welcomes_wrapper_event_id ON processed_welcomes(wrapper_event_id);
Loading
Loading