From 666f12227571077e561a9e42fe5daeeebbeacd58 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 5 May 2020 23:13:36 +0800 Subject: [PATCH 1/2] feat(bcrypt): fastest bcrypt --- .github/workflows/ci.yaml | 9 +- Cargo.toml | 1 + packages/bcrypt/Cargo.toml | 26 ++ packages/bcrypt/README.md | 63 ++++ packages/bcrypt/__tests__/bcrypt.spec.ts | 17 + packages/bcrypt/benchmark/bcrypt.js | 104 ++++++ packages/bcrypt/build.rs | 5 + packages/bcrypt/index.d.ts | 6 + packages/bcrypt/index.js | 35 ++ packages/bcrypt/package.json | 50 +++ packages/bcrypt/src/b64.rs | 207 ++++++++++++ packages/bcrypt/src/bcrypt.rs | 37 ++ packages/bcrypt/src/errors.rs | 75 +++++ packages/bcrypt/src/hash_task.rs | 32 ++ packages/bcrypt/src/lib.rs | 103 ++++++ packages/bcrypt/src/lib_bcrypt.rs | 408 +++++++++++++++++++++++ packages/bcrypt/src/verify_task.rs | 37 ++ scripts/mv-artifacts.js | 3 +- scripts/packages.js | 8 + yarn.lock | 15 +- 20 files changed, 1237 insertions(+), 4 deletions(-) create mode 100644 packages/bcrypt/Cargo.toml create mode 100644 packages/bcrypt/README.md create mode 100644 packages/bcrypt/__tests__/bcrypt.spec.ts create mode 100644 packages/bcrypt/benchmark/bcrypt.js create mode 100644 packages/bcrypt/build.rs create mode 100644 packages/bcrypt/index.d.ts create mode 100644 packages/bcrypt/index.js create mode 100644 packages/bcrypt/package.json create mode 100644 packages/bcrypt/src/b64.rs create mode 100644 packages/bcrypt/src/bcrypt.rs create mode 100644 packages/bcrypt/src/errors.rs create mode 100644 packages/bcrypt/src/hash_task.rs create mode 100644 packages/bcrypt/src/lib.rs create mode 100644 packages/bcrypt/src/lib_bcrypt.rs create mode 100644 packages/bcrypt/src/verify_task.rs create mode 100644 scripts/packages.js diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c46f0325..9bf5fe97 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -85,10 +85,17 @@ jobs: run: yarn lint if: matrix.os == 'ubuntu-latest' - - name: typecheck + - name: TypeCheck run: yarn typecheck if: matrix.os == 'ubuntu-latest' + - name: Cargo test + uses: actions-rs/cargo@v1 + timeout-minutes: 5 + with: + command: test + args: -p nodr-rs-bcrypt --lib -- --nocapture + - name: Run build run: | cargo build --release diff --git a/Cargo.toml b/Cargo.toml index b209f272..5e69e2d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "./packages/bcrypt", "./packages/crc32", "./packages/jieba" ] diff --git a/packages/bcrypt/Cargo.toml b/packages/bcrypt/Cargo.toml new file mode 100644 index 00000000..50763076 --- /dev/null +++ b/packages/bcrypt/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "nodr-rs-bcrypt" +version = "0.1.0" +authors = ["LongYinan "] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +radix64 = "0.6" +blowfish = { version = "0.4", features = ["bcrypt"] } +byteorder = "1" +napi-rs = { version = "0.3" } +napi-rs-derive = { version = "0.2" } +rand = "0.7" +phf = { version = "0.8", features = ["macros"] } + +[target.'cfg(unix)'.dependencies] +jemallocator = { version = "0.3", features = ["disable_initial_exec_tls"] } + +[dev-dependencies] +quickcheck = "0.9" + +[build-dependencies] +napi-build = { version = "0.1" } diff --git a/packages/bcrypt/README.md b/packages/bcrypt/README.md new file mode 100644 index 00000000..3a05e4ee --- /dev/null +++ b/packages/bcrypt/README.md @@ -0,0 +1,63 @@ +# `@node-rs/bcrypt` + +![](https://github.com/Brooooooklyn/node-rs/workflows/CI/badge.svg) + +🚀 Fastest bcrypt in NodeJS + +## Support matrix + +| | node 10 | node12 | node13 | node14 | +| ----------------- | ------- | ------ | ------ | ------ | +| Windows 64 latest | ✓ | ✓ | ✓ | ✓ | +| macOS latest | ✓ | ✓ | ✓ | ✓ | +| Linux | ✓ | ✓ | ✓ | ✓ | + +## Usage + +```typescript +export const DEFAULT_ROUND = 12 + +function hashSync(password: string | Buffer, round?: number): string +function hash(password: string | Buffer, round?: number): Promise +function verifySync(password: string | Buffer, hash: string | Buffer): boolean +function verify(password: string | Buffer, hash: string | Buffer): Promise +``` + +## Bench + +``` +Model Name: MacBook Pro +Model Identifier: MacBookPro15,1 +Processor Name: Intel Core i7 +Processor Speed: 2.6 GHz +Number of Processors: 1 +Total Number of Cores: 6 +L2 Cache (per Core): 256 KB +L3 Cache: 12 MB +Hyper-Threading Technology: Enabled +Memory: 16 GB +``` + +
+  @node-rs/bcrypt x 72.11 ops/sec ±1.43% (33 runs sampled)
+  node bcrypt x 62.75 ops/sec ±2.95% (30 runs sampled)
+  Async hash round 10 bench suite: Fastest is @node-rs/bcrypt
+  @node-rs/bcrypt x 18.49 ops/sec ±1.04% (12 runs sampled)
+  node bcrypt x 16.67 ops/sec ±2.05% (11 runs sampled)
+  Async hash round 12 bench suite: Fastest is @node-rs/bcrypt
+  @node-rs/bcrypt x 3.99 ops/sec ±3.17% (6 runs sampled)
+  node bcrypt x 3.13 ops/sec ±1.92% (6 runs sampled)
+  Async hash round 14 bench suite: Fastest is @node-rs/bcrypt
+  @node-rs/bcrypt x 14.32 ops/sec ±0.55% (10 runs sampled)
+  node bcrypt x 13.55 ops/sec ±2.83% (10 runs sampled)
+  Async verify bench suite: Fastest is @node-rs/bcrypt
+  @node-rs/bcrypt x 15.98 ops/sec ±1.12% (44 runs sampled)
+  node bcrypt x 14.55 ops/sec ±1.30% (40 runs sampled)
+  Hash round 10 bench suite: Fastest is @node-rs/bcrypt
+  @node-rs/bcrypt x 4.65 ops/sec ±3.60% (16 runs sampled)
+  node bcrypt x 4.26 ops/sec ±1.90% (15 runs sampled)
+  Hash round 12 bench suite: Fastest is @node-rs/bcrypt
+  @node-rs/bcrypt x 1.16 ops/sec ±2.65% (7 runs sampled)
+  node bcrypt x 1.04 ops/sec ±2.95% (7 runs sampled)
+  Hash round 14 bench suite: Fastest is @node-rs/bcrypt
+
diff --git a/packages/bcrypt/__tests__/bcrypt.spec.ts b/packages/bcrypt/__tests__/bcrypt.spec.ts new file mode 100644 index 00000000..377936bc --- /dev/null +++ b/packages/bcrypt/__tests__/bcrypt.spec.ts @@ -0,0 +1,17 @@ +import test from 'ava' +import { verifySync, hash } from '../index' + +const { hashSync } = require('bcrypt') + +const fx = Buffer.from('bcrypt-test-password') + +const hashedPassword = hashSync(fx) + +test('verifySync hashed password from bcrypt should be true', (t) => { + t.true(verifySync(fx, hashedPassword)) +}) + +test('verifySync hashed password from @node-rs/bcrypt should be true', async (t) => { + const hashed = await hash(fx) + t.true(verifySync(fx, hashed)) +}) diff --git a/packages/bcrypt/benchmark/bcrypt.js b/packages/bcrypt/benchmark/bcrypt.js new file mode 100644 index 00000000..47431f86 --- /dev/null +++ b/packages/bcrypt/benchmark/bcrypt.js @@ -0,0 +1,104 @@ +const { Suite } = require('benchmark') +const { hashSync, hash, compare } = require('bcrypt') +const { cpus } = require('os') +const chalk = require('chalk') +const { range } = require('lodash') + +const { hash: napiHash, hashSync: napiHashSync, verify } = require('../index') + +const hashRounds = [10, 12, 14] +const parallel = cpus().length + +const password = 'node-rust-password' + +function runAsync(round) { + const asyncHashSuite = new Suite(`Async hash round ${round}`) + return new Promise((resolve) => { + asyncHashSuite + .add('@node-rs/bcrypt', { + defer: true, + fn: (deferred) => { + Promise.all(range(parallel).map(() => napiHash(password, round))).then(() => { + deferred.resolve() + }) + }, + }) + .add('node bcrypt', { + defer: true, + fn: (deferred) => { + Promise.all(range(parallel).map(() => hash(password, round))).then(() => { + deferred.resolve() + }) + }, + }) + .on('cycle', function (event) { + event.target.hz = event.target.hz * parallel + console.info(String(event.target)) + }) + .on('complete', function () { + console.info(`${this.name} bench suite: Fastest is ${chalk.green(this.filter('fastest').map('name'))}`) + resolve() + }) + .run({ async: true }) + }) +} + +hashRounds + .reduce(async (acc, cur) => { + await acc + return runAsync(cur) + }, Promise.resolve()) + .then( + () => + new Promise((resolve) => { + const suite = new Suite('Async verify') + const hash = napiHashSync(password) + suite + .add({ + name: '@node-rs/bcrypt', + defer: true, + fn: (deferred) => { + Promise.all(range(parallel).map(() => verify(password, hash))).then(() => { + deferred.resolve() + }) + }, + }) + .add({ + name: 'node bcrypt', + defer: true, + fn: (deferred) => { + Promise.all(range(parallel).map(() => compare(password, hash))).then(() => { + deferred.resolve() + }) + }, + }) + .on('cycle', function (event) { + event.target.hz = event.target.hz * parallel + console.info(String(event.target)) + }) + .on('complete', function () { + resolve() + console.info(`${this.name} bench suite: Fastest is ${chalk.green(this.filter('fastest').map('name'))}`) + }) + .run() + }), + ) + .then(() => { + for (const round of hashRounds) { + const syncHashSuite = new Suite(`Hash round ${round}`) + syncHashSuite + .add('@node-rs/bcrypt', () => { + napiHashSync(password, round) + }) + .add('node bcrypt', () => { + hashSync(password, round) + }) + .on('cycle', function (event) { + console.info(String(event.target)) + }) + .on('complete', function () { + console.info(`${this.name} bench suite: Fastest is ${chalk.green(this.filter('fastest').map('name'))}`) + }) + .run() + } + }) diff --git a/packages/bcrypt/build.rs b/packages/bcrypt/build.rs new file mode 100644 index 00000000..1f866b6a --- /dev/null +++ b/packages/bcrypt/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/packages/bcrypt/index.d.ts b/packages/bcrypt/index.d.ts new file mode 100644 index 00000000..deb0081f --- /dev/null +++ b/packages/bcrypt/index.d.ts @@ -0,0 +1,6 @@ +export const DEFAULT_COST: 12 + +export function hashSync(password: string | Buffer, round?: number): string +export function hash(password: string | Buffer, round?: number): Promise +export function verifySync(password: string | Buffer, hash: string | Buffer): boolean +export function verify(password: string | Buffer, hash: string | Buffer): Promise diff --git a/packages/bcrypt/index.js b/packages/bcrypt/index.js new file mode 100644 index 00000000..e67d9098 --- /dev/null +++ b/packages/bcrypt/index.js @@ -0,0 +1,35 @@ +const { locateBinding } = require('@node-rs/helper') + +const binding = require(locateBinding(__dirname, 'bcrypt')) + +const DEFAULT_COST = 12 + +module.exports = { + DEFAULT_COST: DEFAULT_COST, + + genSalt: function genSalt(round = 10, version = '2b') { + return binding.genSalt(round, version) + }, + + hashSync: function hashSync(password, round = DEFAULT_COST) { + const input = Buffer.isBuffer(password) ? password : Buffer.from(password) + return binding.hashSync(input, round) + }, + + hash: function hash(password, round = DEFAULT_COST) { + const input = Buffer.isBuffer(password) ? password : Buffer.from(password) + return binding.hash(input, round) + }, + + verifySync: function verifySync(password, hash) { + password = Buffer.isBuffer(password) ? password : Buffer.from(password) + hash = Buffer.isBuffer(hash) ? hash : Buffer.from(hash) + return binding.verifySync(password, hash) + }, + + verify: function verify(password, hash) { + password = Buffer.isBuffer(password) ? password : Buffer.from(password) + hash = Buffer.isBuffer(hash) ? hash : Buffer.from(hash) + return binding.verify(password, hash) + }, +} diff --git a/packages/bcrypt/package.json b/packages/bcrypt/package.json new file mode 100644 index 00000000..724e723e --- /dev/null +++ b/packages/bcrypt/package.json @@ -0,0 +1,50 @@ +{ + "name": "@node-rs/bcrypt", + "version": "0.0.0", + "description": "Rust bcrypt binding", + "keywords": [ + "bcrypt", + "auth", + "password", + "authentication", + "encryption", + "crypto", + "N-API", + "napi-rs", + "node-rs" + ], + "author": "LongYinan ", + "homepage": "https://github.com/Brooooooklyn/node-rs", + "license": "MIT", + "main": "index.js", + "typings": "index.d.ts", + "files": [ + "index.js", + "index.d.ts", + "*.node", + "LICENSE", + "COPYING" + ], + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Brooooooklyn/node-rs.git" + }, + "scripts": { + "bench": "cross-env NODE_ENV=production node benchmark/bcrypt.js", + "build": "napi --release ./bcrypt", + "build:debug": "napi ./bcrypt.debug" + }, + "bugs": { + "url": "https://github.com/Brooooooklyn/node-rs/issues" + }, + "dependencies": { + "@node-rs/helper": "^0.1.3" + }, + "devDependencies": { + "bcrypt": "^4.0.1" + } +} diff --git a/packages/bcrypt/src/b64.rs b/packages/bcrypt/src/b64.rs new file mode 100644 index 00000000..bd2e9592 --- /dev/null +++ b/packages/bcrypt/src/b64.rs @@ -0,0 +1,207 @@ +use crate::errors::{BcryptError, BcryptResult}; +use phf::phf_map; +use radix64::STD; + +// Decoding table from bcrypt base64 to standard base64 and standard -> bcrypt +// Bcrypt has its own base64 alphabet +// ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 +static BCRYPT_TO_STANDARD: phf::Map = phf_map! { + '/' => "B", + '.' => "A", + '1' => "3", + '0' => "2", + '3' => "5", + '2' => "4", + '5' => "7", + '4' => "6", + '7' => "9", + '6' => "8", + '9' => "/", + '8' => "+", + 'A' => "C", + 'C' => "E", + 'B' => "D", + 'E' => "G", + 'D' => "F", + 'G' => "I", + 'F' => "H", + 'I' => "K", + 'H' => "J", + 'K' => "M", + 'J' => "L", + 'M' => "O", + 'L' => "N", + 'O' => "Q", + 'N' => "P", + 'Q' => "S", + 'P' => "R", + 'S' => "U", + 'R' => "T", + 'U' => "W", + 'T' => "V", + 'W' => "Y", + 'V' => "X", + 'Y' => "a", + 'X' => "Z", + 'Z' => "b", + 'a' => "c", + 'c' => "e", + 'b' => "d", + 'e' => "g", + 'd' => "f", + 'g' => "i", + 'f' => "h", + 'i' => "k", + 'h' => "j", + 'k' => "m", + 'j' => "l", + 'm' => "o", + 'l' => "n", + 'o' => "q", + 'n' => "p", + 'q' => "s", + 'p' => "r", + 's' => "u", + 'r' => "t", + 'u' => "w", + 't' => "v", + 'w' => "y", + 'v' => "x", + 'y' => "0", + 'x' => "z", + 'z' => "1", +}; + +static STANDARD_TO_BCRYPT: phf::Map = phf_map! { + 'B' => "/", + 'A' => ".", + '3' => "1", + '2' => "0", + '5' => "3", + '4' => "2", + '7' => "5", + '6' => "4", + '9' => "7", + '8' => "6", + '/' => "9", + '+' => "8", + 'C' => "A", + 'E' => "C", + 'D' => "B", + 'G' => "E", + 'F' => "D", + 'I' => "G", + 'H' => "F", + 'K' => "I", + 'J' => "H", + 'M' => "K", + 'L' => "J", + 'O' => "M", + 'N' => "L", + 'Q' => "O", + 'P' => "N", + 'S' => "Q", + 'R' => "P", + 'U' => "S", + 'T' => "R", + 'W' => "U", + 'V' => "T", + 'Y' => "W", + 'X' => "V", + 'a' => "Y", + 'Z' => "X", + 'b' => "Z", + 'c' => "a", + 'e' => "c", + 'd' => "b", + 'g' => "e", + 'f' => "d", + 'i' => "g", + 'h' => "f", + 'k' => "i", + 'j' => "h", + 'm' => "k", + 'l' => "j", + 'o' => "m", + 'n' => "l", + 'q' => "o", + 'p' => "n", + 's' => "q", + 'r' => "p", + 'u' => "s", + 't' => "r", + 'w' => "u", + 'v' => "t", + 'y' => "w", + 'x' => "v", + '0' => "y", + 'z' => "x", + '1' => "z", + '=' => "=", +}; + +/// First encode to base64 standard and then replaces char with the bcrypt +/// alphabet and removes the '=' chars +pub fn encode(words: &[u8]) -> String { + let hash = STD.encode(words); + let mut res = String::with_capacity(hash.len()); + + for ch in hash.chars() { + // can't fail + let replacement = STANDARD_TO_BCRYPT.get(&ch).unwrap(); + if replacement != &"=" { + res.push_str(replacement); + } + } + + res +} + +// Can potentially panic if the hash given contains invalid characters +pub fn decode(hash: &str) -> BcryptResult> { + let mut res = String::with_capacity(hash.len()); + for ch in hash.chars() { + if let Some(c) = BCRYPT_TO_STANDARD.get(&ch) { + res.push_str(c); + } else { + return Err(BcryptError::DecodeError(ch, hash.to_string())); + } + } + + // Bcrypt base64 has no padding but standard has + // so we need to actually add padding ourselves + if hash.len() % 4 > 0 { + let padding = 4 - hash.len() % 4; + for _ in 0..padding { + res.push_str("="); + } + } + + // safe unwrap: if we had non standard chars, it would have errored before + Ok(STD.decode(&res).unwrap()) +} + +#[cfg(test)] +mod tests { + use super::{decode, encode}; + + #[test] + fn can_decode_bcrypt_base64() { + let hash = "YETqZE6eb07wZEO"; + assert_eq!( + "hello world", + String::from_utf8_lossy(&decode(hash).unwrap()) + ); + } + + #[test] + fn can_encode_to_bcrypt_base64() { + let expected = "YETqZE6eb07wZEO"; + assert_eq!(encode("hello world".as_bytes()), expected); + } + + #[test] + fn decode_errors_with_unknown_char() { + assert!(decode("YETqZE6e_b07wZEO").is_err()); + } +} diff --git a/packages/bcrypt/src/bcrypt.rs b/packages/bcrypt/src/bcrypt.rs new file mode 100644 index 00000000..582ec285 --- /dev/null +++ b/packages/bcrypt/src/bcrypt.rs @@ -0,0 +1,37 @@ +use blowfish::Blowfish; +use byteorder::{ByteOrder, BE}; + +fn setup(cost: u32, salt: &[u8], key: &[u8]) -> Blowfish { + assert!(cost < 32); + let mut state = Blowfish::bc_init_state(); + + state.salted_expand_key(salt, key); + for _ in 0..1u32 << cost { + state.bc_expand_key(key); + state.bc_expand_key(salt); + } + + state +} + +pub fn bcrypt(cost: u32, salt: &[u8], password: &[u8], output: &mut [u8]) { + assert!(salt.len() == 16); + assert!(!password.is_empty() && password.len() <= 72); + assert!(output.len() == 24); + + let state = setup(cost, salt, password); + // OrpheanBeholderScryDoubt + let mut ctext = [ + 0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274, + ]; + for i in 0..3 { + let i: usize = i * 2; + for _ in 0..64 { + let (l, r) = state.bc_encrypt(ctext[i], ctext[i + 1]); + ctext[i] = l; + ctext[i + 1] = r; + } + BE::write_u32(&mut output[i * 4..(i + 1) * 4], ctext[i]); + BE::write_u32(&mut output[(i + 1) * 4..(i + 2) * 4], ctext[i + 1]); + } +} diff --git a/packages/bcrypt/src/errors.rs b/packages/bcrypt/src/errors.rs new file mode 100644 index 00000000..cd192858 --- /dev/null +++ b/packages/bcrypt/src/errors.rs @@ -0,0 +1,75 @@ +use rand; +use std::error; +use std::fmt; +use std::io; + +use crate::lib_bcrypt::{MAX_COST, MIN_COST}; + +/// Library generic result type. +pub type BcryptResult = Result; + +#[derive(Debug)] +/// All the errors we can encounter while hashing/verifying +/// passwords +pub enum BcryptError { + Io(io::Error), + CostNotAllowed(u32), + InvalidPassword, + InvalidVersion(String), + InvalidCost(String), + InvalidPrefix(String), + InvalidHash(String), + DecodeError(char, String), + Rand(rand::Error), +} + +macro_rules! impl_from_error { + ($f: ty, $e: expr) => { + impl From<$f> for BcryptError { + fn from(f: $f) -> BcryptError { + $e(f) + } + } + }; +} + +impl_from_error!(io::Error, BcryptError::Io); +impl_from_error!(rand::Error, BcryptError::Rand); + +impl fmt::Display for BcryptError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + BcryptError::Io(ref err) => write!(f, "IO error: {}", err), + BcryptError::InvalidCost(ref cost) => write!(f, "Invalid Cost: {}", cost), + BcryptError::CostNotAllowed(ref cost) => write!( + f, + "Cost needs to be between {} and {}, got {}", + MIN_COST, MAX_COST, cost + ), + BcryptError::InvalidPassword => write!(f, "Invalid password: contains NULL byte"), + BcryptError::InvalidVersion(ref v) => write!(f, "Invalid version: {}", v), + BcryptError::InvalidPrefix(ref prefix) => write!(f, "Invalid Prefix: {}", prefix), + BcryptError::InvalidHash(ref hash) => write!(f, "Invalid hash: {}", hash), + BcryptError::DecodeError(ref c, ref s) => { + write!(f, "Invalid base64 error in {}, char {}", c, s) + } + BcryptError::Rand(ref err) => write!(f, "Rand error: {}", err), + } + } +} + +impl error::Error for BcryptError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + BcryptError::Io(ref err) => Some(err), + BcryptError::InvalidCost(_) + | BcryptError::CostNotAllowed(_) + | BcryptError::InvalidPassword + | BcryptError::InvalidVersion(_) + | BcryptError::InvalidPrefix(_) + | BcryptError::DecodeError(_, _) + | BcryptError::InvalidHash(_) => None, + BcryptError::Rand(ref err) => Some(err), + } + } +} diff --git a/packages/bcrypt/src/hash_task.rs b/packages/bcrypt/src/hash_task.rs new file mode 100644 index 00000000..8c9dfc15 --- /dev/null +++ b/packages/bcrypt/src/hash_task.rs @@ -0,0 +1,32 @@ +use napi::{Buffer, Env, Error, JsString, Result, Status, Task, Value}; + +use crate::lib_bcrypt::hash; + +pub struct HashTask { + buf: Value, + cost: u32, +} + +impl HashTask { + pub fn new(buf: Value, cost: u32) -> HashTask { + HashTask { buf, cost } + } + + #[inline] + pub fn hash(buf: Value, cost: u32) -> Result { + hash(buf, cost).map_err(|_| Error::from_status(Status::GenericFailure)) + } +} + +impl Task for HashTask { + type Output = String; + type JsValue = JsString; + + fn compute(&self) -> Result { + Self::hash(self.buf, self.cost) + } + + fn resolve(&self, env: &mut Env, output: Self::Output) -> Result> { + env.create_string(&output) + } +} diff --git a/packages/bcrypt/src/lib.rs b/packages/bcrypt/src/lib.rs new file mode 100644 index 00000000..01e3e6d1 --- /dev/null +++ b/packages/bcrypt/src/lib.rs @@ -0,0 +1,103 @@ +#[macro_use] +extern crate napi_rs as napi; +#[macro_use] +extern crate napi_rs_derive; + +use crate::lib_bcrypt::{format_salt, gen_salt, Version}; +use hash_task::HashTask; +use napi::{ + Boolean, Buffer, CallContext, Env, Error, JsString, Number, Object, Result, Status, Value, +}; +use std::convert::TryInto; +use std::str::FromStr; +use verify_task::VerifyTask; + +mod hash_task; +mod verify_task; + +#[cfg(unix)] +#[global_allocator] +static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; + +mod b64; +mod bcrypt; +mod errors; +mod lib_bcrypt; + +#[cfg(not(test))] +register_module!(test_module, init); + +fn init(env: &Env, exports: &mut Value) -> Result<()> { + exports.set_property( + env.create_string("hash")?, + env.create_function("hash", js_async_hash)?, + )?; + + exports.set_property( + env.create_string("hashSync")?, + env.create_function("hashSync", js_hash)?, + )?; + + exports.set_property( + env.create_string("genSalt")?, + env.create_function("genSalt", js_salt)?, + )?; + + exports.set_property( + env.create_string("verifySync")?, + env.create_function("verifySync", js_verify)?, + )?; + + exports.set_property( + env.create_string("verify")?, + env.create_function("verify", js_async_verify)?, + )?; + + Ok(()) +} + +#[js_function(2)] +fn js_salt(ctx: CallContext) -> Result> { + let round = ctx.get::(0)?; + let version = ctx.get::(1)?; + let salt = gen_salt(); + let salt_string = format_salt( + round.try_into()?, + Version::from_str(version.as_str()?).map_err(|_| Error::from_status(Status::InvalidArg))?, + &salt, + ); + ctx.env.create_string(&salt_string) +} + +#[js_function(2)] +fn js_hash(ctx: CallContext) -> Result> { + let password = ctx.get::(0)?; + let cost = ctx.get::(1)?; + let result = HashTask::hash(password, cost.try_into()?)?; + ctx.env.create_string(result.as_str()) +} + +#[js_function(2)] +fn js_async_hash(ctx: CallContext) -> Result> { + let password = ctx.get::(0)?; + let cost = ctx.get::(1)?; + let task = HashTask::new(password, cost.try_into()?); + ctx.env.spawn(task) +} + +#[js_function(2)] +fn js_verify(ctx: CallContext) -> Result> { + let password = ctx.get::(0)?; + let hash = ctx.get::(1)?; + let result = + VerifyTask::verify(password, hash).map_err(|_| Error::from_status(Status::GenericFailure))?; + ctx.env.get_boolean(result) +} + +#[js_function(2)] +fn js_async_verify(ctx: CallContext) -> Result> { + let password = ctx.get::(0)?; + let hash = ctx.get::(1)?; + let task = VerifyTask::new(password, hash); + ctx.env.spawn(task) +} diff --git a/packages/bcrypt/src/lib_bcrypt.rs b/packages/bcrypt/src/lib_bcrypt.rs new file mode 100644 index 00000000..0d594222 --- /dev/null +++ b/packages/bcrypt/src/lib_bcrypt.rs @@ -0,0 +1,408 @@ +//! Easily hash and verify passwords using bcrypt +use rand::{rngs::OsRng, RngCore}; +use std::convert::AsRef; +use std::fmt; +use std::str::FromStr; + +use crate::b64; +pub use crate::bcrypt::bcrypt; +pub use crate::errors::{BcryptError, BcryptResult}; + +// Cost constants +pub const MIN_COST: u32 = 4; +pub const MAX_COST: u32 = 31; +const DEFAULT_COST: u32 = 12; + +#[derive(Debug, PartialEq)] +/// A bcrypt hash result before concatenating +pub struct HashParts { + cost: u32, + salt: String, + hash: String, +} + +/// BCrypt hash version +/// https://en.wikipedia.org/wiki/Bcrypt#Versioning_history +pub enum Version { + TwoA, + TwoX, + TwoY, + TwoB, +} + +impl FromStr for Version { + type Err = BcryptError; + + fn from_str(s: &str) -> Result { + if s == "2a" { + return Ok(Version::TwoA); + } + if s == "2x" { + return Ok(Version::TwoX); + } + if s == "2y" { + return Ok(Version::TwoY); + } + if s == "2b" { + return Ok(Version::TwoB); + } + Err(BcryptError::InvalidVersion(s.to_owned())) + } +} + +impl HashParts { + /// Creates the bcrypt hash string from all its parts + fn format(self) -> String { + self.format_for_version(Version::TwoB) + } + + /// Get the bcrypt hash cost + pub fn get_cost(&self) -> u32 { + self.cost + } + + /// Get the bcrypt hash salt + pub fn get_salt(&self) -> String { + self.salt.clone() + } + + /// Creates the bcrypt hash string from all its part, allowing to customize the version. + pub fn format_for_version(&self, version: Version) -> String { + // Cost need to have a length of 2 so padding with a 0 if cost < 10 + format!("${}${:02}${}{}", version, self.cost, self.salt, self.hash) + } +} + +impl FromStr for HashParts { + type Err = BcryptError; + + fn from_str(s: &str) -> Result { + split_hash(s) + } +} + +impl ToString for HashParts { + fn to_string(&self) -> String { + self.format_for_version(Version::TwoY) + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let str = match self { + Version::TwoA => "2a", + Version::TwoB => "2b", + Version::TwoX => "2x", + Version::TwoY => "2y", + }; + write!(f, "{}", str) + } +} + +/// The main meat: actually does the hashing and does some verification with +/// the cost to ensure it's a correct one +fn _hash_password(password: &[u8], cost: u32, salt: &[u8]) -> BcryptResult { + if cost > MAX_COST || cost < MIN_COST { + return Err(BcryptError::CostNotAllowed(cost)); + } + if password.contains(&0u8) { + return Err(BcryptError::InvalidPassword); + } + + // Output is 24 + let mut output = [0u8; 24]; + // Passwords need to be null terminated + let mut vec = Vec::with_capacity(password.len() + 1); + vec.extend_from_slice(password); + vec.push(0); + // We only consider the first 72 chars; truncate if necessary. + // `bcrypt` below will panic if len > 72 + let truncated = if vec.len() > 72 { &vec[..72] } else { &vec }; + + bcrypt(cost, salt, truncated, &mut output); + + Ok(HashParts { + cost, + salt: b64::encode(salt), + hash: b64::encode(&output[..23]), // remember to remove the last byte + }) +} + +#[inline] +pub fn gen_salt() -> [u8; 16] { + let mut s = [0u8; 16]; + OsRng.fill_bytes(&mut s); + s +} + +#[inline] +pub fn format_salt(rounds: u32, version: Version, salt: &[u8; 16]) -> String { + format!("${}${:0>2}${}", version, rounds, b64::encode(salt)) +} + +/// Takes a full hash and split it into 3 parts: +/// cost, salt and hash +fn split_hash(hash: &str) -> BcryptResult { + let mut parts = HashParts { + cost: 0, + salt: "".to_string(), + hash: "".to_string(), + }; + + // Should be [prefix, cost, hash] + let raw_parts: Vec<_> = hash.split('$').filter(|s| !s.is_empty()).collect(); + + if raw_parts.len() != 3 { + return Err(BcryptError::InvalidHash(hash.to_string())); + } + + if raw_parts[0] != "2y" && raw_parts[0] != "2b" && raw_parts[0] != "2a" { + return Err(BcryptError::InvalidPrefix(raw_parts[0].to_string())); + } + + if let Ok(c) = raw_parts[1].parse::() { + parts.cost = c; + } else { + return Err(BcryptError::InvalidCost(raw_parts[1].to_string())); + } + + if raw_parts[2].len() == 53 { + parts.salt = raw_parts[2][..22].chars().collect(); + parts.hash = raw_parts[2][22..].chars().collect(); + } else { + return Err(BcryptError::InvalidHash(hash.to_string())); + } + + Ok(parts) +} + +/// Generates a password hash using the cost given. +/// The salt is generated randomly using the OS randomness +pub fn hash>(password: P, cost: u32) -> BcryptResult { + hash_with_result(password, cost).map(|r| r.format()) +} + +/// Generates a password hash using the cost given. +/// The salt is generated randomly using the OS randomness. +/// The function returns a result structure and allows to format the hash in different versions. +pub fn hash_with_result>(password: P, cost: u32) -> BcryptResult { + let salt = { + let mut s = [0u8; 16]; + OsRng.fill_bytes(&mut s); + s + }; + + _hash_password(password.as_ref(), cost, salt.as_ref()) +} + +/// Generates a password given a hash and a cost. +/// The function returns a result structure and allows to format the hash in different versions. +pub fn hash_with_salt>( + password: P, + cost: u32, + salt: &[u8], +) -> BcryptResult { + _hash_password(password.as_ref(), cost, salt) +} + +/// Verify that a password is equivalent to the hash provided +pub fn verify>(password: P, hash: &str) -> BcryptResult { + let parts = split_hash(hash)?; + let salt = b64::decode(&parts.salt)?; + let generated = _hash_password(password.as_ref(), parts.cost, &salt)?; + let source_decoded = b64::decode(&parts.hash)?; + let generated_decoded = b64::decode(&generated.hash)?; + if source_decoded.len() != generated_decoded.len() { + return Ok(false); + } + + let mut diff = 0; + for (a, b) in source_decoded.into_iter().zip(generated_decoded) { + diff |= a ^ b; + } + + Ok(diff == 0) +} + +#[cfg(test)] +mod tests { + use super::{ + _hash_password, hash, hash_with_salt, split_hash, verify, BcryptError, BcryptResult, HashParts, + Version, DEFAULT_COST, + }; + use quickcheck::{quickcheck, TestResult}; + use std::iter; + use std::str::FromStr; + + #[test] + fn can_split_hash() { + let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; + let output = split_hash(hash).unwrap(); + let expected = HashParts { + cost: 12, + salt: "L6Bc/AlTQHyd9liGgGEZyO".to_string(), + hash: "FLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u".to_string(), + }; + assert_eq!(output, expected); + } + + #[test] + fn can_output_cost_and_salt_from_parsed_hash() { + let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; + let parsed = HashParts::from_str(hash).unwrap(); + assert_eq!(parsed.get_cost(), 12); + assert_eq!(parsed.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO".to_string()); + } + + #[test] + fn returns_an_error_if_a_parsed_hash_is_baddly_formated() { + let hash1 = "$2y$12$L6Bc/AlTQHyd9lGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; + assert!(HashParts::from_str(hash1).is_err()); + + let hash2 = "!2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; + assert!(HashParts::from_str(hash2).is_err()); + + let hash3 = "$2y$-12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; + assert!(HashParts::from_str(hash3).is_err()); + } + + #[test] + fn can_verify_hash_generated_from_some_online_tool() { + let hash = "$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96"; + assert!(verify("password", hash).unwrap()); + } + + #[test] + fn can_verify_hash_generated_from_python() { + let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie"; + assert!(verify("correctbatteryhorsestapler", hash).unwrap()); + } + + #[test] + fn can_verify_hash_generated_from_node() { + let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi"; + assert!(verify("correctbatteryhorsestapler", hash).unwrap()); + } + + #[test] + fn a_wrong_password_is_false() { + let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie"; + assert!(!verify("wrong", hash).unwrap()); + } + + #[test] + fn errors_with_invalid_hash() { + // there is another $ in the hash part + let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi"; + assert!(verify("correctbatteryhorsestapler", hash).is_err()); + } + + #[test] + fn errors_with_non_number_cost() { + // the cost is not a number + let hash = "$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi"; + assert!(verify("correctbatteryhorsestapler", hash).is_err()); + } + + #[test] + fn errors_with_a_hash_too_long() { + // the cost is not a number + let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri"; + assert!(verify("correctbatteryhorsestapler", hash).is_err()); + } + + #[test] + fn can_verify_own_generated() { + let hashed = hash("hunter2", 4).unwrap(); + assert_eq!(true, verify("hunter2", &hashed).unwrap()); + } + + #[test] + fn long_passwords_truncate_correctly() { + // produced with python -c 'import bcrypt; bcrypt.hashpw(b"x"*100, b"$2a$05$...............................")' + let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2"; + assert!(verify(iter::repeat("x").take(100).collect::(), hash).unwrap()); + } + + #[test] + fn generate_versions() { + let password = "hunter2".as_bytes(); + let salt = vec![0; 16]; + let result = _hash_password(password, DEFAULT_COST, salt.as_slice()).unwrap(); + assert_eq!( + "$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", + result.format_for_version(Version::TwoA) + ); + assert_eq!( + "$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", + result.format_for_version(Version::TwoB) + ); + assert_eq!( + "$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", + result.format_for_version(Version::TwoX) + ); + assert_eq!( + "$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", + result.format_for_version(Version::TwoY) + ); + let hash = result.to_string(); + assert_eq!(true, verify("hunter2", &hash).unwrap()); + } + + #[test] + fn forbid_null_bytes() { + fn assert_invalid_password(password: &[u8]) { + match hash(password, DEFAULT_COST) { + Ok(_) => panic!(format!( + "NULL bytes must be forbidden, but {:?} is allowed.", + password + )), + Err(BcryptError::InvalidPassword) => {} + Err(e) => panic!(format!( + "NULL bytes are forbidden but error differs: {} for {:?}.", + e, password + )), + } + } + assert_invalid_password("\0".as_bytes()); + assert_invalid_password("\0\0\0\0\0\0\0\0".as_bytes()); + assert_invalid_password("passw0rd\0".as_bytes()); + assert_invalid_password("passw0rd\0with tail".as_bytes()); + assert_invalid_password("\0passw0rd".as_bytes()); + } + + #[test] + fn hash_with_fixed_salt() { + let salt = vec![ + 38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18, + ]; + let hashed = hash_with_salt("My S3cre7 P@55w0rd!", 5, &salt) + .unwrap() + .to_string(); + assert_eq!( + "$2y$05$HlFShUxTu4ZHHfOLJwfmCeDj/kuKFKboanXtDJXxCC7aIPTUgxNDe", + &hashed + ); + } + + quickcheck! { + fn can_verify_arbitrary_own_generated(pass: Vec) -> BcryptResult { + let mut pass = pass; + pass.retain(|&b| b != 0); + let hashed = hash(&pass, 4)?; + verify(pass, &hashed) + } + + fn doesnt_verify_different_passwords(a: Vec, b: Vec) -> BcryptResult { + let mut a = a; + a.retain(|&b| b != 0); + let mut b = b; + b.retain(|&b| b != 0); + if a == b { + return Ok(TestResult::discard()); + } + let hashed = hash(a, 4)?; + Ok(TestResult::from_bool(!verify(b, &hashed)?)) + } + } +} diff --git a/packages/bcrypt/src/verify_task.rs b/packages/bcrypt/src/verify_task.rs new file mode 100644 index 00000000..972f521a --- /dev/null +++ b/packages/bcrypt/src/verify_task.rs @@ -0,0 +1,37 @@ +use std::str; + +use crate::lib_bcrypt::verify; +use napi::{Boolean, Buffer, Env, Error, Result, Status, Task, Value}; + +pub struct VerifyTask { + password: Value, + hash: Value, +} + +impl VerifyTask { + pub fn new(password: Value, hash: Value) -> VerifyTask { + Self { password, hash } + } + + #[inline] + pub fn verify(password: Value, hash: Value) -> Result { + verify( + &password, + str::from_utf8(&hash).map_err(|_| Error::from_status(Status::StringExpected))?, + ) + .map_err(|_| Error::from_status(Status::GenericFailure)) + } +} + +impl Task for VerifyTask { + type Output = bool; + type JsValue = Boolean; + + fn compute(&self) -> Result { + VerifyTask::verify(self.password, self.hash) + } + + fn resolve(&self, env: &mut Env, output: Self::Output) -> Result> { + env.get_boolean(output) + } +} diff --git a/scripts/mv-artifacts.js b/scripts/mv-artifacts.js index 1759f6da..aa1f2d5c 100644 --- a/scripts/mv-artifacts.js +++ b/scripts/mv-artifacts.js @@ -8,8 +8,7 @@ const supporttedPlatforms = require('./platforms') const MOVE_ALL = process.env.MOVE_TARGET === 'all' const platforms = MOVE_ALL ? supporttedPlatforms : [platform()] - -const packages = ['crc32', 'jieba'] +const packages = require('./packages') /** * @param {string[]} _platforms platforms diff --git a/scripts/packages.js b/scripts/packages.js new file mode 100644 index 00000000..fde5a906 --- /dev/null +++ b/scripts/packages.js @@ -0,0 +1,8 @@ +const { readdirSync, existsSync, statSync } = require('fs') +const { join } = require('path') + +const packagesDir = join(__dirname, '..', 'packages') + +module.exports = readdirSync(packagesDir) + .filter((dir) => statSync(join(packagesDir, dir)).isDirectory()) + .filter((dir) => existsSync(join(packagesDir, dir, 'Cargo.toml'))) diff --git a/yarn.lock b/yarn.lock index 80b1a32f..da53b7fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1733,6 +1733,14 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bcrypt@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/bcrypt/-/bcrypt-4.0.1.tgz#06e21e749a061020e4ff1283c1faa93187ac57fe" + integrity sha512-hSIZHkUxIDS5zA2o00Kf2O5RfVbQ888n54xQoF/eIaquU4uaLxK8vhhBdktd0B3n2MjkcAWzv4mnhogykBKOUQ== + dependencies: + node-addon-api "^2.0.0" + node-pre-gyp "0.14.0" + before-after-hook@^2.0.0, before-after-hook@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" @@ -5252,6 +5260,11 @@ node-addon-api@^1.3.0: resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz#cf813cd69bb8d9100f6bdca6755fc268f54ac492" integrity sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ== +node-addon-api@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz#f9afb8d777a91525244b01775ea0ddbe1125483b" + integrity sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA== + node-fetch-npm@^2.0.2: version "2.0.4" resolved "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4" @@ -5283,7 +5296,7 @@ node-gyp@^5.0.2: tar "^4.4.12" which "^1.3.1" -node-pre-gyp@^0.14.0: +node-pre-gyp@0.14.0, node-pre-gyp@^0.14.0: version "0.14.0" resolved "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== From f12f7fddd024138683d226522a1acac3fad46c01 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Fri, 15 May 2020 17:34:43 +0800 Subject: [PATCH 2/2] doc: add bcrypt to README --- README.md | 9 +++++---- packages/bcrypt/README.md | 42 +++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 2111ccc4..86c30b51 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Make rust crates binding to NodeJS use [napi-rs](https://github.com/Brooooooklyn # Packages -| Package | Status | Description | -| ---------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------- | -| [`@node-rs/crc32`](./packages/crc32/README.md) | ![](https://github.com/Brooooooklyn/node-rs/workflows/CI/badge.svg) | Fastest `CRC32` implementation using `SIMD` | -| [`@node-rs/jieba`](./packages/jieba/README.md) | ![](https://github.com/Brooooooklyn/node-rs/workflows/CI/badge.svg) | [`jieba-rs`](https://github.com/messense/jieba-rs) binding | +| Package | Status | Description | +| -------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------- | +| [`@node-rs/crc32`](./packages/crc32) | ![](https://github.com/Brooooooklyn/node-rs/workflows/CI/badge.svg) | Fastest `CRC32` implementation using `SIMD` | +| [`@node-rs/jieba`](./packages/jieba) | ![](https://github.com/Brooooooklyn/node-rs/workflows/CI/badge.svg) | [`jieba-rs`](https://github.com/messense/jieba-rs) binding | +| [`@node-rs/bcrypt`](./packages/bcrypt) | ![](https://github.com/Brooooooklyn/node-rs/workflows/CI/badge.svg) | Fastest bcrypt implementation | diff --git a/packages/bcrypt/README.md b/packages/bcrypt/README.md index 3a05e4ee..9aeb7419 100644 --- a/packages/bcrypt/README.md +++ b/packages/bcrypt/README.md @@ -39,25 +39,25 @@ Memory: 16 GB ```
-  @node-rs/bcrypt x 72.11 ops/sec ±1.43% (33 runs sampled)
-  node bcrypt x 62.75 ops/sec ±2.95% (30 runs sampled)
-  Async hash round 10 bench suite: Fastest is @node-rs/bcrypt
-  @node-rs/bcrypt x 18.49 ops/sec ±1.04% (12 runs sampled)
-  node bcrypt x 16.67 ops/sec ±2.05% (11 runs sampled)
-  Async hash round 12 bench suite: Fastest is @node-rs/bcrypt
-  @node-rs/bcrypt x 3.99 ops/sec ±3.17% (6 runs sampled)
-  node bcrypt x 3.13 ops/sec ±1.92% (6 runs sampled)
-  Async hash round 14 bench suite: Fastest is @node-rs/bcrypt
-  @node-rs/bcrypt x 14.32 ops/sec ±0.55% (10 runs sampled)
-  node bcrypt x 13.55 ops/sec ±2.83% (10 runs sampled)
-  Async verify bench suite: Fastest is @node-rs/bcrypt
-  @node-rs/bcrypt x 15.98 ops/sec ±1.12% (44 runs sampled)
-  node bcrypt x 14.55 ops/sec ±1.30% (40 runs sampled)
-  Hash round 10 bench suite: Fastest is @node-rs/bcrypt
-  @node-rs/bcrypt x 4.65 ops/sec ±3.60% (16 runs sampled)
-  node bcrypt x 4.26 ops/sec ±1.90% (15 runs sampled)
-  Hash round 12 bench suite: Fastest is @node-rs/bcrypt
-  @node-rs/bcrypt x 1.16 ops/sec ±2.65% (7 runs sampled)
-  node bcrypt x 1.04 ops/sec ±2.95% (7 runs sampled)
-  Hash round 14 bench suite: Fastest is @node-rs/bcrypt
+@node-rs/bcrypt x 72.11 ops/sec ±1.43% (33 runs sampled)
+node bcrypt x 62.75 ops/sec ±2.95% (30 runs sampled)
+Async hash round 10 bench suite: Fastest is @node-rs/bcrypt
+@node-rs/bcrypt x 18.49 ops/sec ±1.04% (12 runs sampled)
+node bcrypt x 16.67 ops/sec ±2.05% (11 runs sampled)
+Async hash round 12 bench suite: Fastest is @node-rs/bcrypt
+@node-rs/bcrypt x 3.99 ops/sec ±3.17% (6 runs sampled)
+node bcrypt x 3.13 ops/sec ±1.92% (6 runs sampled)
+Async hash round 14 bench suite: Fastest is @node-rs/bcrypt
+@node-rs/bcrypt x 14.32 ops/sec ±0.55% (10 runs sampled)
+node bcrypt x 13.55 ops/sec ±2.83% (10 runs sampled)
+Async verify bench suite: Fastest is @node-rs/bcrypt
+@node-rs/bcrypt x 15.98 ops/sec ±1.12% (44 runs sampled)
+node bcrypt x 14.55 ops/sec ±1.30% (40 runs sampled)
+Hash round 10 bench suite: Fastest is @node-rs/bcrypt
+@node-rs/bcrypt x 4.65 ops/sec ±3.60% (16 runs sampled)
+node bcrypt x 4.26 ops/sec ±1.90% (15 runs sampled)
+Hash round 12 bench suite: Fastest is @node-rs/bcrypt
+@node-rs/bcrypt x 1.16 ops/sec ±2.65% (7 runs sampled)
+node bcrypt x 1.04 ops/sec ±2.95% (7 runs sampled)
+Hash round 14 bench suite: Fastest is @node-rs/bcrypt