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#[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 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#[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
125impl 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#[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}