Skip to content
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions aw-datastore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@ authors = ["Johan Bjäreholt <johan@bjareho.lt>"]
edition = "2021"

[features]
default = [] # no features by default
default = ["bundled"]
# Use bundled SQLite (default, no encryption support)
bundled = ["rusqlite/bundled"]
# Use bundled SQLCipher for encrypted databases (mutually exclusive with 'bundled')
# Build with: cargo build --no-default-features --features encryption
# Requires OpenSSL. Use 'encryption-vendored' to vendor OpenSSL as well.
encryption = ["rusqlite/bundled-sqlcipher"]
# Like 'encryption' but also vendors OpenSSL (fully self-contained)
encryption-vendored = ["rusqlite/bundled-sqlcipher-vendored-openssl"]
legacy_import_tests = []

[dependencies]
dirs = "6"
serde = "1.0"
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
rusqlite = { version = "0.30", features = ["chrono", "serde_json", "bundled"] }
rusqlite = { version = "0.30", features = ["chrono", "serde_json"] }
mpsc_requests = "0.3"
log = "0.4"

Expand Down
19 changes: 18 additions & 1 deletion aw-datastore/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#[macro_use]
extern crate log;

use std::fmt;

#[macro_export]
macro_rules! json_map {
{ $( $key:literal : $value:expr),* } => {{
Expand All @@ -22,10 +24,25 @@ mod worker;
pub use self::datastore::DatastoreInstance;
pub use self::worker::Datastore;

#[derive(Debug, Clone)]
#[derive(Clone)]
pub enum DatastoreMethod {
Memory(),
File(String),
/// Encrypted SQLite file using SQLCipher. Only available with the
/// `encryption` or `encryption-vendored` feature flags.
#[cfg(any(feature = "encryption", feature = "encryption-vendored"))]
FileEncrypted(String, String), // (path, key)
Comment thread
TimeToBuildBob marked this conversation as resolved.
}

impl fmt::Debug for DatastoreMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DatastoreMethod::Memory() => write!(f, "Memory()"),
DatastoreMethod::File(p) => write!(f, "File({p:?})"),
#[cfg(any(feature = "encryption", feature = "encryption-vendored"))]
DatastoreMethod::FileEncrypted(p, _) => write!(f, "FileEncrypted({p:?}, <redacted>)"),
}
}
}

/* TODO: Implement this as a proper error */
Expand Down
24 changes: 24 additions & 0 deletions aw-datastore/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ impl DatastoreWorker {
DatastoreMethod::File(path) => {
Connection::open(path).expect("Failed to create datastore")
}
#[cfg(any(feature = "encryption", feature = "encryption-vendored"))]
DatastoreMethod::FileEncrypted(path, key) => {
let conn = Connection::open(path).expect("Failed to create encrypted datastore");
conn.pragma_update(None, "key", key)
.expect("Failed to set SQLCipher encryption key");
// PRAGMA key always succeeds even with a wrong passphrase; the
// first real SQL query is what fails. Read user_version immediately
// to surface an incorrect key as a clear error rather than an
// opaque panic later.
conn.pragma_query_value(None, "user_version", |row| row.get::<_, i64>(0))
.expect("Failed to open encrypted database: wrong passphrase or not an encrypted database");
info!("Opened encrypted database at {}", path);
conn
}
Comment thread
TimeToBuildBob marked this conversation as resolved.
};
let mut ds = DatastoreInstance::new(&conn, true).unwrap();

Expand Down Expand Up @@ -324,6 +338,16 @@ impl Datastore {
Datastore::_new_internal(method, legacy_import)
}

/// Create an encrypted datastore using SQLCipher.
///
/// Requires the `encryption` or `encryption-vendored` feature flag.
/// Build with: `cargo build --no-default-features --features encryption`
#[cfg(any(feature = "encryption", feature = "encryption-vendored"))]
pub fn new_encrypted(dbpath: String, key: String, legacy_import: bool) -> Self {
let method = DatastoreMethod::FileEncrypted(dbpath, key);
Datastore::_new_internal(method, legacy_import)
}

fn _new_internal(method: DatastoreMethod, legacy_import: bool) -> Self {
let (requester, responder) =
mpsc_requests::channel::<Command, Result<Response, DatastoreError>>();
Expand Down
43 changes: 43 additions & 0 deletions aw-datastore/tests/datastore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,4 +531,47 @@ mod datastore_tests {
);
}
}

/// Test that an encrypted datastore can be created, written to, and reopened with the same key
/// with data intact.
#[test]
#[cfg(any(feature = "encryption", feature = "encryption-vendored"))]
fn test_encrypted_datastore_roundtrip() {
use std::fs;
let dir = get_cache_dir().unwrap();
let db_path = dir.join("test-encrypted.db").to_str().unwrap().to_string();
// Clean up from previous runs
let _ = fs::remove_file(&db_path);

let key = "s3cr3t-p@ssw0rd".to_string();

// Create and populate encrypted datastore
{
let ds = Datastore::new_encrypted(db_path.clone(), key.clone(), false);
let bucket = create_test_bucket(&ds);
let e = Event {
id: None,
timestamp: Utc::now(),
duration: Duration::seconds(1),
data: json_map! { "app": "test-encrypted" },
};
let inserted = ds.insert_events(&bucket.id, &[e]).unwrap();
assert_eq!(inserted.len(), 1);
ds.force_commit().unwrap();
ds.close();
}

// Reopen with correct key — data must survive the roundtrip
{
let ds = Datastore::new_encrypted(db_path.clone(), key.clone(), false);
let events = ds
.get_events("testid", None, None, None)
.expect("should read events from encrypted DB after reopen");
assert_eq!(events.len(), 1, "expected 1 event after encrypted reopen");
assert_eq!(events[0].data["app"], "test-encrypted");
ds.close();
}

let _ = fs::remove_file(&db_path);
}
}
13 changes: 11 additions & 2 deletions aw-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ path = "src/lib.rs"
name = "aw-server"
path = "src/main.rs"

[features]
default = ["bundled"]
# Use bundled SQLite (default, no encryption support)
bundled = ["aw-datastore/bundled"]
# Enable SQLCipher encryption support (requires OpenSSL)
# Build with: cargo build --no-default-features --features encryption
encryption = ["aw-datastore/encryption"]
# Enable SQLCipher encryption with vendored OpenSSL (fully self-contained)
encryption-vendored = ["aw-datastore/encryption-vendored"]

[dependencies]
rocket = { version = "0.5.0", features = ["json"] }
rocket_cors = { version = "0.6.0" }
Expand All @@ -29,8 +39,7 @@ uuid = { version = "1.3", features = ["serde", "v4"] }
clap = { version = "4.1", features = ["derive", "cargo"] }
log-panics = { version = "2", features = ["with-backtrace"]}
rust-embed = { version = "8.0.0", features = ["interpolate-folder-path", "debug-embed"] }

aw-datastore = { path = "../aw-datastore" }
aw-datastore = { path = "../aw-datastore", default-features = false }
aw-models = { path = "../aw-models" }
aw-transform = { path = "../aw-transform" }
aw-query = { path = "../aw-query" }
Expand Down
32 changes: 31 additions & 1 deletion aw-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ struct Opts {
/// Don't import from aw-server-python if no aw-server-rust db found
#[clap(long)]
no_legacy_import: bool,

/// Encryption key for the database (requires 'encryption' feature).
/// Can also be set via the AW_DB_PASSWORD environment variable.
/// WARNING: passing a password on the command line may expose it in process listings.
#[clap(long, env = "AW_DB_PASSWORD")]
#[cfg(any(feature = "encryption", feature = "encryption-vendored"))]
db_password: Option<String>,
}

#[rocket::main]
Expand Down Expand Up @@ -141,10 +148,33 @@ async fn main() -> Result<(), rocket::Error> {
device_id::get_device_id()
};

#[cfg(any(feature = "encryption", feature = "encryption-vendored"))]
let datastore = if let Some(key) = opts.db_password {
// Clear the env var immediately so child processes don't inherit the key.
std::env::remove_var("AW_DB_PASSWORD");
info!("Using encrypted database (SQLCipher)");
aw_datastore::Datastore::new_encrypted(db_path, key, legacy_import)
} else {
aw_datastore::Datastore::new(db_path, legacy_import)
};
#[cfg(not(any(feature = "encryption", feature = "encryption-vendored")))]
{
if std::env::var("AW_DB_PASSWORD").is_ok() {
panic!(
"AW_DB_PASSWORD is set but this binary was not compiled with encryption support. \
Refusing to start with an unencrypted database when the user requested encryption. \
Rebuild with the 'encryption' or 'encryption-vendored' feature, or unset \
AW_DB_PASSWORD to use an unencrypted database."
);
}
}
#[cfg(not(any(feature = "encryption", feature = "encryption-vendored")))]
let datastore = aw_datastore::Datastore::new(db_path, legacy_import);
Comment thread
TimeToBuildBob marked this conversation as resolved.

let server_state = endpoints::ServerState {
// Even if legacy_import is set to true it is disabled on Android so
// it will not happen there
datastore: Mutex::new(aw_datastore::Datastore::new(db_path, legacy_import)),
datastore: Mutex::new(datastore),
asset_resolver: endpoints::AssetResolver::new(asset_path),
device_id,
};
Expand Down
Loading