Add support for using sqlcipher (#737). Fixes #118

This commit is contained in:
Thom 2018-06-13 08:49:40 -07:00 committed by GitHub
parent c0d4568970
commit 6a1a265894
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 367 additions and 56 deletions

View file

@ -18,6 +18,7 @@ build = "build/version.rs"
[features] [features]
default = ["bundled_sqlite3"] default = ["bundled_sqlite3"]
bundled_sqlite3 = ["rusqlite/bundled"] bundled_sqlite3 = ["rusqlite/bundled"]
sqlcipher = ["rusqlite/sqlcipher", "mentat_db/sqlcipher"]
[workspace] [workspace]
members = ["tools/cli", "ffi"] members = ["tools/cli", "ffi"]

View file

@ -3,6 +3,10 @@ name = "mentat_db"
version = "0.0.1" version = "0.0.1"
workspace = ".." workspace = ".."
[features]
default = []
sqlcipher = ["rusqlite/sqlcipher"]
[dependencies] [dependencies]
error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" } error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" }
indexmap = "1" indexmap = "1"

View file

@ -71,30 +71,72 @@ use watcher::{
NullWatcher, NullWatcher,
}; };
pub fn new_connection<T>(uri: T) -> rusqlite::Result<rusqlite::Connection> where T: AsRef<Path> { // In PRAGMA foo='bar', `'bar'` must be a constant string (it cannot be a
let conn = match uri.as_ref().to_string_lossy().len() { // 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<rusqlite::Connection> {
let conn = match uri.to_string_lossy().len() {
0 => rusqlite::Connection::open_in_memory()?, 0 => rusqlite::Connection::open_in_memory()?,
_ => rusqlite::Connection::open(uri)?, _ => 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 // 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. // 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. // 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) // 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 // necessary to store temp files on disk. Ideally, consumers should be able to
// override this behaviour (see issue 505). // override this behaviour (see issue 505).
conn.execute_batch(" conn.execute_batch(&format!("
PRAGMA page_size=32768; {}
PRAGMA journal_mode=wal; PRAGMA journal_mode=wal;
PRAGMA wal_autocheckpoint=32; PRAGMA wal_autocheckpoint=32;
PRAGMA journal_size_limit=3145728; PRAGMA journal_size_limit=3145728;
PRAGMA foreign_keys=ON; PRAGMA foreign_keys=ON;
PRAGMA temp_store=2; PRAGMA temp_store=2;
")?; ", initial_pragmas))?;
Ok(conn) Ok(conn)
} }
pub fn new_connection<T>(uri: T) -> rusqlite::Result<rusqlite::Connection> where T: AsRef<Path> {
make_connection(uri.as_ref(), None)
}
#[cfg(feature = "sqlcipher")]
pub fn new_connection_with_key(uri: impl AsRef<Path>, encryption_key: impl AsRef<str>) -> rusqlite::Result<rusqlite::Connection> {
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<str>) -> 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: /// Version history:
/// ///
/// 1: initial Rust Mentat schema. /// 1: initial Rust Mentat schema.
@ -1228,11 +1270,8 @@ mod tests {
fn fulltext_values(&self) -> edn::Value { fn fulltext_values(&self) -> edn::Value {
debug::fulltext_values(&self.sqlite).expect("fulltext_values").into_edn() debug::fulltext_values(&self.sqlite).expect("fulltext_values").into_edn()
} }
}
impl Default for TestConn { fn with_sqlite(mut conn: rusqlite::Connection) -> TestConn {
fn default() -> TestConn {
let mut conn = new_connection("").expect("Couldn't open in-memory db");
let db = ensure_current_version(&mut conn).unwrap(); let db = ensure_current_version(&mut conn).unwrap();
// Does not include :db/txInstant. // 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 { fn tempids(report: &TxReport) -> edn::Value {
let mut map: BTreeMap<edn::Value, edn::Value> = BTreeMap::default(); let mut map: BTreeMap<edn::Value, edn::Value> = BTreeMap::default();
for (tempid, &entid) in report.tempids.iter() { for (tempid, &entid) in report.tempids.iter() {
@ -1274,10 +1319,7 @@ mod tests {
edn::Value::Map(map) edn::Value::Map(map)
} }
#[test] fn run_test_add(mut conn: TestConn) {
fn test_add() {
let mut conn = TestConn::default();
// Test inserting :db.cardinality/one elements. // Test inserting :db.cardinality/one elements.
assert_transact!(conn, "[[:db/add 100 :db.schema/version 1] assert_transact!(conn, "[[:db/add 100 :db.schema/version 1]
[:db/add 101 :db.schema/version 2]]"); [:db/add 101 :db.schema/version 2]]");
@ -1342,6 +1384,11 @@ mod tests {
[200 :db.schema/attribute 101]]"); [200 :db.schema/attribute 101]]");
} }
#[test]
fn test_add() {
run_test_add(TestConn::default());
}
#[test] #[test]
fn test_tx_assertions() { fn test_tx_assertions() {
let mut conn = TestConn::default(); 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")); 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<rusqlite::Connection>) {
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));
}
} }

View file

@ -77,6 +77,12 @@ pub use db::{
new_connection, new_connection,
}; };
#[cfg(feature = "sqlcipher")]
pub use db::{
new_connection_with_key,
change_encryption_key,
};
pub use watcher::{ pub use watcher::{
TransactWatcher, TransactWatcher,
}; };

View file

@ -7,8 +7,14 @@ authors = ["Emily Toop <etoop@mozilla.com>"]
name = "mentat_ffi" name = "mentat_ffi"
crate-type = ["lib", "staticlib", "cdylib"] crate-type = ["lib", "staticlib", "cdylib"]
[features]
default = ["bundled_sqlite3"]
sqlcipher = ["mentat/sqlcipher"]
bundled_sqlite3 = ["mentat/bundled_sqlite3"]
[dependencies] [dependencies]
libc = "0.2" libc = "0.2"
[dependencies.mentat] [dependencies.mentat]
path = "../" path = "../"
default-features = false

View file

@ -248,6 +248,16 @@ pub extern "C" fn store_open(uri: *const c_char) -> *mut Store {
Box::into_raw(Box::new(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: open empty
// TODO: dismantle // TODO: dismantle

BIN
fixtures/v1encrypted.db Normal file

Binary file not shown.

View file

@ -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<Store> {
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<Store> {
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 { pub trait Queryable {
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation> fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>>; where T: Into<Option<QueryInputs>>;

View file

@ -59,6 +59,12 @@ pub use mentat_db::{
new_connection, 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. /// Produce the appropriate `Variable` for the provided valid ?-prefixed name.
/// This lives here because we can't re-export macros: /// This lives here because we can't re-export macros:
/// https://github.com/rust-lang/rust/issues/29638. /// https://github.com/rust-lang/rust/issues/29638.

View file

@ -1474,10 +1474,7 @@ fn test_tx_ids() {
assert_tx_id_range(&store, tx2, tx3 + 1, vec![TypedValue::Ref(tx2), TypedValue::Ref(tx3)]); assert_tx_id_range(&store, tx2, tx3 + 1, vec![TypedValue::Ref(tx2), TypedValue::Ref(tx3)]);
} }
#[test] fn run_tx_data_test(mut store: Store) {
fn test_tx_data() {
let mut store = Store::open("").expect("opened");
store.transact(r#"[ store.transact(r#"[
[:db/add "a" :db/ident :foo/term] [:db/add "a" :db/ident :foo/term]
[:db/add "a" :db/valueType :db.type/string] [: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, &tx1, "1".into());
assert_tx_data(&store, &tx2, "2".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"));
}

View file

@ -2,6 +2,12 @@
name = "mentat_cli" name = "mentat_cli"
version = "0.0.1" version = "0.0.1"
# Forward mentat's features.
[features]
default = ["bundled_sqlite3"]
sqlcipher = ["mentat/sqlcipher"]
bundled_sqlite3 = ["mentat/bundled_sqlite3"]
[lib] [lib]
name = "mentat_cli" name = "mentat_cli"
path = "src/mentat_cli/lib.rs" path = "src/mentat_cli/lib.rs"
@ -30,6 +36,7 @@ features = ["limits"]
[dependencies.mentat] [dependencies.mentat]
path = "../.." path = "../.."
default-features = false
[dependencies.mentat_parser_utils] [dependencies.mentat_parser_utils]
path = "../../parser-utils" path = "../../parser-utils"

View file

@ -47,6 +47,8 @@ pub static COMMAND_IMPORT_LONG: &'static str = &"import";
pub static COMMAND_IMPORT_SHORT: &'static str = &"i"; pub static COMMAND_IMPORT_SHORT: &'static str = &"i";
pub static COMMAND_OPEN: &'static str = &"open"; pub static COMMAND_OPEN: &'static str = &"open";
pub static COMMAND_OPEN_EMPTY: &'static str = &"empty"; 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_LONG: &'static str = &"query";
pub static COMMAND_QUERY_SHORT: &'static str = &"q"; pub static COMMAND_QUERY_SHORT: &'static str = &"q";
pub static COMMAND_QUERY_EXPLAIN_LONG: &'static str = &"explain_query"; pub static COMMAND_QUERY_EXPLAIN_LONG: &'static str = &"explain_query";
@ -67,6 +69,8 @@ pub enum Command {
Import(String), Import(String),
Open(String), Open(String),
OpenEmpty(String), OpenEmpty(String),
OpenEncrypted(String, String),
OpenEmptyEncrypted(String, String),
Query(String), Query(String),
QueryExplain(String), QueryExplain(String),
QueryPrepared(String), QueryPrepared(String),
@ -97,6 +101,8 @@ impl Command {
&Command::Import(_) | &Command::Import(_) |
&Command::Open(_) | &Command::Open(_) |
&Command::OpenEmpty(_) | &Command::OpenEmpty(_) |
&Command::OpenEncrypted(_, _) |
&Command::OpenEmptyEncrypted(_, _) |
&Command::Timer(_) | &Command::Timer(_) |
&Command::Schema | &Command::Schema |
&Command::Sync(_) &Command::Sync(_)
@ -118,6 +124,8 @@ impl Command {
&Command::Help(_) | &Command::Help(_) |
&Command::Open(_) | &Command::Open(_) |
&Command::OpenEmpty(_) | &Command::OpenEmpty(_) |
&Command::OpenEncrypted(_, _) |
&Command::OpenEmptyEncrypted(_, _) |
&Command::QueryExplain(_) | &Command::QueryExplain(_) |
&Command::Timer(_) | &Command::Timer(_) |
&Command::Schema | &Command::Schema |
@ -149,6 +157,12 @@ impl Command {
&Command::OpenEmpty(ref args) => { &Command::OpenEmpty(ref args) => {
format!(".{} {}", COMMAND_OPEN_EMPTY, 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) => { &Command::Query(ref args) => {
format!(".{} {}", COMMAND_QUERY_LONG, args) format!(".{} {}", COMMAND_QUERY_LONG, args)
}, },
@ -197,6 +211,20 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
.skip(spaces()) .skip(spaces())
.skip(eof()); .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. // Commands.
let cache_parser = string(COMMAND_CACHE) let cache_parser = string(COMMAND_CACHE)
@ -246,31 +274,17 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
Ok(Command::Import(x)) Ok(Command::Import(x))
}); });
let open_parser = string(COMMAND_OPEN) let open_parser = opener(COMMAND_OPEN, 1).map(|args_res|
.with(spaces()) args_res.map(|args| Command::Open(args[0].clone())));
.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_empty_parser = string(COMMAND_OPEN_EMPTY) let open_empty_parser = opener(COMMAND_OPEN_EMPTY, 1).map(|args_res|
.with(spaces()) args_res.map(|args| Command::OpenEmpty(args[0].clone())));
.with(arguments())
.map(|args| { let open_encrypted_parser = opener(COMMAND_OPEN_ENCRYPTED, 2).map(|args_res|
if args.len() < 1 { args_res.map(|args| Command::OpenEncrypted(args[0].clone(), args[1].clone())));
bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string()));
} let open_empty_encrypted_parser = opener(COMMAND_OPEN_EMPTY_ENCRYPTED, 2).map(|args_res|
if args.len() > 1 { args_res.map(|args| Command::OpenEmptyEncrypted(args[0].clone(), args[1].clone())));
bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1])));
}
Ok(Command::OpenEmpty(args[0].clone()))
});
let query_parser = try(string(COMMAND_QUERY_LONG)).or(try(string(COMMAND_QUERY_SHORT))) let query_parser = try(string(COMMAND_QUERY_LONG)).or(try(string(COMMAND_QUERY_SHORT)))
.with(edn_arg_parser()) .with(edn_arg_parser())
@ -321,11 +335,13 @@ pub fn command(s: &str) -> Result<Command, cli::Error> {
spaces() spaces()
.skip(token('.')) .skip(token('.'))
.with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 14], _> .with(choice::<[&mut Parser<Input = _, Output = Result<Command, cli::Error>>; 16], _>
([&mut try(help_parser), ([&mut try(help_parser),
&mut try(import_parser), &mut try(import_parser),
&mut try(timer_parser), &mut try(timer_parser),
&mut try(cache_parser), &mut try(cache_parser),
&mut try(open_encrypted_parser),
&mut try(open_empty_encrypted_parser),
&mut try(open_parser), &mut try(open_parser),
&mut try(open_empty_parser), &mut try(open_empty_parser),
&mut try(close_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] #[test]
fn test_sync_parser_path_arg() { fn test_sync_parser_path_arg() {
let input = ".sync https://example.com/api/ 316ea470-ce35-4adf-9c61-e0de6e289c59"; let input = ".sync https://example.com/api/ 316ea470-ce35-4adf-9c61-e0de6e289c59";

View file

@ -50,6 +50,9 @@ pub fn run() -> i32 {
let mut opts = Options::new(); let mut opts = Options::new();
opts.optopt("d", "", "The path to a database to open", "DATABASE"); 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.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("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"); 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; 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 mut last_arg: Option<&str> = None;
let cmds:Vec<command_parser::Command> = args.iter().filter_map(|arg| { let cmds:Vec<command_parser::Command> = args.iter().filter_map(|arg| {
match last_arg { match last_arg {
Some("-d") => { Some("-d") => {
last_arg = None; last_arg = None;
if let Some(k) = &key {
Some(command_parser::Command::OpenEncrypted(arg.clone(), k.clone()))
} else {
Some(command_parser::Command::Open(arg.clone())) Some(command_parser::Command::Open(arg.clone()))
}
}, },
Some("-q") => { Some("-q") => {
last_arg = None; last_arg = None;

View file

@ -52,6 +52,7 @@ use command_parser::{
COMMAND_HELP, COMMAND_HELP,
COMMAND_IMPORT_LONG, COMMAND_IMPORT_LONG,
COMMAND_OPEN, COMMAND_OPEN,
COMMAND_OPEN_EMPTY,
COMMAND_QUERY_LONG, COMMAND_QUERY_LONG,
COMMAND_QUERY_SHORT, COMMAND_QUERY_SHORT,
COMMAND_QUERY_EXPLAIN_LONG, COMMAND_QUERY_EXPLAIN_LONG,
@ -64,6 +65,16 @@ use command_parser::{
COMMAND_TRANSACT_SHORT, 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::InputReader;
use input::InputResult::{ use input::InputResult::{
Empty, Empty,
@ -81,6 +92,12 @@ lazy_static! {
(COMMAND_EXIT_SHORT, "Shortcut for `.exit`. Close the current database and exit the REPL."), (COMMAND_EXIT_SHORT, "Shortcut for `.exit`. Close the current database and exit the REPL."),
(COMMAND_OPEN, "Open a database at path."), (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."), (COMMAND_SCHEMA, "Output the schema for the current open database."),
@ -267,6 +284,18 @@ impl Repl {
Err(e) => eprintln!("{}", e.to_string()), 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) => { Command::Query(query) => {
self.store self.store
.q_once(query.as_str(), None) .q_once(query.as_str(), None)
@ -345,11 +374,32 @@ impl Repl {
} }
} }
fn open<T>(&mut self, path: T) -> ::mentat::errors::Result<()> fn open_common(
where T: Into<String> { &mut self,
let path = path.into(); empty: bool,
path: String,
encryption_key: Option<&str>
) -> ::mentat::errors::Result<()> {
if self.path.is_empty() || path != self.path { 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.path = path;
self.store = next; self.store = next;
} }
@ -357,16 +407,23 @@ impl Repl {
Ok(()) Ok(())
} }
fn open_empty<T>(&mut self, path: T) -> ::mentat::errors::Result<()> fn open(&mut self, path: impl Into<String>) -> ::mentat::errors::Result<()> {
where T: Into<String> { self.open_common(false, path.into(), None)
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;
} }
Ok(()) fn open_empty(&mut self, path: impl Into<String>)
-> ::mentat::errors::Result<()> {
self.open_common(true, path.into(), None)
}
fn open_with_key(&mut self, path: impl Into<String>, encryption_key: impl AsRef<str>)
-> ::mentat::errors::Result<()> {
self.open_common(false, path.into(), Some(encryption_key.as_ref()))
}
fn open_empty_with_key(&mut self, path: impl Into<String>, encryption_key: impl AsRef<str>)
-> ::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. // Close the current store by opening a new in-memory store in its place.