diff --git a/Cargo.toml b/Cargo.toml index 9f86a5de..33d49058 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ build = "build/version.rs" [features] default = ["bundled_sqlite3"] bundled_sqlite3 = ["rusqlite/bundled"] +sqlcipher = ["rusqlite/sqlcipher", "mentat_db/sqlcipher"] [workspace] members = ["tools/cli", "ffi"] diff --git a/db/Cargo.toml b/db/Cargo.toml index 668da59b..6a9a166b 100644 --- a/db/Cargo.toml +++ b/db/Cargo.toml @@ -3,6 +3,10 @@ name = "mentat_db" version = "0.0.1" workspace = ".." +[features] +default = [] +sqlcipher = ["rusqlite/sqlcipher"] + [dependencies] error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" } indexmap = "1" diff --git a/db/src/db.rs b/db/src/db.rs index de2ea52d..8da5fe73 100644 --- a/db/src/db.rs +++ b/db/src/db.rs @@ -71,30 +71,72 @@ use watcher::{ NullWatcher, }; -pub fn new_connection(uri: T) -> rusqlite::Result where T: AsRef { - let conn = match uri.as_ref().to_string_lossy().len() { +// In PRAGMA foo='bar', `'bar'` must be a constant string (it cannot be a +// bound parameter), so we need to escape manually. According to +// https://www.sqlite.org/faq.html, the only character that must be escaped is +// the single quote, which is escaped by placing two single quotes in a row. +fn escape_string_for_pragma(s: &str) -> String { + s.replace("'", "''") +} + +fn make_connection(uri: &Path, maybe_encryption_key: Option<&str>) -> rusqlite::Result { + let conn = match uri.to_string_lossy().len() { 0 => rusqlite::Connection::open_in_memory()?, _ => rusqlite::Connection::open(uri)?, }; + let page_size = 32768; + + let initial_pragmas = if let Some(encryption_key) = maybe_encryption_key { + assert!(cfg!(feature = "sqlcipher"), + "This function shouldn't be called with a key unless we have sqlcipher support"); + // Important: The `cipher_page_size` cannot be changed without breaking + // the ability to open databases that were written when using a + // different `cipher_page_size`. Additionally, it (AFAICT) must be a + // positive multiple of `page_size`. We use the same value for both here. + format!(" + PRAGMA key='{}'; + PRAGMA cipher_page_size={}; + ", escape_string_for_pragma(encryption_key), page_size) + } else { + String::new() + }; + // See https://github.com/mozilla/mentat/issues/505 for details on temp_store // pragma and how it might interact together with consumers such as Firefox. // temp_store=2 is currently present to force SQLite to store temp files in memory. // Some of the platforms we support do not have a tmp partition (e.g. Android) // necessary to store temp files on disk. Ideally, consumers should be able to // override this behaviour (see issue 505). - conn.execute_batch(" - PRAGMA page_size=32768; + conn.execute_batch(&format!(" + {} PRAGMA journal_mode=wal; PRAGMA wal_autocheckpoint=32; PRAGMA journal_size_limit=3145728; PRAGMA foreign_keys=ON; PRAGMA temp_store=2; - ")?; + ", initial_pragmas))?; Ok(conn) } +pub fn new_connection(uri: T) -> rusqlite::Result where T: AsRef { + make_connection(uri.as_ref(), None) +} + +#[cfg(feature = "sqlcipher")] +pub fn new_connection_with_key(uri: impl AsRef, encryption_key: impl AsRef) -> rusqlite::Result { + make_connection(uri.as_ref(), Some(encryption_key.as_ref())) +} + +#[cfg(feature = "sqlcipher")] +pub fn change_encryption_key(conn: &rusqlite::Connection, encryption_key: impl AsRef) -> rusqlite::Result<()> { + let escaped = escape_string_for_pragma(encryption_key.as_ref()); + // `conn.execute` complains that this returns a result, and using a query + // for it requires more boilerplate. + conn.execute_batch(&format!("PRAGMA rekey = '{}';", escaped)) +} + /// Version history: /// /// 1: initial Rust Mentat schema. @@ -1228,11 +1270,8 @@ mod tests { fn fulltext_values(&self) -> edn::Value { debug::fulltext_values(&self.sqlite).expect("fulltext_values").into_edn() } - } - impl Default for TestConn { - fn default() -> TestConn { - let mut conn = new_connection("").expect("Couldn't open in-memory db"); + fn with_sqlite(mut conn: rusqlite::Connection) -> TestConn { let db = ensure_current_version(&mut conn).unwrap(); // Does not include :db/txInstant. @@ -1266,6 +1305,12 @@ mod tests { } } + impl Default for TestConn { + fn default() -> TestConn { + TestConn::with_sqlite(new_connection("").expect("Couldn't open in-memory db")) + } + } + fn tempids(report: &TxReport) -> edn::Value { let mut map: BTreeMap = BTreeMap::default(); for (tempid, &entid) in report.tempids.iter() { @@ -1274,10 +1319,7 @@ mod tests { edn::Value::Map(map) } - #[test] - fn test_add() { - let mut conn = TestConn::default(); - + fn run_test_add(mut conn: TestConn) { // Test inserting :db.cardinality/one elements. assert_transact!(conn, "[[:db/add 100 :db.schema/version 1] [:db/add 101 :db.schema/version 2]]"); @@ -1342,6 +1384,11 @@ mod tests { [200 :db.schema/attribute 101]]"); } + #[test] + fn test_add() { + run_test_add(TestConn::default()); + } + #[test] fn test_tx_assertions() { let mut conn = TestConn::default(); @@ -2694,4 +2741,48 @@ mod tests { ]"#, Err("schema constraint violation: cardinality conflicts:\n AddRetractConflict { e: 100, a: 200, vs: {Long(7)} }\n AddRetractConflict { e: 100, a: 201, vs: {Long(8)} }\n")); } + + #[test] + #[cfg(feature = "sqlcipher")] + fn test_sqlcipher_openable() { + let secret_key = "key"; + let sqlite = new_connection_with_key("../fixtures/v1encrypted.db", secret_key).expect("Failed to find test DB"); + sqlite.query_row("SELECT COUNT(*) FROM sqlite_master", &[], |row| row.get::<_, i64>(0)) + .expect("Failed to execute sql query on encrypted DB"); + } + + #[cfg(feature = "sqlcipher")] + fn test_open_fail(opener: impl FnOnce() -> rusqlite::Result) { + let err = opener().expect_err("Should fail to open encrypted DB"); + match err { + rusqlite::Error::SqliteFailure(err, ..) => { + assert_eq!(err.extended_code, 26, "Should get error code 26 (not a database)."); + }, + err => { + panic!("Wrong error type! {}", err); + } + } + } + + #[test] + #[cfg(feature = "sqlcipher")] + fn test_sqlcipher_requires_key() { + // Don't use a key. + test_open_fail(|| new_connection("../fixtures/v1encrypted.db")); + } + + #[test] + #[cfg(feature = "sqlcipher")] + fn test_sqlcipher_requires_correct_key() { + // Use a key, but the wrong one. + test_open_fail(|| new_connection_with_key("../fixtures/v1encrypted.db", "wrong key")); + } + + #[test] + #[cfg(feature = "sqlcipher")] + fn test_sqlcipher_some_transactions() { + let sqlite = new_connection_with_key("", "hunter2").expect("Failed to create encrypted connection"); + // Run a basic test as a sanity check. + run_test_add(TestConn::with_sqlite(sqlite)); + } } diff --git a/db/src/lib.rs b/db/src/lib.rs index 39ec8820..87813828 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -77,6 +77,12 @@ pub use db::{ new_connection, }; +#[cfg(feature = "sqlcipher")] +pub use db::{ + new_connection_with_key, + change_encryption_key, +}; + pub use watcher::{ TransactWatcher, }; diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 7c1e7c10..08c6425d 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -7,8 +7,14 @@ authors = ["Emily Toop "] name = "mentat_ffi" crate-type = ["lib", "staticlib", "cdylib"] +[features] +default = ["bundled_sqlite3"] +sqlcipher = ["mentat/sqlcipher"] +bundled_sqlite3 = ["mentat/bundled_sqlite3"] + [dependencies] libc = "0.2" [dependencies.mentat] path = "../" +default-features = false diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 5a2b0edb..7e8eaa49 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -248,6 +248,16 @@ pub extern "C" fn store_open(uri: *const c_char) -> *mut Store { Box::into_raw(Box::new(store)) } +/// Variant of store_open that opens an encrypted database. +#[cfg(feature = "sqlcipher")] +#[no_mangle] +pub extern "C" fn store_open_encrypted(uri: *const c_char, key: *const c_char) -> *mut Store { + let uri = c_char_to_string(uri); + let key = c_char_to_string(key); + let store = Store::open_with_key(&uri, &key).expect("expected a store"); + Box::into_raw(Box::new(store)) +} + // TODO: open empty // TODO: dismantle diff --git a/fixtures/v1encrypted.db b/fixtures/v1encrypted.db new file mode 100644 index 00000000..6061d913 Binary files /dev/null and b/fixtures/v1encrypted.db differ diff --git a/src/conn.rs b/src/conn.rs index 43d5a57e..4be04767 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -206,6 +206,48 @@ impl Store { } } +#[cfg(feature = "sqlcipher")] +impl Store { + /// Variant of `open` that allows a key (for encryption/decryption) to be + /// supplied. Fails unless linked against sqlcipher (or something else that + /// supports the Sqlite Encryption Extension). + pub fn open_with_key(path: &str, encryption_key: &str) -> Result { + let mut connection = ::new_connection_with_key(path, encryption_key)?; + let conn = Conn::connect(&mut connection)?; + Ok(Store { + conn: conn, + sqlite: connection, + }) + } + + /// Variant of `open_empty` that allows a key (for encryption/decryption) to + /// be supplied. Fails unless linked against sqlcipher (or something else + /// that supports the Sqlite Encryption Extension). + pub fn open_empty_with_key(path: &str, encryption_key: &str) -> Result { + if !path.is_empty() { + if Path::new(path).exists() { + bail!(ErrorKind::PathAlreadyExists(path.to_string())); + } + } + + let mut connection = ::new_connection_with_key(path, encryption_key)?; + let conn = Conn::empty(&mut connection)?; + Ok(Store { + conn: conn, + sqlite: connection, + }) + } + + /// Change the key for a database that was opened using `open_with_key` or + /// `open_empty_with_key` (using `PRAGMA rekey`). Fails unless linked + /// against sqlcipher (or something else that supports the Sqlite Encryption + /// Extension). + pub fn change_encryption_key(&mut self, new_encryption_key: &str) -> Result<()> { + ::change_encryption_key(&self.sqlite, new_encryption_key)?; + Ok(()) + } +} + pub trait Queryable { fn q_explain(&self, query: &str, inputs: T) -> Result where T: Into>; diff --git a/src/lib.rs b/src/lib.rs index 7d783ea8..4e4c2969 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,12 @@ pub use mentat_db::{ new_connection, }; +#[cfg(feature = "sqlcipher")] +pub use mentat_db::{ + new_connection_with_key, + change_encryption_key, +}; + /// Produce the appropriate `Variable` for the provided valid ?-prefixed name. /// This lives here because we can't re-export macros: /// https://github.com/rust-lang/rust/issues/29638. diff --git a/tests/query.rs b/tests/query.rs index b0422f23..6b7d2a48 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -1474,10 +1474,7 @@ fn test_tx_ids() { assert_tx_id_range(&store, tx2, tx3 + 1, vec![TypedValue::Ref(tx2), TypedValue::Ref(tx3)]); } -#[test] -fn test_tx_data() { - let mut store = Store::open("").expect("opened"); - +fn run_tx_data_test(mut store: Store) { store.transact(r#"[ [:db/add "a" :db/ident :foo/term] [:db/add "a" :db/valueType :db.type/string] @@ -1534,3 +1531,16 @@ fn test_tx_data() { assert_tx_data(&store, &tx1, "1".into()); assert_tx_data(&store, &tx2, "2".into()); } + +#[test] +fn test_tx_data() { + run_tx_data_test(Store::open("").expect("opened")); +} + +#[cfg(feature = "sqlite")] +#[test] +fn test_encrypted() { + // We expect this to blow up completely if something is wrong with the encryption, + // so the specific test we use doesn't matter that much. + run_tx_data_test(Store::open_with_key("", "secret").expect("opened")); +} diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index e06753b3..081a2d4f 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -2,6 +2,12 @@ name = "mentat_cli" version = "0.0.1" +# Forward mentat's features. +[features] +default = ["bundled_sqlite3"] +sqlcipher = ["mentat/sqlcipher"] +bundled_sqlite3 = ["mentat/bundled_sqlite3"] + [lib] name = "mentat_cli" path = "src/mentat_cli/lib.rs" @@ -30,6 +36,7 @@ features = ["limits"] [dependencies.mentat] path = "../.." +default-features = false [dependencies.mentat_parser_utils] path = "../../parser-utils" diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs index 9eaa40a0..ec49618d 100644 --- a/tools/cli/src/mentat_cli/command_parser.rs +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -47,6 +47,8 @@ pub static COMMAND_IMPORT_LONG: &'static str = &"import"; pub static COMMAND_IMPORT_SHORT: &'static str = &"i"; pub static COMMAND_OPEN: &'static str = &"open"; pub static COMMAND_OPEN_EMPTY: &'static str = &"empty"; +pub static COMMAND_OPEN_ENCRYPTED: &'static str = &"open_encrypted"; +pub static COMMAND_OPEN_EMPTY_ENCRYPTED: &'static str = &"empty_encrypted"; pub static COMMAND_QUERY_LONG: &'static str = &"query"; pub static COMMAND_QUERY_SHORT: &'static str = &"q"; pub static COMMAND_QUERY_EXPLAIN_LONG: &'static str = &"explain_query"; @@ -67,6 +69,8 @@ pub enum Command { Import(String), Open(String), OpenEmpty(String), + OpenEncrypted(String, String), + OpenEmptyEncrypted(String, String), Query(String), QueryExplain(String), QueryPrepared(String), @@ -97,6 +101,8 @@ impl Command { &Command::Import(_) | &Command::Open(_) | &Command::OpenEmpty(_) | + &Command::OpenEncrypted(_, _) | + &Command::OpenEmptyEncrypted(_, _) | &Command::Timer(_) | &Command::Schema | &Command::Sync(_) @@ -118,6 +124,8 @@ impl Command { &Command::Help(_) | &Command::Open(_) | &Command::OpenEmpty(_) | + &Command::OpenEncrypted(_, _) | + &Command::OpenEmptyEncrypted(_, _) | &Command::QueryExplain(_) | &Command::Timer(_) | &Command::Schema | @@ -149,6 +157,12 @@ impl Command { &Command::OpenEmpty(ref args) => { format!(".{} {}", COMMAND_OPEN_EMPTY, args) }, + &Command::OpenEncrypted(ref db, ref key) => { + format!(".{} {} {}", COMMAND_OPEN_ENCRYPTED, db, key) + }, + &Command::OpenEmptyEncrypted(ref db, ref key) => { + format!(".{} {} {}", COMMAND_OPEN_EMPTY_ENCRYPTED, db, key) + }, &Command::Query(ref args) => { format!(".{} {}", COMMAND_QUERY_LONG, args) }, @@ -197,6 +211,20 @@ pub fn command(s: &str) -> Result { .skip(spaces()) .skip(eof()); + let opener = |command, num_args| { + string(command) + .with(spaces()) + .with(arguments()) + .map(move |args| { + if args.len() < num_args { + bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string())); + } + if args.len() > num_args { + bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[num_args]))); + } + Ok(args) + }) + }; // Commands. let cache_parser = string(COMMAND_CACHE) @@ -246,31 +274,17 @@ pub fn command(s: &str) -> Result { Ok(Command::Import(x)) }); - let open_parser = string(COMMAND_OPEN) - .with(spaces()) - .with(arguments()) - .map(|args| { - if args.len() < 1 { - bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string())); - } - if args.len() > 1 { - bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1]))); - } - Ok(Command::Open(args[0].clone())) - }); + let open_parser = opener(COMMAND_OPEN, 1).map(|args_res| + args_res.map(|args| Command::Open(args[0].clone()))); - let open_empty_parser = string(COMMAND_OPEN_EMPTY) - .with(spaces()) - .with(arguments()) - .map(|args| { - if args.len() < 1 { - bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string())); - } - if args.len() > 1 { - bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1]))); - } - Ok(Command::OpenEmpty(args[0].clone())) - }); + let open_empty_parser = opener(COMMAND_OPEN_EMPTY, 1).map(|args_res| + args_res.map(|args| Command::OpenEmpty(args[0].clone()))); + + let open_encrypted_parser = opener(COMMAND_OPEN_ENCRYPTED, 2).map(|args_res| + args_res.map(|args| Command::OpenEncrypted(args[0].clone(), args[1].clone()))); + + let open_empty_encrypted_parser = opener(COMMAND_OPEN_EMPTY_ENCRYPTED, 2).map(|args_res| + args_res.map(|args| Command::OpenEmptyEncrypted(args[0].clone(), args[1].clone()))); let query_parser = try(string(COMMAND_QUERY_LONG)).or(try(string(COMMAND_QUERY_SHORT))) .with(edn_arg_parser()) @@ -321,11 +335,13 @@ pub fn command(s: &str) -> Result { spaces() .skip(token('.')) - .with(choice::<[&mut Parser>; 14], _> + .with(choice::<[&mut Parser>; 16], _> ([&mut try(help_parser), &mut try(import_parser), &mut try(timer_parser), &mut try(cache_parser), + &mut try(open_encrypted_parser), + &mut try(open_empty_encrypted_parser), &mut try(open_parser), &mut try(open_empty_parser), &mut try(close_parser), @@ -425,6 +441,46 @@ mod tests { } } + #[test] + fn test_open_encrypted_parser() { + let input = ".open_encrypted /path/to/my.db hunter2"; + let cmd = command(&input).expect("Expected open_encrypted command"); + match cmd { + Command::OpenEncrypted(path, key) => { + assert_eq!(path, "/path/to/my.db".to_string()); + assert_eq!(key, "hunter2".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_empty_encrypted_parser() { + let input = ".empty_encrypted /path/to/my.db hunter2"; + let cmd = command(&input).expect("Expected empty_encrypted command"); + match cmd { + Command::OpenEmptyEncrypted(path, key) => { + assert_eq!(path, "/path/to/my.db".to_string()); + assert_eq!(key, "hunter2".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_encrypted_parser_missing_key() { + let input = ".open_encrypted path/to/db.db"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), "Missing required argument"); + } + + #[test] + fn test_empty_encrypted_parser_missing_key() { + let input = ".empty_encrypted path/to/db.db"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), "Missing required argument"); + } + #[test] fn test_sync_parser_path_arg() { let input = ".sync https://example.com/api/ 316ea470-ce35-4adf-9c61-e0de6e289c59"; diff --git a/tools/cli/src/mentat_cli/lib.rs b/tools/cli/src/mentat_cli/lib.rs index 6feb2f8b..7afb7441 100644 --- a/tools/cli/src/mentat_cli/lib.rs +++ b/tools/cli/src/mentat_cli/lib.rs @@ -50,6 +50,9 @@ pub fn run() -> i32 { let mut opts = Options::new(); opts.optopt("d", "", "The path to a database to open", "DATABASE"); + if cfg!(feature = "sqlcipher") { + opts.optopt("k", "key", "The key to use to open the database (only available when using sqlcipher)", "KEY"); + } opts.optflag("h", "help", "Print this help message and exit"); opts.optmulti("q", "query", "Execute a query on startup. Queries are executed after any transacts.", "QUERY"); opts.optmulti("t", "transact", "Execute a transact on startup. Transacts are executed before queries.", "TRANSACT"); @@ -74,12 +77,24 @@ pub fn run() -> i32 { return 0; } + // It's still possible to pass this in even if it's not a documented flag above. + let key = matches.opt_str("key"); + if key.is_some() && !cfg!(feature = "sqlcipher") { + eprintln!("Decryption key provided, but this build does not have sqlcipher support"); + return 1; + } + let mut last_arg: Option<&str> = None; + let cmds:Vec = args.iter().filter_map(|arg| { match last_arg { Some("-d") => { last_arg = None; - Some(command_parser::Command::Open(arg.clone())) + if let Some(k) = &key { + Some(command_parser::Command::OpenEncrypted(arg.clone(), k.clone())) + } else { + Some(command_parser::Command::Open(arg.clone())) + } }, Some("-q") => { last_arg = None; diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index f6a47c09..9a399aa0 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -52,6 +52,7 @@ use command_parser::{ COMMAND_HELP, COMMAND_IMPORT_LONG, COMMAND_OPEN, + COMMAND_OPEN_EMPTY, COMMAND_QUERY_LONG, COMMAND_QUERY_SHORT, COMMAND_QUERY_EXPLAIN_LONG, @@ -64,6 +65,16 @@ use command_parser::{ COMMAND_TRANSACT_SHORT, }; +// These are still defined when this feature is disabled (so that we can +// give decent error messages when a user tries open_encrypted when +// we weren't compiled with sqlcipher), but they're unused, since we +// omit them from help message (since they wouldn't work). +#[cfg(feature = "sqlcipher")] +use command_parser::{ + COMMAND_OPEN_EMPTY_ENCRYPTED, + COMMAND_OPEN_ENCRYPTED, +}; + use input::InputReader; use input::InputResult::{ Empty, @@ -81,6 +92,12 @@ lazy_static! { (COMMAND_EXIT_SHORT, "Shortcut for `.exit`. Close the current database and exit the REPL."), (COMMAND_OPEN, "Open a database at path."), + (COMMAND_OPEN_EMPTY, "Open an empty database at path."), + + #[cfg(feature = "sqlcipher")] + (COMMAND_OPEN_ENCRYPTED, "Open an encrypted database at path using the provided key."), + #[cfg(feature = "sqlcipher")] + (COMMAND_OPEN_EMPTY_ENCRYPTED, "Open an empty encrypted database at path using the provided key."), (COMMAND_SCHEMA, "Output the schema for the current open database."), @@ -267,6 +284,18 @@ impl Repl { Err(e) => eprintln!("{}", e.to_string()), }; }, + Command::OpenEncrypted(db, encryption_key) => { + match self.open_with_key(db, &encryption_key) { + Ok(_) => println!("Database {:?} opened with key {:?}", self.db_name(), encryption_key), + Err(e) => eprintln!("{}", e.to_string()), + } + }, + Command::OpenEmptyEncrypted(db, encryption_key) => { + match self.open_empty_with_key(db, &encryption_key) { + Ok(_) => println!("Empty database {:?} opened with key {:?}", self.db_name(), encryption_key), + Err(e) => eprintln!("{}", e.to_string()), + } + }, Command::Query(query) => { self.store .q_once(query.as_str(), None) @@ -345,11 +374,32 @@ impl Repl { } } - fn open(&mut self, path: T) -> ::mentat::errors::Result<()> - where T: Into { - let path = path.into(); + fn open_common( + &mut self, + empty: bool, + path: String, + encryption_key: Option<&str> + ) -> ::mentat::errors::Result<()> { if self.path.is_empty() || path != self.path { - let next = Store::open(path.as_str())?; + let next = match encryption_key { + #[cfg(not(feature = "sqlcipher"))] + Some(_) => bail!(".open_encrypted and .empty_encrypted require the sqlcipher Mentat feature"), + #[cfg(feature = "sqlcipher")] + Some(k) => { + if empty { + Store::open_empty_with_key(path.as_str(), k)? + } else { + Store::open_with_key(path.as_str(), k)? + } + }, + _ => { + if empty { + Store::open_empty(path.as_str())? + } else { + Store::open(path.as_str())? + } + } + }; self.path = path; self.store = next; } @@ -357,16 +407,23 @@ impl Repl { Ok(()) } - fn open_empty(&mut self, path: T) -> ::mentat::errors::Result<()> - where T: Into { - let path = path.into(); - if self.path.is_empty() || path != self.path { - let next = Store::open_empty(path.as_str())?; - self.path = path; - self.store = next; - } + fn open(&mut self, path: impl Into) -> ::mentat::errors::Result<()> { + self.open_common(false, path.into(), None) + } - Ok(()) + fn open_empty(&mut self, path: impl Into) + -> ::mentat::errors::Result<()> { + self.open_common(true, path.into(), None) + } + + fn open_with_key(&mut self, path: impl Into, encryption_key: impl AsRef) + -> ::mentat::errors::Result<()> { + self.open_common(false, path.into(), Some(encryption_key.as_ref())) + } + + fn open_empty_with_key(&mut self, path: impl Into, encryption_key: impl AsRef) + -> ::mentat::errors::Result<()> { + self.open_common(true, path.into(), Some(encryption_key.as_ref())) } // Close the current store by opening a new in-memory store in its place.