bcrypt_pbkdf/
lib.rs

1//! This crate implements [bcrypt_pbkdf], a custom derivative of PBKDF2 used in
2//! [OpenSSH].
3//!
4//! [bcrypt_pbkdf]: https://flak.tedunangst.com/post/bcrypt-pbkdf
5//! [OpenSSH]: https://flak.tedunangst.com/post/new-openssh-key-format-and-bcrypt-pbkdf
6
7#![no_std]
8#![doc = include_str!("../README.md")]
9#![doc(
10    html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg",
11    html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg"
12)]
13
14#[cfg(feature = "alloc")]
15extern crate alloc;
16
17mod errors;
18
19pub use errors::Error;
20
21use blowfish::Blowfish;
22use sha2::{
23    Digest, Sha512,
24    digest::{
25        FixedOutput, MacMarker, Output, OutputSizeUser, Update,
26        crypto_common::{Key, KeyInit, KeySizeUser},
27        typenum::U32,
28    },
29};
30
31#[cfg(feature = "zeroize")]
32use zeroize::Zeroize;
33
34const BHASH_WORDS: usize = 8;
35const BHASH_OUTPUT_SIZE: usize = BHASH_WORDS * 4;
36const BHASH_SEED: &[u8; BHASH_OUTPUT_SIZE] = b"OxychromaticBlowfishSwatDynamite";
37
38/// The bcrypt_pbkdf function.
39///
40/// # Arguments
41/// - `passphrase` - The passphrase to process.
42/// - `salt` - The salt value to use as a byte vector.
43/// - `rounds` - The number of rounds to apply.
44/// - `output` - The resulting derived key is returned in this byte vector.
45///
46/// # Returns
47/// - `Ok(())` if everything is fine.
48/// - `Err(Error::InvalidParamLen)` if `passphrase.is_empty() || salt.is_empty()`.
49/// - `Err(Error::InvalidRounds)` if `rounds == 0`.
50/// - `Err(Error::InvalidOutputLen)` if `output.is_empty() || output.len() > 1024`.
51#[cfg(feature = "alloc")]
52pub fn bcrypt_pbkdf(
53    passphrase: impl AsRef<[u8]>,
54    salt: &[u8],
55    rounds: u32,
56    output: &mut [u8],
57) -> Result<(), Error> {
58    /// number of strides which will be processed on stack
59    const STACK_STRIDE: usize = 8;
60
61    // Allocate a Vec large enough to hold the output we require.
62    let stride = output.len().div_ceil(BHASH_OUTPUT_SIZE);
63
64    let mut vec_buf;
65    let mut stack_buf = [0u8; STACK_STRIDE * BHASH_OUTPUT_SIZE];
66    let generated = if stride > STACK_STRIDE {
67        vec_buf = alloc::vec![0u8; stride * BHASH_OUTPUT_SIZE];
68        &mut vec_buf[..]
69    } else {
70        &mut stack_buf[..stride * BHASH_OUTPUT_SIZE]
71    };
72
73    bcrypt_pbkdf_with_memory(passphrase, salt, rounds, output, generated)
74}
75
76/// Like [`bcrypt_pbkdf`], but usable on "heapless" targets.
77///
78/// # Arguments
79/// - `passphrase` - The passphrase to process.
80/// - `salt` - The salt value to use as a byte vector.
81/// - `rounds` - The number of rounds to apply.
82/// - `output` - The resulting derived key is returned in this byte vector.
83/// - `memory` - Buffer space used for internal computation.
84///
85/// # Returns
86/// - `Ok(())` if everything is fine.
87/// - `Err(Error::InvalidParamLen)` if `passphrase.is_empty() || salt.is_empty()`.
88/// - `Err(Error::InvalidRounds)` if `rounds == 0`.
89/// - `Err(Error::InvalidOutputLen)` if `output.is_empty() || output.len() > 1024`.
90/// - `Err(Error::InvalidMemoryLen)` if `memory.len() < (output.len() + 32 - 1) / 32 * 32`, i.e.
91///   `output.len()` rounded up to the nearest multiple of 32.
92pub fn bcrypt_pbkdf_with_memory(
93    passphrase: impl AsRef<[u8]>,
94    salt: &[u8],
95    rounds: u32,
96    output: &mut [u8],
97    memory: &mut [u8],
98) -> Result<(), Error> {
99    let stride = output.len().div_ceil(BHASH_OUTPUT_SIZE);
100
101    // Validate inputs in same way as OpenSSH implementation
102    let passphrase = passphrase.as_ref();
103    if passphrase.is_empty() || salt.is_empty() {
104        return Err(errors::Error::InvalidParamLen);
105    } else if rounds == 0 {
106        return Err(errors::Error::InvalidRounds);
107    } else if output.is_empty() || output.len() > BHASH_OUTPUT_SIZE * BHASH_OUTPUT_SIZE {
108        return Err(errors::Error::InvalidOutputLen);
109    } else if memory.len() < stride * BHASH_OUTPUT_SIZE {
110        return Err(errors::Error::InvalidMemoryLen);
111    }
112
113    // Run the regular PBKDF2 algorithm with bhash as the PRF.
114    pbkdf2::pbkdf2::<Bhash>(&Sha512::digest(passphrase), salt, rounds, memory)
115        .expect("Bhash can be initialized with any key length");
116
117    // Apply the bcrypt_pbkdf non-linear transformation on the output.
118    for (i, out_byte) in output.iter_mut().enumerate() {
119        let chunk_num = i % stride;
120        let chunk_index = i / stride;
121        *out_byte = memory[chunk_num * BHASH_OUTPUT_SIZE + chunk_index];
122    }
123
124    Ok(())
125}
126
127fn bhash(sha2_pass: &Output<Sha512>, sha2_salt: &Output<Sha512>) -> Output<Bhash> {
128    let mut blowfish = Blowfish::bc_init_state();
129
130    blowfish.salted_expand_key(sha2_salt, sha2_pass);
131    for _ in 0..64 {
132        blowfish.bc_expand_key(sha2_salt);
133        blowfish.bc_expand_key(sha2_pass);
134    }
135
136    let mut cdata = [0u32; BHASH_WORDS];
137    for i in 0..BHASH_WORDS {
138        cdata[i] = u32::from_be_bytes(BHASH_SEED[i * 4..(i + 1) * 4].try_into().unwrap());
139    }
140
141    for _ in 0..64 {
142        for i in (0..BHASH_WORDS).step_by(2) {
143            let [l, r] = blowfish.bc_encrypt([cdata[i], cdata[i + 1]]);
144            cdata[i] = l;
145            cdata[i + 1] = r;
146        }
147    }
148
149    let mut output = Output::<Bhash>::default();
150    for i in 0..BHASH_WORDS {
151        output[i * 4..(i + 1) * 4].copy_from_slice(&cdata[i].to_le_bytes());
152    }
153
154    output
155}
156
157#[derive(Clone)]
158struct Bhash {
159    sha2_pass: Output<Sha512>,
160    salt: Sha512,
161}
162
163impl MacMarker for Bhash {}
164
165impl KeySizeUser for Bhash {
166    type KeySize = <Sha512 as OutputSizeUser>::OutputSize;
167}
168
169impl KeyInit for Bhash {
170    fn new(key: &Key<Self>) -> Self {
171        Bhash {
172            sha2_pass: *key,
173            salt: Sha512::default(),
174        }
175    }
176}
177
178impl Update for Bhash {
179    fn update(&mut self, data: &[u8]) {
180        Update::update(&mut self.salt, data);
181    }
182}
183
184impl OutputSizeUser for Bhash {
185    type OutputSize = U32;
186}
187
188impl FixedOutput for Bhash {
189    fn finalize_into(mut self, out: &mut Output<Self>) {
190        *out = bhash(&self.sha2_pass, &self.salt.finalize_reset());
191    }
192}
193
194#[cfg(feature = "zeroize")]
195impl Drop for Bhash {
196    fn drop(&mut self) {
197        self.sha2_pass.zeroize();
198    }
199}
200
201#[cfg(test)]
202mod test {
203    use super::bhash;
204    use hex_literal::hex;
205    use sha2::digest::array::Array;
206
207    #[test]
208    fn test_bhash() {
209        struct Test {
210            hpass: [u8; 64],
211            hsalt: [u8; 64],
212            out: [u8; 32],
213        }
214
215        const TEST_VAL: [u8; 64] = hex!(
216            "000102030405060708090a0b0c0d0e0f"
217            "101112131415161718191a1b1c1d1e1f"
218            "202122232425262728292a2b2c2d2e2f"
219            "303132333435363738393a3b3c3d3e3f"
220        );
221
222        let tests = [
223            Test {
224                hpass: [0; 64],
225                hsalt: [0; 64],
226                out: hex!(
227                    "460286e972fa833f8b1283ad8fa919fa"
228                    "29bde20e23329e774d8422bac0a7926c"
229                ),
230            },
231            Test {
232                hpass: TEST_VAL,
233                hsalt: [0; 64],
234                out: hex!(
235                    "b0b229dbc6badef0e1da2527474a8b28"
236                    "888f8b061476fe80c32256e1142dd00d"
237                ),
238            },
239            Test {
240                hpass: [0; 64],
241                hsalt: TEST_VAL,
242                out: hex!(
243                    "b62b4e367d3157f5c31e4d2cbafb2931"
244                    "494d9d3bdd171d55cf799fa4416042e2"
245                ),
246            },
247            Test {
248                hpass: TEST_VAL,
249                hsalt: TEST_VAL,
250                out: hex!(
251                    "c6a95fe6413115fb57e99f757498e85d"
252                    "a3c6e1df0c3c93aa975c548a344326f8"
253                ),
254            },
255        ];
256
257        for t in tests.iter() {
258            let hpass = Array(t.hpass);
259            let hsalt = Array(t.hsalt);
260            let out = bhash(&hpass, &hsalt);
261            assert_eq!(out[..], t.out[..]);
262        }
263    }
264}