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}