firebase_scrypt/
lib.rs

1//! Implementation of [Firebase Scrypt](https://github.com/firebase/scrypt) in pure Rust.
2//!
3//! If you are only using the raw functions instead of the higher-level struct [`FirebaseScrypt`],
4//! it's recommended to disable default features in your ``Cargo.toml``
5//!
6//! ```toml
7//! [dependencies]
8//! firebase-scrypt = { version = "0.1", default-features = false }
9//! ```
10//!
11//! # Usage (with ``simple`` feature)
12//! ```
13//! use firebase_scrypt::FirebaseScrypt;
14//!
15//! const SALT_SEPARATOR: &str = "Bw==";
16//! const SIGNER_KEY: &str = "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==";
17//! const ROUNDS: u32 = 8;
18//! const MEM_COST: u32 = 14;
19//!
20//! let firebase_scrypt = FirebaseScrypt::new(SALT_SEPARATOR, SIGNER_KEY, ROUNDS, MEM_COST);
21//!
22//! let password = "user1password";
23//! let salt = "42xEC+ixf3L2lw==";
24//! let password_hash ="lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ==";
25//!
26//! assert!(firebase_scrypt.verify_password(password, salt, password_hash).unwrap())
27//! ```
28
29use crate::errors::{DerivedKeyError, EncryptError, GenerateHashError};
30use aes::{
31    cipher::{KeyIvInit, StreamCipher},
32    Aes256,
33};
34use constant_time_eq::constant_time_eq;
35use ctr::Ctr128BE;
36use scrypt::Params;
37
38pub mod errors;
39#[cfg(feature = "simple")]
40mod simple;
41
42#[cfg(feature = "simple")]
43pub use simple::FirebaseScrypt;
44
45const IV: [u8; 16] = *b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
46
47fn clean(a: &str) -> String {
48    a.replace("-", "+").replace("_", "/")
49}
50
51fn generate_derived_key<'a>(
52    password: &'a str,
53    salt: &'a str,
54    salt_separator: &'a str,
55    rounds: u32,
56    mem_cost: u32,
57) -> Result<[u8; 64], DerivedKeyError> {
58    let log2_n = 2_f32.powf(mem_cost as f32).log2().floor() as u32;
59    let p: u32 = 1;
60
61    debug_assert!(log2_n < 64, "log2 of n must not be larger than 64");
62
63    let mut salt = base64::decode(salt)?;
64    salt.append(&mut base64::decode(salt_separator)?);
65    let password = password.as_bytes();
66
67    let params = Params::new(log2_n as u8, rounds, p)?;
68
69    let mut result = [0u8; 64];
70    scrypt::scrypt(password, salt.as_slice(), &params, &mut result)?;
71
72    Ok(result)
73}
74
75fn encrypt(signer_key: &[u8], key: [u8; 32]) -> Result<Vec<u8>, EncryptError> {
76    let mut cipher = Ctr128BE::<Aes256>::new(&key.into(), &IV.into());
77
78    let mut buffer = vec![0u8; signer_key.len()];
79    cipher.apply_keystream_b2b(signer_key, &mut buffer)?;
80
81    Ok(buffer)
82}
83
84/// Verifies the password with a given known hash.
85///
86/// In case the salt separator, signer key, number of rounds and cost of memory don't change in
87/// runtime, you may want to use the [`FirebaseScrypt`] struct to manage them.
88///
89/// # Example
90/// ```
91/// use firebase_scrypt::verify_password;
92///
93/// const SALT_SEPARATOR: &str = "Bw==";
94/// const SIGNER_KEY: &str = "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==";
95/// const ROUNDS: u32 = 8;
96/// const MEM_COST: u32 = 14;
97///
98/// let password = "user1password";
99/// let salt = "42xEC+ixf3L2lw==";
100/// let password_hash ="lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ==";
101///
102/// let is_valid = verify_password(
103///     password,
104///     password_hash,
105///     salt,
106///     SALT_SEPARATOR,
107///     SIGNER_KEY,
108///     ROUNDS,
109///     MEM_COST,
110/// ).unwrap();
111///
112/// assert!(is_valid)
113/// ```
114pub fn verify_password(
115    password: &str,
116    known_hash: &str,
117    salt: &str,
118    salt_separator: &str,
119    signer_key: &str,
120    rounds: u32,
121    mem_cost: u32,
122) -> Result<bool, GenerateHashError> {
123    let password_hash =
124        generate_raw_hash(password, salt, salt_separator, signer_key, rounds, mem_cost)?;
125
126    Ok(constant_time_eq(
127        password_hash.as_slice(),
128        base64::decode(clean(known_hash))?.as_slice(),
129    ))
130}
131
132/// Generates a hash in the form of a [`Vec<u8>`]
133///
134/// In case you want or are using the same hash representation as Firebase, use the [`FirebaseScrypt`]
135/// struct to get the Base64 hashed directly.
136///
137/// # Example (generate Base64 hash)
138/// ```
139/// // Base64 crate for encoding the hash
140/// use base64::encode;
141/// use firebase_scrypt::generate_raw_hash;
142///
143/// const SALT_SEPARATOR: &str = "Bw==";
144/// const SIGNER_KEY: &str = "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==";
145/// const ROUNDS: u32 = 8;
146/// const MEM_COST: u32 = 14;
147///
148/// let password = "user1password";
149/// let salt = "42xEC+ixf3L2lw==";
150/// let password_hash ="lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ==";
151///
152/// let hash = encode(generate_raw_hash(
153///     password,
154///     salt,
155///     SALT_SEPARATOR,
156///     SIGNER_KEY,
157///     ROUNDS,
158///     MEM_COST,
159/// ).unwrap());
160///
161/// assert_eq!(hash, password_hash);
162/// ```
163pub fn generate_raw_hash(
164    password: &str,
165    salt: &str,
166    salt_separator: &str,
167    signer_key: &str,
168    rounds: u32,
169    mem_cost: u32,
170) -> Result<Vec<u8>, GenerateHashError> {
171    let derived_key =
172        generate_derived_key(password, &clean(salt), salt_separator, rounds, mem_cost)?;
173    let signer_key = base64::decode(signer_key)?;
174
175    let result = encrypt(signer_key.as_slice(), derived_key[..32].try_into().unwrap())?;
176    Ok(base64::decode(base64::encode(result))?)
177}
178
179#[cfg(test)]
180mod tests {
181    const SALT_SEPARATOR: &str = "Bw==";
182    const SIGNER_KEY: &str =
183        "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==";
184    const ROUNDS: u32 = 8;
185    const MEM_COST: u32 = 14;
186
187    const PASSWORD: &str = "user1password";
188    const SALT: &str = "42xEC+ixf3L2lw==";
189    const PASSWORD_HASH: &str =
190        "lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ==";
191
192    use super::*;
193
194    #[test]
195    fn verify_password_works() {
196        assert!(verify_password(
197            PASSWORD,
198            PASSWORD_HASH,
199            SALT,
200            SALT_SEPARATOR,
201            SIGNER_KEY,
202            ROUNDS,
203            MEM_COST
204        )
205        .unwrap())
206    }
207
208    #[test]
209    fn generate_hash_works() {
210        assert_eq!(
211            base64::encode(
212                generate_raw_hash(PASSWORD, SALT, SALT_SEPARATOR, SIGNER_KEY, ROUNDS, MEM_COST,)
213                    .unwrap()
214            ),
215            PASSWORD_HASH
216        )
217    }
218
219    #[test]
220    fn encrypt_works() {
221        let param_1 = b"randomrandomrandomrandomrandomrandomrandom";
222        let param_2 = b"12345678901234567890123456789012";
223
224        assert_eq!(
225            hex::encode(encrypt(param_1, *param_2).unwrap()),
226            "09f509fa3d09cde568f80709416681e4ed5d9677ca8b4807a932869ba3fd057be3606c2940877850ed96"
227        );
228    }
229
230    #[test]
231    fn generate_derived_key_works() {
232        assert_eq!(hex::encode(generate_derived_key(PASSWORD, SALT, SALT_SEPARATOR, ROUNDS, MEM_COST).unwrap()), "e87fa22d9b4e3be6bbd41214f2f98f8c78b694bd17e12c2b73501054a2099ce11fe896483c68a443c6cf9ff8a8dfe1dfe2adaa4be6c8ca1b7686687a26f48831");
233    }
234}