Skip to main content

shared/
crypt.rs

1use sha2::Digest;
2use std::sync::LazyLock;
3
4const BCRYPT_COST: u32 = 12;
5const DUMMY_HASH: &str = "$2a$12$am3C/uakIz4zE7LHEEPjI.CmfRECf79hfaThUs801uJVg0vGFRg1y";
6
7static SEMAPHORE: LazyLock<tokio::sync::Semaphore> = LazyLock::new(|| {
8    tokio::sync::Semaphore::new(
9        std::thread::available_parallelism()
10            .map(|n| n.get() * 2)
11            .unwrap_or(4),
12    )
13});
14
15/// A bcrypt hash as stored in the database.
16///
17/// Emits `$2a$` rather than the crate default `$2b$`, as pgcrypto silently falls back to DES for `$2b$`.
18#[derive(Debug, Clone, PartialEq, Eq, sqlx::Type)]
19#[sqlx(transparent)]
20pub struct BcryptString(String);
21
22impl BcryptString {
23    fn hash_blocking(password: &str) -> Result<Self, bcrypt::BcryptError> {
24        bcrypt::hash_with_result(password, BCRYPT_COST)
25            .map(|parts| Self(parts.format_for_version(bcrypt::Version::TwoA)))
26    }
27
28    pub async fn hash(password: &str) -> Result<Self, anyhow::Error> {
29        let password = password.to_owned();
30        let _permit = SEMAPHORE.acquire().await?;
31
32        Ok(tokio::task::spawn_blocking(move || Self::hash_blocking(&password)).await??)
33    }
34
35    pub async fn verify(&self, password: &str) -> Result<bool, anyhow::Error> {
36        let password = password.to_owned();
37        let hash = self.0.clone();
38        let _permit = SEMAPHORE.acquire().await?;
39
40        Ok(tokio::task::spawn_blocking(move || bcrypt::verify(password, &hash)).await??)
41    }
42
43    /// Burns the same time a real verification would so a missing user or a user without a password
44    /// is indistinguishable from a wrong password.
45    pub async fn verify_dummy(password: &str) -> Result<(), anyhow::Error> {
46        Self(DUMMY_HASH.to_owned()).verify(password).await?;
47
48        Ok(())
49    }
50
51    #[inline]
52    pub fn needs_rehash(&self) -> bool {
53        !self.0.starts_with(&format!("$2a${BCRYPT_COST:02}$"))
54    }
55}
56
57impl AsRef<str> for BcryptString {
58    fn as_ref(&self) -> &str {
59        &self.0
60    }
61}
62
63/// An encrypted string as stored in the database.
64///
65/// Stores the encrypted bytes of the original plaintext.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct EncryptedString(tokio_util::bytes::Bytes);
68
69impl EncryptedString {
70    #[inline]
71    pub fn blocking_from_plaintext(
72        plaintext: impl AsRef<[u8]>,
73        database: &super::database::Database,
74    ) -> Result<Self, anyhow::Error> {
75        database
76            .blocking_encrypt(plaintext.as_ref())
77            .map(|bytes| Self(bytes.into()))
78    }
79
80    #[inline]
81    pub async fn from_plaintext(
82        plaintext: impl AsRef<[u8]> + Send + 'static,
83        database: &super::database::Database,
84    ) -> Result<Self, anyhow::Error> {
85        database
86            .encrypt(plaintext)
87            .await
88            .map(|bytes| Self(bytes.into()))
89    }
90
91    #[inline]
92    pub async fn from_plaintext_with_input<P: AsRef<[u8]> + Send + 'static>(
93        plaintext: P,
94        database: &super::database::Database,
95    ) -> Result<(P, Self), anyhow::Error> {
96        database
97            .encrypt_with_input(plaintext)
98            .await
99            .map(|(plaintext, bytes)| (plaintext, Self(bytes.into())))
100    }
101
102    #[inline]
103    pub fn blocking_decrypt(
104        &self,
105        database: &super::database::Database,
106    ) -> Result<compact_str::CompactString, anyhow::Error> {
107        database.blocking_decrypt(&self.0)
108    }
109
110    #[inline]
111    pub async fn decrypt(
112        &self,
113        database: &super::database::Database,
114    ) -> Result<compact_str::CompactString, anyhow::Error> {
115        database.decrypt(self.0.clone()).await
116    }
117}
118
119impl AsRef<[u8]> for EncryptedString {
120    fn as_ref(&self) -> &[u8] {
121        &self.0
122    }
123}
124
125/// Serialized as a byte string, which msgpack stores as a `bin` rather than the array a `Vec<u8>`
126/// produces, around a third smaller for ciphertext. Both forms are accepted on the way back in, so
127/// entries cached as either decode.
128impl serde::Serialize for EncryptedString {
129    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
130        serializer.serialize_bytes(&self.0)
131    }
132}
133impl<'de> serde::Deserialize<'de> for EncryptedString {
134    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
135        struct EncryptedStringVisitor;
136
137        impl<'de> serde::de::Visitor<'de> for EncryptedStringVisitor {
138            type Value = EncryptedString;
139
140            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
141                formatter.write_str("a byte string or a sequence of bytes")
142            }
143
144            fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
145                Ok(EncryptedString(tokio_util::bytes::Bytes::copy_from_slice(
146                    bytes,
147                )))
148            }
149
150            fn visit_byte_buf<E: serde::de::Error>(self, bytes: Vec<u8>) -> Result<Self::Value, E> {
151                Ok(EncryptedString(bytes.into()))
152            }
153
154            fn visit_seq<A: serde::de::SeqAccess<'de>>(
155                self,
156                mut seq: A,
157            ) -> Result<Self::Value, A::Error> {
158                let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or_default());
159
160                while let Some(byte) = seq.next_element()? {
161                    bytes.push(byte);
162                }
163
164                Ok(EncryptedString(bytes.into()))
165            }
166        }
167
168        deserializer.deserialize_byte_buf(EncryptedStringVisitor)
169    }
170}
171
172impl sqlx::Type<sqlx::Postgres> for EncryptedString {
173    fn type_info() -> sqlx::postgres::PgTypeInfo {
174        <[u8] as sqlx::Type<sqlx::Postgres>>::type_info()
175    }
176
177    fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
178        <[u8] as sqlx::Type<sqlx::Postgres>>::compatible(ty)
179    }
180}
181impl sqlx::Encode<'_, sqlx::Postgres> for EncryptedString {
182    fn encode_by_ref(
183        &self,
184        buf: &mut sqlx::postgres::PgArgumentBuffer,
185    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
186        <&[u8] as sqlx::Encode<sqlx::Postgres>>::encode(&self.0, buf)
187    }
188
189    fn size_hint(&self) -> usize {
190        self.0.len()
191    }
192}
193impl sqlx::Decode<'_, sqlx::Postgres> for EncryptedString {
194    fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
195        Ok(Self(match value.format() {
196            sqlx::postgres::PgValueFormat::Binary => {
197                tokio_util::bytes::Bytes::copy_from_slice(value.as_bytes()?)
198            }
199            sqlx::postgres::PgValueFormat::Text => hex::decode(
200                value
201                    .as_bytes()?
202                    .strip_prefix(b"\\x")
203                    .ok_or("text does not start with \\x")?,
204            )?
205            .into(),
206        }))
207    }
208}
209
210/// Random high-entropy tokens (sessions, api keys) are stored as a plain digest, a KDF buys nothing
211/// against a 256-bit preimage and the deterministic value makes the unique index a real lookup.
212#[inline]
213pub fn token_digest(token: &str) -> String {
214    hex::encode(sha2::Sha256::digest(token.as_bytes()))
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[tokio::test]
222    async fn hash_emits_two_a_at_current_cost() {
223        let hash = BcryptString::hash("hunter2").await.unwrap();
224
225        assert!(hash.0.starts_with("$2a$12$"));
226        assert_eq!(hash.0.len(), 60);
227        assert!(!hash.needs_rehash());
228    }
229
230    #[tokio::test]
231    async fn verify_roundtrip() {
232        let hash = BcryptString::hash("hunter2").await.unwrap();
233
234        assert!(hash.verify("hunter2").await.unwrap());
235        assert!(!hash.verify("hunter3").await.unwrap());
236    }
237
238    #[tokio::test]
239    async fn verify_accepts_imported_variants() {
240        let two_y = BcryptString(
241            bcrypt::hash_with_result("hunter2", 4)
242                .unwrap()
243                .format_for_version(bcrypt::Version::TwoY),
244        );
245        let two_a_low_cost = BcryptString(
246            bcrypt::hash_with_result("hunter2", 6)
247                .unwrap()
248                .format_for_version(bcrypt::Version::TwoA),
249        );
250
251        assert!(two_y.verify("hunter2").await.unwrap());
252        assert!(two_a_low_cost.verify("hunter2").await.unwrap());
253        assert!(two_y.needs_rehash());
254        assert!(two_a_low_cost.needs_rehash());
255    }
256
257    #[tokio::test]
258    async fn verify_dummy_does_not_fail() {
259        BcryptString::verify_dummy("anything").await.unwrap();
260    }
261
262    #[test]
263    fn dummy_hash_is_at_current_cost() {
264        assert!(!BcryptString(DUMMY_HASH.to_owned()).needs_rehash());
265    }
266
267    #[test]
268    fn encrypted_string_encodes_raw_bytes() {
269        let encrypted = EncryptedString(tokio_util::bytes::Bytes::from_static(b"\x00\xffabc"));
270        let mut buf = sqlx::postgres::PgArgumentBuffer::default();
271
272        let is_null =
273            <EncryptedString as sqlx::Encode<sqlx::Postgres>>::encode_by_ref(&encrypted, &mut buf)
274                .unwrap();
275
276        assert!(!is_null.is_null());
277        assert_eq!(&**buf, b"\x00\xffabc");
278        assert_eq!(
279            <EncryptedString as sqlx::Encode<sqlx::Postgres>>::size_hint(&encrypted),
280            5
281        );
282    }
283
284    #[test]
285    fn encrypted_string_serializes_as_a_msgpack_bin() {
286        let encrypted = EncryptedString(tokio_util::bytes::Bytes::from_static(b"\x00\xffabc"));
287        let encoded = rmp_serde::to_vec(&encrypted).unwrap();
288
289        assert_eq!(encoded, b"\xc4\x05\x00\xffabc");
290        assert_eq!(
291            rmp_serde::from_slice::<EncryptedString>(&encoded).unwrap(),
292            encrypted
293        );
294    }
295
296    #[test]
297    fn encrypted_string_decodes_the_vec_form_it_replaced() {
298        let bytes: Vec<u8> = (0..=255).collect();
299        let encrypted = EncryptedString(bytes.clone().into());
300
301        assert_eq!(
302            rmp_serde::from_slice::<EncryptedString>(&rmp_serde::to_vec(&bytes).unwrap()).unwrap(),
303            encrypted
304        );
305        assert!(
306            rmp_serde::to_vec(&encrypted).unwrap().len() < rmp_serde::to_vec(&bytes).unwrap().len()
307        );
308    }
309
310    #[test]
311    fn encrypted_string_round_trips_through_json() {
312        let encrypted = EncryptedString(tokio_util::bytes::Bytes::from_static(b"\x00\xffabc"));
313        let encoded = serde_json::to_string(&encrypted).unwrap();
314
315        assert_eq!(encoded, "[0,255,97,98,99]");
316        assert_eq!(
317            serde_json::from_str::<EncryptedString>(&encoded).unwrap(),
318            encrypted
319        );
320    }
321
322    #[test]
323    fn token_digest_is_deterministic_hex_sha256() {
324        let digest = token_digest("c7sp_abc");
325
326        assert_eq!(digest.len(), 64);
327        assert_eq!(digest, token_digest("c7sp_abc"));
328        assert_ne!(digest, token_digest("c7sp_abd"));
329    }
330}