Skip to main content

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