diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8e88a04140..6647bab9dd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -130,6 +130,9 @@ jobs: - name: Run tests run: cargo nextest run + + - name: Run libsql encryption tests + run: cargo test --features encryption --color=always --test encryption test_encryption # test-custom-pager: # runs-on: ubuntu-latest # name: Run Tests diff --git a/libsql-ffi/build.rs b/libsql-ffi/build.rs index a5aab595c4..c3c5beb0c4 100644 --- a/libsql-ffi/build.rs +++ b/libsql-ffi/build.rs @@ -494,6 +494,7 @@ fn build_multiple_ciphers(out_path: &Path) -> PathBuf { if cfg!(feature = "wasmtime-bindings") { config.define("LIBSQL_ENABLE_WASM_RUNTIME", "1"); } + config.define("LIBSQL_ENCRYPTION", "1"); if cfg!(feature = "session") { config diff --git a/libsql-ffi/bundled/SQLite3MultipleCiphers/CMakeLists.txt b/libsql-ffi/bundled/SQLite3MultipleCiphers/CMakeLists.txt index 98bce0a596..e178589249 100644 --- a/libsql-ffi/bundled/SQLite3MultipleCiphers/CMakeLists.txt +++ b/libsql-ffi/bundled/SQLite3MultipleCiphers/CMakeLists.txt @@ -124,6 +124,7 @@ set(SQLITE3MC_BASE_DEFINITIONS $<$:LIBSQL_ENABLE_WASM_RUNTIME=1> LIBSQL_EXTRA_PRAGMAS=1 LIBSQL_CUSTOM_PAGER_CODEC=1 + LIBSQL_ENCRYPTION=1 SQLITE_ENABLE_DBSTAT_VTAB=1 SQLITE_ENABLE_DBPAGE_VTAB=1 diff --git a/libsql-ffi/bundled/src/sqlite3.c b/libsql-ffi/bundled/src/sqlite3.c index 265c94ad12..a446238be3 100644 --- a/libsql-ffi/bundled/src/sqlite3.c +++ b/libsql-ffi/bundled/src/sqlite3.c @@ -121872,6 +121872,10 @@ SQLITE_PRIVATE int sqlite3DbIsNamed(sqlite3 *db, int iDb, const char *zName){ int libsql_handle_extra_attach_params(sqlite3* db, const char* zName, const char* zPath, sqlite3_value* pKey, char** zErrDyn); #endif +#ifdef LIBSQL_ENCRYPTION +SQLITE_PRIVATE int sqlite3mcHandleAttachKey(sqlite3*, const char*, const char*, sqlite3_value*, char**); +#endif + /* ** An SQL user-function registered to do the work of an ATTACH statement. The ** three arguments to the function come directly from an attach statement: @@ -122031,6 +122035,16 @@ static void attachFunc( rc = libsql_handle_extra_attach_params(db, zName, zPath, argv, &zErrDyn); } #endif + +#ifdef LIBSQL_ENCRYPTION + /* If the ATTACH statement came with key parameter, then lets handle it here. */ + if( rc==SQLITE_OK ){ + if( argv != NULL && argv[0] != NULL && argv[1] != NULL && argv[2] != NULL ){ + rc = sqlite3mcHandleAttachKey(db, zName, zPath, argv[2], &zErrDyn); + } + } +#endif + sqlite3_free_filename( zPath ); /* If the file was opened successfully, read the schema for the new database. diff --git a/libsql-sqlite3/src/attach.c b/libsql-sqlite3/src/attach.c index bf2ac8c80e..989cc49ac3 100644 --- a/libsql-sqlite3/src/attach.c +++ b/libsql-sqlite3/src/attach.c @@ -61,6 +61,10 @@ int sqlite3DbIsNamed(sqlite3 *db, int iDb, const char *zName){ int libsql_handle_extra_attach_params(sqlite3* db, const char* zName, const char* zPath, sqlite3_value* pKey, char** zErrDyn); #endif +#ifdef LIBSQL_ENCRYPTION +SQLITE_PRIVATE int sqlite3mcHandleAttachKey(sqlite3*, const char*, const char*, sqlite3_value*, char**); +#endif + /* ** An SQL user-function registered to do the work of an ATTACH statement. The ** three arguments to the function come directly from an attach statement: @@ -220,6 +224,16 @@ static void attachFunc( rc = libsql_handle_extra_attach_params(db, zName, zPath, argv, &zErrDyn); } #endif + +#ifdef LIBSQL_ENCRYPTION + /* If the ATTACH statement came with key parameter, then lets handle it here. */ + if( rc==SQLITE_OK ){ + if( argv != NULL && argv[0] != NULL && argv[1] != NULL && argv[2] != NULL ){ + rc = sqlite3mcHandleAttachKey(db, zName, zPath, argv[2], &zErrDyn); + } + } +#endif + sqlite3_free_filename( zPath ); /* If the file was opened successfully, read the schema for the new database. diff --git a/libsql/examples/encryption_local.rs b/libsql/examples/encryption_local.rs new file mode 100644 index 0000000000..8b03cc0d02 --- /dev/null +++ b/libsql/examples/encryption_local.rs @@ -0,0 +1,72 @@ +// Example of showing using an encrypted local database with libsql. It also shows how to +// attach another encrypted database. The example expects a local `world.db` encrypted database +// to be present in the same directory. + +use libsql::{params, Builder}; +use libsql::{Cipher, EncryptionConfig}; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + // The local database path where the data will be stored. + let db_path = std::env::var("LIBSQL_DB_PATH").unwrap(); + // The encryption key for the database. + let encryption_key = std::env::var("LIBSQL_ENCRYPTION_KEY").unwrap_or("s3cR3t".to_string()); + + let mut db_builder = Builder::new_local(db_path); + + db_builder = db_builder.encryption_config(EncryptionConfig { + cipher: Cipher::Aes256Cbc, + encryption_key: encryption_key.into(), + }); + + let db = db_builder.build().await.unwrap(); + let conn = db.connect().unwrap(); + conn.execute( + "CREATE TABLE IF NOT EXISTS guest_book_entries (text TEXT)", + (), + ) + .await + .unwrap(); + + // let's attach another encrypted database and print its contents + conn.execute("ATTACH DATABASE 'world.db' AS world KEY s3cR3t", ()) + .await + .unwrap(); + + let mut attached_results = conn + .query("SELECT * FROM world.guest_book_entries", ()) + .await + .unwrap(); + + println!("attached database guest book entries:"); + while let Some(row) = attached_results.next().await.unwrap() { + let text: String = row.get(0).unwrap(); + println!(" {}", text); + } + + let mut input = String::new(); + println!("Please write your entry to the guestbook:"); + match std::io::stdin().read_line(&mut input) { + Ok(_) => { + println!("You entered: {}", input); + let params = params![input.as_str()]; + conn.execute("INSERT INTO guest_book_entries (text) VALUES (?)", params) + .await + .unwrap(); + } + Err(error) => { + eprintln!("Error reading input: {}", error); + } + } + let mut results = conn + .query("SELECT * FROM guest_book_entries", ()) + .await + .unwrap(); + println!("Guest book entries:"); + while let Some(row) = results.next().await.unwrap() { + let text: String = row.get(0).unwrap(); + println!(" {}", text); + } +} diff --git a/libsql/tests/encryption.rs b/libsql/tests/encryption.rs new file mode 100644 index 0000000000..b6cc926740 --- /dev/null +++ b/libsql/tests/encryption.rs @@ -0,0 +1,52 @@ +use libsql::{params, Builder}; +use libsql_sys::{Cipher, EncryptionConfig}; + +#[tokio::test] +#[cfg(feature = "encryption")] +async fn test_encryption() { + let tempdir = std::env::temp_dir(); + let encrypted_path = tempdir.join("encrypted.db"); + let base_path = tempdir.join("base.db"); + + // lets create an encrypted database + { + let mut db_builder = Builder::new_local(&encrypted_path); + db_builder = db_builder.encryption_config(EncryptionConfig { + cipher: Cipher::Aes256Cbc, + encryption_key: "s3cR3t".into(), + }); + let db = db_builder.build().await.unwrap(); + + let conn = db.connect().unwrap(); + conn.execute("CREATE TABLE IF NOT EXISTS messages (text TEXT)", ()) + .await + .unwrap(); + let params = params!["the only winning move is not to play"]; + conn.execute("INSERT INTO messages (text) VALUES (?)", params) + .await + .unwrap(); + } + + // lets test encryption with ATTACH + { + let db = Builder::new_local(&base_path).build().await.unwrap(); + let conn = db.connect().unwrap(); + let attach_stmt = format!( + "ATTACH DATABASE '{}' AS encrypted KEY 's3cR3t'", + tempdir.join("encrypted.db").display() + ); + conn.execute(&attach_stmt, ()).await.unwrap(); + let mut attached_results = conn + .query("SELECT * FROM encrypted.messages", ()) + .await + .unwrap(); + let row = attached_results.next().await.unwrap().unwrap(); + let text: String = row.get(0).unwrap(); + assert_eq!(text, "the only winning move is not to play"); + } + + { + let _ = std::fs::remove_file(&encrypted_path); + let _ = std::fs::remove_file(&base_path); + } +}