pyrus_cert_store/
connection.rs

1use argon2::{Algorithm, Argon2, Version};
2
3use rusqlite::{params, Connection};
4
5use zeroize::{Zeroize, Zeroizing};
6
7use std::path::Path;
8
9use crate::error::{Result, StoreError};
10use crate::store::CertStore;
11
12/// A [`CertStore`] connection builder.
13///
14/// An instance of this struct is returned by [`CertStore::open`] and can be 
15/// configured using exposed methods. In simple terms:
16/// * [`CertStoreConn::with_params`] - changes the Argon2 parameters used for 
17/// password hashing (only applicable if using encryption),
18/// * [`CertStoreConn::with_passphrase`] - enables encryption. The user 
19/// supplies a password and a salt,
20/// * [`CertStoreConn::connect`] - starts the connection to the underlying 
21/// SQL database.
22///
23/// # Example
24/// Start a connection with custom password hashing parameters.
25/// ```rust
26/// use pyrus_cert_store::CertStore;
27/// # use std::error::Error;
28/// # use pyrus_crypto::prelude::*;
29///
30/// # fn main() -> Result<(), Box<dyn Error>> {
31/// let store = CertStore::open("certstore.db3")
32/// # ;
33/// # let store = CertStore::open("")
34///     // 20 blocks of memory used, 3 threads, 4 iterations
35///     .with_params(20 * 1024, 3, 4)
36///     .with_passphrase(String::from("password123"), b"use a better password and salt")
37///     .connect()?;
38/// # Ok(())
39/// # }
40/// ```
41pub struct CertStoreConn<P: AsRef<Path>> {
42    passphrase: Option<String>,
43    salt: Option<Vec<u8>>,
44    path: P,
45    memory: u32,
46    threads: u32,
47    iterations: u32,
48}
49
50impl<P: AsRef<Path>> CertStoreConn<P> {
51    fn params(&self) -> Result<argon2::Params> {
52        argon2::Params::new(self.memory, self.iterations, self.threads, Some(64))
53            .map_err(|_| StoreError::InvalidParams)
54    }
55
56    pub(crate) fn new(path: P) -> Self {
57        Self {
58            passphrase: None,
59            salt: None,
60            path,
61            memory: argon2::Params::DEFAULT_M_COST,
62            threads: argon2::Params::DEFAULT_P_COST,
63            iterations: argon2::Params::DEFAULT_T_COST,
64        }
65    }
66
67    /// Modifies the parameters used for the Argon2 password hashing 
68    /// algorithm. For detailed information about these parameters 
69    /// read the [`argon2`] crate documentation.
70    ///
71    /// In simple terms:
72    /// * `memory` - the number of 1 KiB memory blocks,
73    /// * `threads` - the number of threads used for calculations,
74    /// * `iterations` - the number of passes through the algorithm.
75    pub fn with_params(mut self, memory: u32, threads: u32, iterations: u32) -> Self {
76        self.memory = memory;
77        self.threads = threads;
78        self.iterations = iterations;
79        self
80    }
81
82    /// Sets a passphrase to be used for the connection. Not calling this 
83    /// method is equivalent to not enabling encryption for the [`CertStore`]. 
84    ///
85    /// # Example
86    /// Using a wrong password results in an error
87    /// ```rust
88    /// # use pyrus_cert_store::CertStore;
89    /// # use std::error::Error;
90    /// # use std::path::Path;
91    /// # fn main() -> Result<(), Box<dyn Error>> {
92    /// let store_file = Path::new("certstore.db3");
93    /// # let file = temp_file::empty();
94    /// # let store_file = file.path();
95    /// {
96    ///     let store = CertStore::open(store_file)
97    ///         .with_passphrase(String::from("1234"), b"saltysalt")
98    ///         .connect()?;
99    /// } // drops the connection
100    /// {
101    ///     // reconnect with a wrong password
102    ///     let store = CertStore::open(store_file)
103    ///         .with_passphrase(String::from("banana"), b"saltysalt")
104    ///         .connect();
105    ///
106    ///     assert!(store.is_err());
107    /// }
108    /// # Ok(())
109    /// # }
110    /// ```
111    pub fn with_passphrase<S: AsRef<[u8]>>(mut self, passphrase: String, salt: S) -> Self {
112        self.passphrase = Some(passphrase);
113        self.salt = Some(salt.as_ref().to_vec());
114        self
115    }
116
117    fn derive_key(&self) -> Result<Zeroizing<[u8; 64]>> {
118        let params = self.params()?;
119        let hasher = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
120        let mut key = Zeroizing::new([0; 64]);
121        let passphrase = self.passphrase.as_ref().unwrap();
122        let salt = self.salt.as_ref().unwrap();
123        hasher.hash_password_into(passphrase.as_bytes(), salt, &mut *key)?;
124        Ok(key)
125    }
126
127    /// Attempts the connection to the underlying SQL database. If the 
128    /// database does not exist, it is created and initialized.
129    ///
130    /// # Errors
131    /// * [`StoreError::InvalidPath`] when the store path cannot be converted 
132    /// to a C-compatible string.
133    /// * [`StoreError::SQLiteFail`] when any other `rusqlite` error occured.
134    pub fn connect(self) -> Result<CertStore> {
135        let conn = if self.path.as_ref() != Path::new("") {
136            match Connection::open(&self.path) {
137                Ok(c) => c,
138                Err(rusqlite::Error::NulError(_)) => return Err(StoreError::InvalidPath),
139                Err(e) => return Err(StoreError::SQLiteFail(e)),
140            }
141        } else {
142            Connection::open_in_memory()?
143        };
144        // enable encryption
145        if self.passphrase.is_some() && self.salt.is_some() {
146            let key = self.derive_key()?;
147            let key = Zeroizing::new(hex::encode(&key));
148            conn.pragma_update(None, "key", hex::encode(key))?;
149            conn.pragma_update(None, "cipher_memory_security", "ON")?;
150        }
151        conn.query_row("SELECT COUNT(*) FROM `sqlite_master`;", params![], |_| {
152            Ok(())
153        })?;
154        conn.execute(
155            "CREATE TABLE IF NOT EXISTS certs (
156                fpr TEXT UNIQUE NOT NULL,
157                uid TEXT NOT NULL,
158                bytes BLOB NOT NULL
159            )",
160            (),
161        )?;
162        Ok(CertStore { conn })
163    }
164}
165
166/// Zeroizes the passphrase and salt on drop. This is for security reasons 
167/// to not leave the passphrase in memory after opening the connection.
168impl<P: AsRef<Path>> Drop for CertStoreConn<P> {
169    fn drop(&mut self) {
170        self.passphrase.zeroize();
171        self.salt.zeroize();
172    }
173}