Skip to content

Commit

Permalink
Make alloc optional (#79)
Browse files Browse the repository at this point in the history
* Make alloc optional

This puts all uses of alloc behind cfg, allowing to run on no_std
platforms without alloc. It also sets default-features=false for
getrandom and zeroize dependencies, getrandom's `std` or `js` are already
set by features.

getrandom is made optional since it won't be available on some no_std
platforms. It's enabled automatically by `alloc`.

* Add CI test for building against bare metal

This catches attempting to link to alloc or getrandom
which won't work on bare metal platforms.

* Add unit test for raw bcrypt()
  • Loading branch information
mkj authored Jul 5, 2023
1 parent 37053f2 commit 63d190d
Show file tree
Hide file tree
Showing 12 changed files with 182 additions and 6 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ jobs:
if: matrix.test_no_std == true
run: cargo test --no-default-features --features alloc

bare-metal:
# tests no alloc, no_std, no getrandom
name: bare-metal
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: thumbv6m-none-eabi
profile: minimal
override: true

- name: Build
run: cd examples/bare-metal; cargo build

fmt:
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ include = ["src/**/*", "LICENSE", "README.md"]
[features]
default = ["std", "zeroize"]
std = ["getrandom/std", "base64/std"]
alloc = ["base64/alloc"]
alloc = ["base64/alloc", "getrandom"]
js = ["getrandom/js"]

[dependencies]
blowfish = { version = "0.9", features = ["bcrypt"] }
getrandom = "0.2"
getrandom = { version = "0.2", default-features = false, optional = true }
base64 = { version = "0.21", default-features = false }
zeroize = { version = "1.5.4", optional = true }
subtle = "2.4.1"
subtle = { version = "2.4.1", default-features = false }

[dev-dependencies]
# no default features avoid pulling in log
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ let valid = verify("hunter2", &hashed)?;

The cost needs to be an integer between 4 and 31 (see benchmarks to have an idea of the speed for each), the `DEFAULT_COST` is 12.

## `no_std`

`bcrypt` crate supports `no_std` platforms. When `alloc` feature is enabled,
all crate functionality is available. When `alloc` is not enabled only the
raw `bcrypt()` function is usable.

## Benchmarks
Speed depends on the cost used: the highest the slowest.
Here are some benchmarks on a 2019 Macbook Pro to give you some ideas on the cost/speed ratio.
Expand Down
3 changes: 3 additions & 0 deletions examples/bare-metal/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build]
# Cortex-M0 and Cortex-M0+
target = "thumbv6m-none-eabi"
10 changes: 10 additions & 0 deletions examples/bare-metal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
edition = "2021"
name = "bcrypt-bare-metal-test"
version = "0.1.0"

[dependencies]
bcrypt = { path = "../../", default-features = false }

cortex-m-rt = "0.6.10"
panic-halt = "0.2.0"
9 changes: 9 additions & 0 deletions examples/bare-metal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Bare metal build test
==

Use by CI to ensure that `bcrypt` with `no-default-features` can work
in a bare metal program. The platform doesn't have any of
`std`, `alloc`, `getrandom`.

Based on https://github.com/rust-embedded/cortex-m-quickstart
under a MIT license.
43 changes: 43 additions & 0 deletions examples/bare-metal/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.
//!
//! The build script also sets the linker flags to tell it which link script to use.

use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

fn main() {
// Put `memory.x` in our output directory and ensure it's
// on the linker search path.
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());

// By default, Cargo will re-run a build script whenever
// any file in the project changes. By specifying `memory.x`
// here, we ensure the build script is only re-run when
// `memory.x` is changed.
println!("cargo:rerun-if-changed=memory.x");

// Specify linker arguments.

// `--nmagic` is required if memory section addresses are not aligned to 0x10000,
// for example the FLASH and RAM sections in your `memory.x`.
// See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
println!("cargo:rustc-link-arg=--nmagic");

// Set the linker script to the one provided by cortex-m-rt.
println!("cargo:rustc-link-arg=-Tlink.x");
}
6 changes: 6 additions & 0 deletions examples/bare-metal/memory.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
MEMORY
{
/* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
12 changes: 12 additions & 0 deletions examples/bare-metal/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#![no_std]
#![no_main]

use panic_halt as _;
use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
let salt = [1u8; 16];
let _crypt = bcrypt::bcrypt(6, salt, b"password");
loop {}
}
50 changes: 50 additions & 0 deletions src/bcrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,53 @@ pub fn bcrypt(cost: u32, salt: [u8; 16], password: &[u8]) -> [u8; 24] {

output
}

#[cfg(test)]
mod tests {
use super::bcrypt;

#[test]
fn raw_bcrypt() {
// test vectors unbase64ed from
// https://github.com/djmdjm/jBCrypt/blob/master/test/org/mindrot/jbcrypt/TestBCrypt.java

// $2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.
let pw = b"\0";
let cost = 6;
let salt = [
0x14, 0x4b, 0x3d, 0x69, 0x1a, 0x7b, 0x4e, 0xcf, 0x39, 0xcf, 0x73, 0x5c, 0x7f, 0xa7,
0xa7, 0x9c,
];
let result = [
0x55, 0x7e, 0x94, 0xf3, 0x4b, 0xf2, 0x86, 0xe8, 0x71, 0x9a, 0x26, 0xbe, 0x94, 0xac,
0x1e, 0x16, 0xd9, 0x5e, 0xf9, 0xf8, 0x19, 0xde, 0xe0,
];
assert_eq!(bcrypt(cost, salt, pw)[..23], result);

// $2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe
let pw = b"a\0";
let cost = 6;
let salt = [
0xa3, 0x61, 0x2d, 0x8c, 0x9a, 0x37, 0xda, 0xc2, 0xf9, 0x9d, 0x94, 0xda, 0x3, 0xbd,
0x45, 0x21,
];
let result = [
0xe6, 0xd5, 0x38, 0x31, 0xf8, 0x20, 0x60, 0xdc, 0x8, 0xa2, 0xe8, 0x48, 0x9c, 0xe8,
0x50, 0xce, 0x48, 0xfb, 0xf9, 0x76, 0x97, 0x87, 0x38,
];
assert_eq!(bcrypt(cost, salt, pw)[..23], result);

// // $2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz.
let pw = b"abcdefghijklmnopqrstuvwxyz\0";
let cost = 8;
let salt = [
0x71, 0x5b, 0x96, 0xca, 0xed, 0x2a, 0xc9, 0x2c, 0x35, 0x4e, 0xd1, 0x6c, 0x1e, 0x19,
0xe3, 0x8a,
];
let result = [
0x98, 0xbf, 0x9f, 0xfc, 0x1f, 0x5b, 0xe4, 0x85, 0xf9, 0x59, 0xe8, 0xb1, 0xd5, 0x26,
0x39, 0x2f, 0xbd, 0x4e, 0xd2, 0xd5, 0x71, 0x9f, 0x50,
];
assert_eq!(bcrypt(cost, salt, pw)[..23], result);
}
}
10 changes: 10 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[cfg(any(feature = "alloc", feature = "std"))]
use alloc::string::String;
use core::fmt;

Expand All @@ -16,11 +17,15 @@ pub enum BcryptError {
#[cfg(feature = "std")]
Io(io::Error),
CostNotAllowed(u32),
#[cfg(any(feature = "alloc", feature = "std"))]
InvalidCost(String),
#[cfg(any(feature = "alloc", feature = "std"))]
InvalidPrefix(String),
#[cfg(any(feature = "alloc", feature = "std"))]
InvalidHash(String),
InvalidSaltLen(usize),
InvalidBase64(base64::DecodeError),
#[cfg(any(feature = "alloc", feature = "std"))]
Rand(getrandom::Error),
}

Expand All @@ -37,13 +42,15 @@ macro_rules! impl_from_error {
impl_from_error!(base64::DecodeError, BcryptError::InvalidBase64);
#[cfg(feature = "std")]
impl_from_error!(io::Error, BcryptError::Io);
#[cfg(any(feature = "alloc", feature = "std"))]
impl_from_error!(getrandom::Error, BcryptError::Rand);

impl fmt::Display for BcryptError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
#[cfg(feature = "std")]
BcryptError::Io(ref err) => write!(f, "IO error: {}", err),
#[cfg(any(feature = "alloc", feature = "std"))]
BcryptError::InvalidCost(ref cost) => write!(f, "Invalid Cost: {}", cost),
BcryptError::CostNotAllowed(ref cost) => write!(
f,
Expand All @@ -52,12 +59,15 @@ impl fmt::Display for BcryptError {
crate::MAX_COST,
cost
),
#[cfg(any(feature = "alloc", feature = "std"))]
BcryptError::InvalidPrefix(ref prefix) => write!(f, "Invalid Prefix: {}", prefix),
#[cfg(any(feature = "alloc", feature = "std"))]
BcryptError::InvalidHash(ref hash) => write!(f, "Invalid hash: {}", hash),
BcryptError::InvalidBase64(ref err) => write!(f, "Base64 error: {}", err),
BcryptError::InvalidSaltLen(len) => {
write!(f, "Invalid salt len: expected 16, received {}", len)
}
#[cfg(any(feature = "alloc", feature = "std"))]
BcryptError::Rand(ref err) => write!(f, "Rand error: {}", err),
}
}
Expand Down
13 changes: 10 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(any(feature = "alloc", feature = "std", test))]
extern crate alloc;

#[cfg(any(feature = "alloc", feature = "std", test))]
use alloc::{
string::{String, ToString},
vec::Vec,
Expand All @@ -12,10 +14,10 @@ use alloc::{
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;

use base64::{alphabet::BCRYPT, engine::general_purpose::NO_PAD, engine::GeneralPurpose, Engine};
use core::{fmt, str::FromStr};
use base64::{alphabet::BCRYPT, engine::general_purpose::NO_PAD, engine::GeneralPurpose};
use core::fmt;
#[cfg(any(feature = "alloc", feature = "std"))]
use {core::convert::AsRef, getrandom::getrandom};
use {base64::Engine, core::convert::AsRef, core::str::FromStr, getrandom::getrandom};

mod bcrypt;
mod errors;
Expand All @@ -29,6 +31,7 @@ const MAX_COST: u32 = 31;
pub const DEFAULT_COST: u32 = 12;
pub const BASE_64: GeneralPurpose = GeneralPurpose::new(&BCRYPT, NO_PAD);

#[cfg(any(feature = "alloc", feature = "std"))]
#[derive(Debug, PartialEq)]
/// A bcrypt hash result before concatenating
pub struct HashParts {
Expand All @@ -46,6 +49,7 @@ pub enum Version {
TwoB,
}

#[cfg(any(feature = "alloc", feature = "std"))]
impl HashParts {
/// Creates the bcrypt hash string from all its parts
fn format(self) -> String {
Expand All @@ -69,6 +73,7 @@ impl HashParts {
}
}

#[cfg(any(feature = "alloc", feature = "std"))]
impl FromStr for HashParts {
type Err = BcryptError;

Expand All @@ -77,6 +82,7 @@ impl FromStr for HashParts {
}
}

#[cfg(any(feature = "alloc", feature = "std"))]
impl ToString for HashParts {
fn to_string(&self) -> String {
self.format_for_version(Version::TwoY)
Expand Down Expand Up @@ -125,6 +131,7 @@ fn _hash_password(password: &[u8], cost: u32, salt: [u8; 16]) -> BcryptResult<Ha

/// Takes a full hash and split it into 3 parts:
/// cost, salt and hash
#[cfg(any(feature = "alloc", feature = "std"))]
fn split_hash(hash: &str) -> BcryptResult<HashParts> {
let mut parts = HashParts {
cost: 0,
Expand Down

0 comments on commit 63d190d

Please sign in to comment.