Skip to content

Commit

Permalink
Explore using sqlite
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton committed Mar 7, 2024
1 parent d2c6897 commit 21e6143
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 0 deletions.
95 changes: 95 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/bitwarden/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ rand = ">=0.8.5, <0.9"
reqwest = { version = ">=0.11, <0.12", features = [
"json",
], default-features = false }
rusqlite = { version = ">=0.31.0, <0.32", features = ["uuid"] }
schemars = { version = ">=0.8.9, <0.9", features = ["uuid1", "chrono"] }
serde = { version = ">=1.0, <2.0", features = ["derive"] }
serde_json = ">=1.0.96, <2.0"
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ pub enum Error {
InvalidBase64(#[from] base64::DecodeError),
#[error(transparent)]
Chrono(#[from] chrono::ParseError),
#[error(transparent)]
Sqlite(#[from] rusqlite::Error),

#[error("Received error message from server: [{}] {}", .status, .message)]
ResponseContent { status: StatusCode, message: String },
Expand Down
1 change: 1 addition & 0 deletions crates/bitwarden/src/vault/cipher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub(crate) mod identity;
pub(crate) mod linked_id;
pub(crate) mod local_data;
pub(crate) mod login;
pub(crate) mod repository;
pub(crate) mod secure_note;

pub use attachment::{
Expand Down
191 changes: 191 additions & 0 deletions crates/bitwarden/src/vault/cipher/repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use rusqlite::{params, Connection};
use uuid::Uuid;

use crate::error::Error;

use super::Cipher;

struct CipherRow {
id: Uuid,
value: String,
}

struct CipherSqliteRepository {
conn: Connection,
}

impl CipherSqliteRepository {
pub fn new(conn: Connection) -> Self {
// TODO: Handle schema migrations
conn.execute(
"CREATE TABLE IF NOT EXISTS ciphers (
id TEXT PRIMARY KEY,
value TEXT NOT NULL
)",
(),
)
.unwrap();

Self { conn }
}

pub fn save(&self, cipher: &Cipher) -> Result<(), Error> {
let id = cipher.id.unwrap();
let serialized = serde_json::to_string(cipher)?;

let mut stmt = self.conn.prepare(
"
INSERT INTO ciphers (id, value)
VALUES (?1, ?2)
ON CONFLICT(id) DO UPDATE SET
value = ?2
",
)?;
stmt.execute((&id, &serialized))?;

Ok(())
}

/// Replace all ciphers in the repository with the given ciphers.
///
/// Typically used during a sync operation.
pub fn replace_all(&mut self, ciphers: &[&Cipher]) -> Result<(), Error> {
let tx = self.conn.transaction()?;
{
tx.execute("DELETE FROM ciphers", [])?;

let mut stmt = tx.prepare(
"
INSERT INTO ciphers (id, value)
VALUES (?1, ?2)
",
)?;

for cipher in ciphers {
let id = cipher.id.unwrap();
let serialized = serde_json::to_string(&cipher)?;

stmt.execute(params![id, serialized])?;
}
}
tx.commit()?;

Ok(())
}

pub fn delete_by_id(&self, id: Uuid) -> Result<(), Error> {
let mut stmt = self.conn.prepare("DELETE FROM ciphers WHERE id = ?1")?;
stmt.execute(params![id])?;

Ok(())
}

pub fn get_all(&self) -> Result<Vec<Cipher>, Error> {
let mut stmt = self.conn.prepare("SELECT id, value FROM ciphers")?;
let rows = stmt.query_map([], |row| {
Ok(CipherRow {
id: row.get(0)?,
value: row.get(1)?,
})
})?;

let ciphers: Vec<Cipher> = rows
.flatten()
.flat_map(|row| -> Result<Cipher, Error> {
let cipher: Cipher = serde_json::from_str(&row.value)?;
Ok(cipher)
})
.collect();

Ok(ciphers)
}
}

#[cfg(test)]
mod tests {
use crate::vault::{CipherRepromptType, CipherType};

use super::*;
use rusqlite::Connection;

fn mock_cipher(id: Uuid) -> Cipher {
Cipher {
id: Some(id),
organization_id: None,
folder_id: None,
collection_ids: vec![],
key: None,
name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
notes: None,
r#type: CipherType::Login,
login: None,
identity: None,
card: None,
secure_note: None,
favorite: false,
reprompt: CipherRepromptType::None,
organization_use_totp: false,
edit: true,
view_password: true,
local_data: None,
attachments: None,
fields: None,
password_history: None,
creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
deleted_date: None,
revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
}
}

#[test]
fn test_save_get_all() {
let conn = Connection::open_in_memory().unwrap();
let repo = CipherSqliteRepository::new(conn);

let cipher = mock_cipher("d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap());

repo.save(&cipher).unwrap();

let ciphers = repo.get_all().unwrap();

assert_eq!(ciphers.len(), 1);
assert_eq!(ciphers[0].id, cipher.id);
}

#[test]
fn test_delete_by_id() {
let conn = Connection::open_in_memory().unwrap();
let repo = CipherSqliteRepository::new(conn);

let cipher = mock_cipher("d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap());
repo.save(&cipher).unwrap();

let ciphers = repo.get_all().unwrap();
assert_eq!(ciphers.len(), 1);

repo.delete_by_id(cipher.id.unwrap()).unwrap();
let ciphers = repo.get_all().unwrap();
assert_eq!(ciphers.len(), 0);
}

#[test]
fn test_replace_all() {
let conn = Connection::open_in_memory().unwrap();
let mut repo = CipherSqliteRepository::new(conn);

let old_cipher = mock_cipher("d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap());

repo.save(&old_cipher).unwrap();

let ciphers = repo.get_all().unwrap();
assert_eq!(ciphers.len(), 1);
assert_eq!(ciphers[0].id, old_cipher.id);

let new_cipher = mock_cipher("d55d65d7-c161-40a4-94ca-b0d20184d91c".parse().unwrap());
repo.replace_all(&[&new_cipher]).unwrap();

let ciphers = repo.get_all().unwrap();
assert_eq!(ciphers.len(), 1);
assert_eq!(ciphers[0].id, new_cipher.id);
}
}

0 comments on commit 21e6143

Please sign in to comment.