diff --git a/packages/bcrypt/Cargo.toml b/packages/bcrypt/Cargo.toml index 5218b8ff..c9f3c498 100644 --- a/packages/bcrypt/Cargo.toml +++ b/packages/bcrypt/Cargo.toml @@ -8,13 +8,13 @@ version = "0.1.0" crate-type = ["cdylib"] [dependencies] -bcrypt = "0.13" -blowfish = { version = "0.9", features = ["bcrypt"] } +bcrypt = "0.14" +getrandom = "0.2" global_alloc = { path = "../../crates/alloc" } napi = { version = "2", default-features = false, features = ["napi3"] } napi-derive = { version = "2" } -getrandom = "0.2" -base64 = { version = "0.20" } +rand = "0.8" +base64 = { version = "0.21" } [dev-dependencies] quickcheck = "1.0" diff --git a/packages/bcrypt/benchmark/bcrypt.js b/packages/bcrypt/benchmark/bcrypt.js index 914fe441..dba74630 100644 --- a/packages/bcrypt/benchmark/bcrypt.js +++ b/packages/bcrypt/benchmark/bcrypt.js @@ -1,144 +1,224 @@ const { cpus } = require('os') +const { hrtime } = require('process') const { hashSync, hash, compare, genSaltSync } = require('bcrypt') const { hashSync: hashSyncJs, hash: hashJs, compare: compareJs, genSaltSync: genSaltSyncJs } = require('bcryptjs') const { Suite } = require('benchmark') const chalk = require('chalk') const { range } = require('lodash') +const { from, timer, lastValueFrom, Subject } = require('rxjs') +const { mergeMap, takeUntil } = require('rxjs/operators') const { hash: napiHash, hashSync: napiHashSync, verify, genSaltSync: napiGenSaltSync } = require('../index') -const parallel = cpus().length +const parallel = cpus().length - 1 const password = 'node-rust-password' -function runAsync(round = 12) { - 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() - }) - }, - }) - .add('bcryptjs', { - defer: true, - fn: (deferred) => { - Promise.all(range(parallel).map(() => hashJs(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() +const CPU_LENGTH = cpus().length + +const DEFAULT_TOTAL_ITERATIONS = 10000 +const DEFAULT_MAX_DURATION = 20000 + +function bench(name, options = {}) { + const suites = [] + return { + add(suiteName, suiteFn) { + suites.push({ + name: suiteName, + fn: suiteFn, }) - .run({ async: true }) - }) + return this + }, + run: async () => { + let fastest = { + perf: -1, + name: '', + } + for (const { suiteName, fn: suiteFn } of suites) { + try { + await suiteFn() + } catch (e) { + console.error(`Warming up ${suiteName} failed`) + throw e + } + } + for (const { name: suiteName, fn: suiteFn } of suites) { + const iterations = options.iterations ?? DEFAULT_TOTAL_ITERATIONS + const parallel = options.parallel ?? CPU_LENGTH + const maxDuration = options.maxDuration ?? DEFAULT_MAX_DURATION + const start = hrtime.bigint() + let totalIterations = 0 + let finishedIterations = 0 + const finish$ = new Subject() + await lastValueFrom( + from({ length: iterations }).pipe( + mergeMap(async () => { + totalIterations++ + await suiteFn() + finishedIterations++ + if (finishedIterations === totalIterations) { + finish$.next() + finish$.complete() + } + }, parallel), + takeUntil(timer(maxDuration)), + ), + ) + if (finishedIterations !== totalIterations) { + await lastValueFrom(finish$) + } + const duration = Number(hrtime.bigint() - start) + const currentPerf = totalIterations / duration + if (currentPerf> fastest.perf) { + fastest = { + perf: currentPerf, + name: suiteName, + } + } + console.info(`${suiteName} ${Math.round(currentPerf * 1e9)} ops/s`) + } + console.info(`In ${name} suite, fastest is ${fastest.name}`) + }, + } } -runAsync() - .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() - }) - }, - }) - .add({ - name: 'bcryptjs', - defer: true, - fn: (deferred) => { - Promise.all(range(parallel).map(() => compareJs(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(() => { - return new Promise((resolve) => { - const syncHashSuite = new Suite(`Hash round 12`) - syncHashSuite - .add('@node-rs/bcrypt', () => { - napiHashSync(password, 12) - }) - .add('node bcrypt', () => { - hashSync(password, 12) - }) - .add('bcryptjs', () => { - hashSyncJs(password, 12) - }) - .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'))}`) - resolve() - }) - .run() - }) - }) - .then(() => { - return new Promise((resolve) => { - new Suite('genSaltSync') - .add('@node-rs/bcrypt', () => { - napiGenSaltSync(10, '2b') - }) - .add('node bcrypt', () => { - genSaltSync(10, 'b') - }) - .add('bcryptjs', () => { - genSaltSyncJs(10) - }) - .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'))}`) - resolve() - }) - .run() - }) +bench(`Hash round 12`) + .add('@node-rs/bcrypt', () => napiHash(password, 12)) + .add('node bcrypt', () => hash(password, 12)) + .run() + .catch((err) => { + console.error(err) + process.exit(1) }) + +// function runAsync(round = 12) { +// 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() +// }) +// }, +// }) +// .add('bcryptjs', { +// defer: true, +// fn: (deferred) => { +// Promise.all(range(parallel).map(() => hashJs(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 }) +// }) +// } + +// runAsync() +// .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() +// }) +// }, +// }) +// .add({ +// name: 'bcryptjs', +// defer: true, +// fn: (deferred) => { +// Promise.all(range(parallel).map(() => compareJs(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(() => { +// return new Promise((resolve) => { +// const syncHashSuite = new Suite(`Hash round 12`) +// syncHashSuite +// .add('@node-rs/bcrypt', () => { +// napiHashSync(password, 12) +// }) +// .add('node bcrypt', () => { +// hashSync(password, 12) +// }) +// .add('bcryptjs', () => { +// hashSyncJs(password, 12) +// }) +// .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'))}`) +// resolve() +// }) +// .run() +// }) +// }) +// .then(() => { +// return new Promise((resolve) => { +// new Suite('genSaltSync') +// .add('@node-rs/bcrypt', () => { +// napiGenSaltSync(10, '2b') +// }) +// .add('node bcrypt', () => { +// genSaltSync(10, 'b') +// }) +// .add('bcryptjs', () => { +// genSaltSyncJs(10) +// }) +// .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'))}`) +// resolve() +// }) +// .run() +// }) +// }) diff --git a/packages/bcrypt/package.json b/packages/bcrypt/package.json index ebf02902..bec25063 100644 --- a/packages/bcrypt/package.json +++ b/packages/bcrypt/package.json @@ -69,7 +69,9 @@ "devDependencies": { "@types/bcrypt": "^5.0.0", "bcrypt": "^5.1.0", - "bcryptjs": "^2.4.3" + "bcryptjs": "^2.4.3", + "cross-env": "^7.0.3", + "rxjs": "^7.8.0" }, "funding": { "type": "github", diff --git a/packages/bcrypt/src/hash_task.rs b/packages/bcrypt/src/hash_task.rs index 7607c772..049bf115 100644 --- a/packages/bcrypt/src/hash_task.rs +++ b/packages/bcrypt/src/hash_task.rs @@ -1,5 +1,6 @@ use napi::{ - bindgen_prelude::Either, Env, Error, JsBuffer, JsBufferValue, Ref, Result, Status, Task, + bindgen_prelude::{Buffer, Either}, + Env, Error, JsBuffer, JsBufferValue, Ref, Result, Status, Task, }; use napi_derive::napi; @@ -9,7 +10,7 @@ pub enum AsyncHashInput { } impl AsyncHashInput { - #[inline] + #[inline(always)] pub fn from_either(input: Either) -> Result { match input { Either::A(s) => Ok(Self::String(s)), @@ -19,7 +20,7 @@ impl AsyncHashInput { } impl AsRef<[u8]> for AsyncHashInput { - #[inline] + #[inline(always)] fn as_ref(&self) -> &[u8] { match self { Self::String(s) => s.as_bytes(), @@ -31,16 +32,16 @@ impl AsRef<[u8]> for AsyncHashInput { pub struct HashTask { buf: AsyncHashInput, cost: u32, - salt: [u8; 16], + salt: Option, } impl HashTask { - #[inline] - pub fn new(buf: AsyncHashInput, cost: u32, salt: [u8; 16]) -> HashTask { + #[inline(always)] + pub fn new(buf: AsyncHashInput, cost: u32, salt: Option) -> HashTask { HashTask { buf, cost, salt } } - #[inline] + #[inline(always)] pub fn hash(buf: &[u8], salt: [u8; 16], cost: u32) -> Result { bcrypt::hash_with_salt(buf, cost, salt) .map(|hash_part| hash_part.to_string()) @@ -54,16 +55,25 @@ impl Task for HashTask { type JsValue = String; fn compute(&mut self) -> Result { + let salt = if let Some(salt) = &self.salt { + let mut s = [0u8; 16]; + s.copy_from_slice(salt.as_ref()); + s + } else { + rand::random() + }; match &self.buf { - AsyncHashInput::String(s) => Self::hash(s.as_bytes(), self.salt, self.cost), - AsyncHashInput::Buffer(buf) => Self::hash(buf.as_ref(), self.salt, self.cost), + AsyncHashInput::String(s) => Self::hash(s.as_bytes(), salt, self.cost), + AsyncHashInput::Buffer(buf) => Self::hash(buf.as_ref(), salt, self.cost), } } + #[inline(always)] fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { Ok(output) } + #[inline(always)] fn finally(&mut self, env: Env) -> Result<()> { if let AsyncHashInput::Buffer(buf) = &mut self.buf { buf.unref(env)?; diff --git a/packages/bcrypt/src/lib.rs b/packages/bcrypt/src/lib.rs index aaa336a2..6bb0891e 100644 --- a/packages/bcrypt/src/lib.rs +++ b/packages/bcrypt/src/lib.rs @@ -11,7 +11,7 @@ use napi_derive::*; use crate::hash_task::AsyncHashInput; use crate::hash_task::HashTask; -use crate::salt_task::{format_salt, gen_salt}; +use crate::salt_task::format_salt; use crate::verify_task::VerifyTask; mod hash_task; @@ -23,12 +23,7 @@ pub const DEFAULT_COST: u32 = 12; #[napi] pub fn gen_salt_sync(round: u32, version: String) -> Result { - let salt = gen_salt().map_err(|err| { - Error::new( - Status::GenericFailure, - format!("Generate salt failed {err}"), - ) - })?; + let salt = rand::random(); Ok(format_salt( round, &version_from_str(version.as_str())?, @@ -60,7 +55,7 @@ pub fn hash_sync( s.copy_from_slice(salt.as_ref()); s } else { - gen_salt().map_err(|err| Error::new(Status::InvalidArg, format!("{err}")))? + rand::random() }; match input { Either::A(s) => HashTask::hash(s.as_bytes(), salt, cost.unwrap_or(DEFAULT_COST)), @@ -69,19 +64,13 @@ pub fn hash_sync( } #[napi] +#[inline(always)] pub fn hash( input: Either, cost: Option, salt: Option, signal: Option, ) -> Result> { - let salt = if let Some(salt) = salt { - let mut s = [0u8; 16]; - s.copy_from_slice(salt.as_ref()); - s - } else { - gen_salt().map_err(|err| Error::new(Status::InvalidArg, format!("{err}")))? - }; let task = HashTask::new( AsyncHashInput::from_either(input)?, cost.unwrap_or(DEFAULT_COST), diff --git a/packages/bcrypt/src/salt_task.rs b/packages/bcrypt/src/salt_task.rs index af8b34e2..06c40ce9 100644 --- a/packages/bcrypt/src/salt_task.rs +++ b/packages/bcrypt/src/salt_task.rs @@ -1,29 +1,18 @@ -use getrandom::getrandom; -use napi::{Env, Error, Result, Status, Task}; +use base64::Engine; +use napi::{Env, Result, Task}; use napi_derive::napi; use crate::Version; -#[inline] -pub(crate) fn gen_salt() -> bcrypt::BcryptResult<[u8; 16]> { - let mut s = [0u8; 16]; - getrandom(&mut s) - .map(|_| s) - .map_err(bcrypt::BcryptError::from)?; - Ok(s) -} +const BASE64_ENCODE_BCRYPT: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::BCRYPT, + base64::engine::GeneralPurposeConfig::new().with_encode_padding(true), +); -#[inline] +#[inline(always)] pub(crate) fn format_salt(rounds: u32, version: &Version, salt: &[u8; 16]) -> String { let mut base64_string = String::new(); - base64::encode_engine_string( - salt, - &mut base64_string, - &base64::engine::fast_portable::FastPortable::from( - &base64::alphabet::BCRYPT, - base64::engine::fast_portable::PAD, - ), - ); + BASE64_ENCODE_BCRYPT.encode_string(salt, &mut base64_string); format!("${version}${rounds:0>2}${base64_string}") } @@ -37,16 +26,13 @@ impl Task for SaltTask { type Output = String; type JsValue = String; + #[inline(always)] fn compute(&mut self) -> Result { - let random = gen_salt().map_err(|err| { - Error::new( - Status::GenericFailure, - format!("Generate salt failed {err}"), - ) - })?; + let random = rand::random(); Ok(format_salt(self.round, &self.version, &random)) } + #[inline(always)] fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { Ok(output) } diff --git a/yarn.lock b/yarn.lock index 36d6fc38..c0b528db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1020,6 +1020,8 @@ __metadata: "@types/bcrypt": ^5.0.0 bcrypt: ^5.1.0 bcryptjs: ^2.4.3 + cross-env: ^7.0.3 + rxjs: ^7.8.0 languageName: unknown linkType: soft @@ -7776,7 +7778,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.5.5, rxjs@npm:^7.5.7": +"rxjs@npm:^7.5.5, rxjs@npm:^7.5.7, rxjs@npm:^7.8.0": version: 7.8.0 resolution: "rxjs@npm:7.8.0" dependencies:

AltStyle によって変換されたページ (->オリジナル) /