Skip to content

Commit 9af7daa

Browse files
committed
feat: improve ergonomics and add hash generation functions
1 parent 88432d9 commit 9af7daa

File tree

3 files changed

+184
-25
lines changed

3 files changed

+184
-25
lines changed

src/errors.rs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
use std::num::TryFromIntError;
21
use base64::DecodeError;
32
use ctr::cipher::StreamCipherError;
43
use scrypt::errors::{InvalidOutputLen, InvalidParams};
54

65
#[derive(Clone, Debug, PartialEq, Eq)]
76
pub(crate) enum DerivedKeyError {
87
Base64Decode(DecodeError),
9-
IntError(TryFromIntError),
108
InvalidScryptParams(InvalidParams),
119
InvalidOutputLen(InvalidOutputLen),
1210
}
@@ -17,12 +15,6 @@ impl From<DecodeError> for DerivedKeyError {
1715
}
1816
}
1917

20-
impl From<TryFromIntError> for DerivedKeyError {
21-
fn from(e: TryFromIntError) -> Self {
22-
Self::IntError(e)
23-
}
24-
}
25-
2618
impl From<InvalidParams> for DerivedKeyError {
2719
fn from(e: InvalidParams) -> Self {
2820
Self::InvalidScryptParams(e)
@@ -47,25 +39,25 @@ impl From<StreamCipherError> for EncryptError {
4739
}
4840

4941
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
50-
pub enum VerifyPasswordError {
42+
pub enum GenerateHashError {
5143
GenerateDerivedKeyFailed,
5244
DecodingFailed,
5345
EncryptionFailed,
5446
}
5547

56-
impl From<DecodeError> for VerifyPasswordError {
48+
impl From<DecodeError> for GenerateHashError {
5749
fn from(_: DecodeError) -> Self {
5850
Self::DecodingFailed
5951
}
6052
}
6153

62-
impl From<EncryptError> for VerifyPasswordError {
54+
impl From<EncryptError> for GenerateHashError {
6355
fn from(_: EncryptError) -> Self {
6456
Self::EncryptionFailed
6557
}
6658
}
6759

68-
impl From<DerivedKeyError> for VerifyPasswordError {
60+
impl From<DerivedKeyError> for GenerateHashError {
6961
fn from(_: DerivedKeyError) -> Self {
7062
Self::GenerateDerivedKeyFailed
7163
}

src/lib.rs

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Implementation of [Firebase Scrypt](https://github.com/firebase/scrypt) in pure Rust.
22
//!
3-
//! If you are only using the [`verify_password`] function instead of the higher-level struct [`FirebaseScrypt`],
3+
//! If you are only using the raw functions instead of the higher-level struct [`FirebaseScrypt`],
44
//! it's recommended to disable default features in your ``Cargo.toml``
55
//!
66
//! ```toml
@@ -31,7 +31,7 @@ use aes::cipher::{KeyIvInit, StreamCipher};
3131
use constant_time_eq::constant_time_eq;
3232
use ctr::{Ctr128BE};
3333
use scrypt::Params;
34-
use crate::errors::{DerivedKeyError, EncryptError, VerifyPasswordError};
34+
use crate::errors::{DerivedKeyError, EncryptError, GenerateHashError};
3535

3636
pub mod errors;
3737
#[cfg(feature = "simple")]
@@ -119,14 +119,56 @@ pub fn verify_password(
119119
signer_key: &str,
120120
rounds: u32,
121121
mem_cost: u32,
122-
) -> Result<bool, VerifyPasswordError> {
122+
) -> Result<bool, GenerateHashError> {
123+
let password_hash = generate_raw_hash(password, salt, salt_separator, signer_key, rounds, mem_cost)?;
124+
125+
Ok(constant_time_eq(password_hash.as_slice(), base64::decode(known_hash)?.as_slice()))
126+
}
127+
128+
/// Generates a hash in the form of a [`Vec<u8>`]
129+
///
130+
/// In case you want or are using the same hash representation as Firebase, use the [`FirebaseScrypt`]
131+
/// struct to get the Base64 hashed directly.
132+
///
133+
/// # Example (generate Base64 hash)
134+
/// ```
135+
/// // Base64 crate for encoding the hash
136+
/// use base64::encode;
137+
/// use firebase_scrypt::generate_raw_hash;
138+
///
139+
/// const SALT_SEPARATOR: &str = "Bw==";
140+
/// const SIGNER_KEY: &str = "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==";
141+
/// const ROUNDS: u32 = 8;
142+
/// const MEM_COST: u32 = 14;
143+
///
144+
/// let password = "user1password";
145+
/// let salt = "42xEC+ixf3L2lw==";
146+
/// let password_hash ="lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ==";
147+
///
148+
/// let hash = encode(generate_raw_hash(
149+
/// password,
150+
/// salt,
151+
/// SALT_SEPARATOR,
152+
/// SIGNER_KEY,
153+
/// ROUNDS,
154+
/// MEM_COST,
155+
/// ).unwrap());
156+
///
157+
/// assert_eq!(hash, password_hash);
158+
/// ```
159+
pub fn generate_raw_hash(
160+
password: &str,
161+
salt: &str,
162+
salt_separator: &str,
163+
signer_key: &str,
164+
rounds: u32,
165+
mem_cost: u32,
166+
) -> Result<Vec<u8>, GenerateHashError> {
123167
let derived_key = generate_derived_key(password, salt, salt_separator, rounds, mem_cost)?;
124168
let signer_key = base64::decode(signer_key)?;
125169

126170
let result = encrypt(signer_key.as_slice(), derived_key[..32].try_into().unwrap())?;
127-
let password_hash = base64::decode(base64::encode(result))?;
128-
129-
Ok(constant_time_eq(password_hash.as_slice(), base64::decode(known_hash)?.as_slice()))
171+
Ok(base64::decode(base64::encode(result))?)
130172
}
131173

132174
#[cfg(test)]
@@ -141,6 +183,7 @@ mod tests {
141183
const PASSWORD_HASH: &str ="lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ==";
142184

143185
use super::*;
186+
144187
#[test]
145188
fn verify_password_works() {
146189
assert!(verify_password(
@@ -154,6 +197,18 @@ mod tests {
154197
).unwrap())
155198
}
156199

200+
#[test]
201+
fn generate_hash_works() {
202+
assert_eq!(base64::encode(generate_raw_hash(
203+
PASSWORD,
204+
SALT,
205+
SALT_SEPARATOR,
206+
SIGNER_KEY,
207+
ROUNDS,
208+
MEM_COST,
209+
).unwrap()), PASSWORD_HASH)
210+
}
211+
157212
#[test]
158213
fn encrypt_works() {
159214
let param_1 = b"randomrandomrandomrandomrandomrandomrandom";

src/simple.rs

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use crate::{verify_password, VerifyPasswordError};
1+
use crate::{verify_password, GenerateHashError, generate_raw_hash};
22

3-
/// Struct to simplify the usage of the [`verify_password`] function.
3+
/// Struct to simplify the usage of hash generation and checking.
44
///
5-
/// Holds the salt separator, signer key, round and memory cost to make the usage of the [`verify_password`]
6-
/// function easier.
5+
/// Holds the salt separator, signer key, round and memory cost to make the usage of the hash generation
6+
/// and checking function easier.
77
///
88
/// # Example
99
/// ```
@@ -31,6 +31,7 @@ pub struct FirebaseScrypt {
3131
}
3232

3333
impl FirebaseScrypt {
34+
/// Create a new [`FirebaseScrypt`] instance.
3435
pub fn new(salt_separator: &str, signer_key: &str, rounds: u32, mem_cost: u32) -> Self {
3536
Self {
3637
salt_separator: salt_separator.to_string(),
@@ -41,15 +42,126 @@ impl FirebaseScrypt {
4142
}
4243

4344
/// Calls [`verify_password`] with the data from the [`FirebaseScrypt`]
44-
pub fn verify_password(&self, password: &str, salt: &str, known_hash: &str) -> Result<bool, VerifyPasswordError> {
45-
Ok(verify_password(
45+
///
46+
/// # Example
47+
/// ```no_test
48+
/// # This test doesn't pass for (some?) reason. But the ``verify_password_with_simple_works`` test
49+
/// # passes, so no idea.
50+
/// use firebase_scrypt::FirebaseScrypt;
51+
///
52+
/// const SALT_SEPARATOR: &str = "Bw==";
53+
/// const SIGNER_KEY: &str = "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==";
54+
/// const ROUNDS: u32 = 8;
55+
/// const MEM_COST: u32 = 14;
56+
///
57+
/// let password: &str = "user1password";
58+
/// let salt: &str = "42xEC+ixf3L2lw==";
59+
/// let password_hash: &str = "lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ==";
60+
///
61+
/// let firebase_scrypt = FirebaseScrypt::new(SALT_SEPARATOR, SIGNER_KEY, ROUNDS, MEM_COST);
62+
///
63+
/// let is_valid = firebase_scrypt.verify_password(
64+
/// password,
65+
/// password_hash,
66+
/// salt,
67+
/// ).unwrap();
68+
///
69+
/// assert!(is_valid)
70+
/// ```
71+
pub fn verify_password(&self, password: &str, salt: &str, known_hash: &str) -> Result<bool, GenerateHashError> {
72+
verify_password(
4673
password,
4774
known_hash,
4875
salt,
4976
self.salt_separator.as_str(),
5077
self.signer_key.as_str(),
5178
self.rounds,
5279
self.mem_cost,
53-
)?)
80+
)
81+
}
82+
83+
/// Calls [`FirebaseScrypt::verify_password`] but returns false also if an error occurs, which
84+
/// is _usually_ the best thing to do.
85+
pub fn verify_password_bool(&self, password: &str, salt: &str, known_hash: &str) -> bool {
86+
if let Ok(result) = self.verify_password(password, salt, known_hash) {
87+
result
88+
} else {
89+
false
90+
}
91+
}
92+
93+
/// Generates a hash and returns its Base64 form, the same as the hashes from Firebase
94+
///
95+
/// <div class="example-wrap" style="display:inline-block"><pre class="compile_fail" style="white-space:normal;font:inherit;">
96+
///
97+
/// **Warning**: Do not use this function to check if a given password is valid, because that
98+
/// could result in [side-channel attacks](https://en.wikipedia.org/wiki/Side-channel_attack).
99+
///
100+
/// Use the [`FirebaseScrypt::verify_password`] function instead.
101+
///
102+
/// </pre></div>
103+
///
104+
/// # Example
105+
/// ```
106+
/// use firebase_scrypt::FirebaseScrypt;
107+
///
108+
/// const SALT_SEPARATOR: &str = "Bw==";
109+
/// const SIGNER_KEY: &str = "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==";
110+
/// const ROUNDS: u32 = 8;
111+
/// const MEM_COST: u32 = 14;
112+
///
113+
/// let password = "user1password";
114+
/// let salt = "42xEC+ixf3L2lw==";
115+
///
116+
/// let firebase_scrypt = FirebaseScrypt::new(SALT_SEPARATOR, SIGNER_KEY, ROUNDS, MEM_COST);
117+
///
118+
/// firebase_scrypt.generate_base64_hash(password, salt).unwrap();
119+
/// ```
120+
pub fn generate_base64_hash(&self, password: &str, salt: &str) -> Result<String, GenerateHashError> {
121+
let hash = generate_raw_hash(
122+
password,
123+
salt,
124+
self.salt_separator.as_str(),
125+
self.signer_key.as_str(),
126+
self.rounds,
127+
self.mem_cost,
128+
)?;
129+
130+
Ok(base64::encode(hash))
131+
}
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
const SALT_SEPARATOR: &str = "Bw==";
137+
const SIGNER_KEY: &str = "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==";
138+
const ROUNDS: u32 = 8;
139+
const MEM_COST: u32 = 14;
140+
141+
const PASSWORD: &str = "user1password";
142+
const SALT: &str = "42xEC+ixf3L2lw==";
143+
const PASSWORD_HASH: &str ="lSrfV15cpx95/sZS2W9c9Kp6i/LVgQNDNC/qzrCnh1SAyZvqmZqAjTdn3aoItz+VHjoZilo78198JAdRuid5lQ==";
144+
145+
use super::*;
146+
147+
#[test]
148+
fn verify_password_with_simple_works() {
149+
let firebase_scrypt = FirebaseScrypt::new(SALT_SEPARATOR, SIGNER_KEY, ROUNDS, MEM_COST);
150+
151+
assert!(firebase_scrypt.verify_password(
152+
PASSWORD,
153+
SALT,
154+
PASSWORD_HASH,
155+
).unwrap())
156+
}
157+
158+
#[test]
159+
fn generate_hash_with_simple_works() {
160+
let firebase_scrypt = FirebaseScrypt::new(SALT_SEPARATOR, SIGNER_KEY, ROUNDS, MEM_COST);
161+
162+
assert_eq!(firebase_scrypt.generate_base64_hash(
163+
PASSWORD,
164+
SALT,
165+
).unwrap(), PASSWORD_HASH)
54166
}
55167
}

0 commit comments

Comments
 (0)