pyrus_cert_store/lib.rs
1//! # Pyrus Cert Store
2//!
3//! This crate's main purpose is to provide a simple API for "secure"
4//! [`Cert`](pyrus_crypto::cert::Cert) storage. It is largely inspired
5//! by [`sequoia-cert-store`](https://docs.rs/sequoia-cert-store/latest/sequoia_cert_store/).
6//!
7//! # A note on security
8//!
9//! This crate makes no security guarantees and its security goes as far
10//! as the author's will to make his school project secure.
11//!
12//! There were no and will not be any security audits of this crate and
13//! so use with caution.
14//!
15//! # How it works?
16//!
17//! The storage backend is
18//! [`rusqlite`] with crate feature `bundled-sqlcipher-vendored-openssl`
19//! enabled. This allows for encrypting the SQL database and keeps the secrets
20//! "secure".
21//!
22//! Certificates are stored as [`LazyCert`]s, which is basically a serialized
23//! (unparsed) certificate, a fingerprint, and a user id, which allows for
24//! filtering and listing the certificates without parsing them which is
25//! fallible.
26//!
27//! # Examples
28//! ## Openning a store and saving a certificate
29//! ```rust
30//! use pyrus_cert_store::{CertStore, LazyCert};
31//! # use std::error::Error;
32//! # use pyrus_crypto::prelude::*;
33//!
34//! # fn main() -> Result<(), Box<dyn Error>> {
35//! let my_cert: Cert = //..
36//! # Cert::new("Alice");
37//! let store = CertStore::open("certstore.db3")
38//! # ;
39//! # let store = CertStore::open("")
40//! .with_passphrase(String::from("password123"), b"use a better password and salt")
41//! .connect()?;
42//!
43//! store.insert(LazyCert::try_from(&my_cert)?)?;
44//! let stored_cert: LazyCert = store.get(my_cert.fingerprint())?;
45//! let stored_cert = stored_cert.to_cert()?;
46//!
47//! assert_eq!(my_cert, stored_cert);
48//! # Ok(())
49//! # }
50//! ```
51//! ## Openning a store in memory and removing a saved certificate
52//! ```rust
53//! use pyrus_cert_store::{CertStore, LazyCert};
54//! # use std::error::Error;
55//! # use pyrus_crypto::prelude::*;
56//!
57//! # fn main() -> Result<(), Box<dyn Error>> {
58//! let my_cert: Cert = //..
59//! # Cert::new("Alice");
60//!
61//! // passing "" as path opens the store in memory
62//! let store = CertStore::open("")
63//! .with_passphrase(String::from("password123"), b"use a better password and salt")
64//! .connect()?;
65//!
66//! store.insert(LazyCert::try_from(&my_cert)?)?;
67//! store.remove(my_cert.fingerprint())?;
68//!
69//! assert!(store.get(my_cert.fingerprint()).is_err());
70//! # Ok(())
71//! # }
72//! ```
73//! ## Openning an unencrypted store
74//!
75//! Since encryption is done using an SQL pragma there is no way to prove
76//! that in tests.
77//! ```rust
78//! use pyrus_cert_store::{CertStore, LazyCert};
79//! # use std::error::Error;
80//! # use pyrus_crypto::prelude::*;
81//!
82//! # fn main() -> Result<(), Box<dyn Error>> {
83//! # let my_cert: Cert = //..
84//! # Cert::new("Alice");
85//!
86//! // not configuring a passphrase assumes no encryption
87//! let store = CertStore::open("").connect()?;
88//!
89//! // dropping a store safely flushes all statements and saves it
90//! // well this one is in memory so it will be simply dropped
91//! # Ok(())
92//! # }
93//! ```
94
95mod connection;
96pub use connection::CertStoreConn;
97pub mod error;
98mod lazy_cert;
99pub use lazy_cert::LazyCert;
100mod store;
101pub use store::CertStore;
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use pyrus_crypto::prelude::*;
107 use std::error::Error;
108
109 fn make_certs() -> Vec<Cert> {
110 let mut certs = vec![];
111 for i in 0..5 {
112 certs.push(Cert::new(&format!("Cert {i}")));
113 }
114 certs
115 }
116
117 #[test]
118 fn unencrypted_store_works() -> Result<(), Box<dyn Error>> {
119 let certs: Vec<Cert> = make_certs()
120 .iter()
121 .map(|i| i.clone().strip_secrets())
122 .collect();
123
124 let store = CertStore::open("").connect()?;
125
126 for c in certs.iter().map(|i| LazyCert::try_from(i).unwrap()) {
127 store.insert(c)?;
128 }
129
130 let c0 = certs.get(0).unwrap();
131
132 let c0_stored = store.get(c0.fingerprint())?;
133 let c0_retrieved = c0_stored.to_cert()?;
134
135 assert_eq!(c0, &c0_retrieved);
136
137 store.remove(c0.fingerprint())?;
138 assert!(store.get(c0.fingerprint()).is_err());
139
140 Ok(())
141 }
142
143 #[test]
144 fn encrypted_store_works() -> Result<(), Box<dyn Error>> {
145 let certs = make_certs();
146
147 let password = String::from("secretpass2137");
148 let salt = blake3::hash(b"saltysalt");
149 let store = CertStore::open("")
150 .with_passphrase(password, salt.as_bytes())
151 .connect()?;
152
153 for c in certs.iter().map(|i| LazyCert::try_from(i).unwrap()) {
154 store.insert(c)?;
155 }
156
157 let c0 = certs.get(0).unwrap();
158
159 let c0_stored = store.get(c0.fingerprint())?;
160 let c0_retrieved = c0_stored.to_cert()?;
161
162 assert_eq!(c0, &c0_retrieved);
163
164 store.remove(c0.fingerprint())?;
165 assert!(store.get(c0.fingerprint()).is_err());
166
167 Ok(())
168 }
169
170 #[test]
171 fn store_in_dir() -> Result<(), Box<dyn Error>> {
172 let certs = make_certs();
173
174 let store_file = temp_file::empty();
175 let password = String::from("secretpass2137");
176 let salt = blake3::hash(b"saltysalt");
177 {
178 let store = CertStore::open(store_file.path())
179 .with_passphrase(password.clone(), salt.as_bytes())
180 .connect()?;
181 for c in certs.iter().map(|i| LazyCert::try_from(i).unwrap()) {
182 store.insert(c)?;
183 }
184 }
185 {
186 let store = CertStore::open(store_file.path())
187 .with_passphrase(password, salt.as_bytes())
188 .connect()?;
189 let store_certs = store.certs()?;
190 assert!(!store_certs.is_empty());
191
192 for (cert, store_cert) in certs.iter().zip(store_certs.iter()) {
193 let store_cert = store_cert.to_cert()?;
194 assert_eq!(cert, &store_cert);
195 }
196 }
197 Ok(())
198 }
199
200 #[test]
201 fn encrypted_store_passwords() -> Result<(), Box<dyn Error>> {
202 let store_file = temp_file::empty();
203 let password1 = String::from("secretpass2137");
204 let salt1 = blake3::hash(b"saltysalt");
205 {
206 let _store = CertStore::open(store_file.path())
207 .with_passphrase(password1.clone(), salt1.as_bytes())
208 .connect()?;
209 }
210 let salt2 = blake3::hash(b"sweetsalt");
211 {
212 let store = CertStore::open(store_file.path())
213 .with_passphrase(password1, salt2.as_bytes())
214 .connect();
215 assert!(store.is_err());
216 }
217 let password2 = String::from("secretpass69420");
218 {
219 let store = CertStore::open(store_file.path())
220 .with_passphrase(password2, salt1.as_bytes())
221 .connect();
222 assert!(store.is_err());
223 }
224 Ok(())
225 }
226
227 #[test]
228 fn store_retains_all() -> Result<(), Box<dyn Error>> {
229 let certs: Vec<Cert> = make_certs()
230 .iter()
231 .map(|i| i.clone().strip_secrets())
232 .collect();
233
234 let store = CertStore::open("").connect()?;
235
236 for c in certs.iter().map(|i| LazyCert::try_from(i).unwrap()) {
237 store.insert(c)?;
238 }
239
240 let uids = store.uids()?;
241
242 for (cert, store_cert) in certs.iter().zip(uids.iter()) {
243 let (fpr, uid) = store_cert;
244 assert_eq!(cert.fingerprint(), fpr);
245 assert_eq!(cert.user_id(), uid);
246 }
247
248 let store_certs = store.certs()?;
249
250 for (cert, store_cert) in certs.iter().zip(store_certs.iter()) {
251 let store_cert = store_cert.to_cert()?;
252 assert_eq!(cert, &store_cert);
253 }
254
255 Ok(())
256 }
257}