diff --git a/packages/bcrypt/__tests__/bcrypt.spec.ts b/packages/bcrypt/__tests__/bcrypt.spec.ts index dd03c27a..6787aef3 100644 --- a/packages/bcrypt/__tests__/bcrypt.spec.ts +++ b/packages/bcrypt/__tests__/bcrypt.spec.ts @@ -1,12 +1,12 @@ import test from 'ava' -import { verifySync, compareSync, verify, compare, hash } from '../index' +import { verifySync, compareSync, verify, compare, hash, genSaltSync } from '../index' const { hashSync } = require('bcryptjs') const fx = Buffer.from('bcrypt-test-password') -const hashedPassword = hashSync(fx.toString('utf8'), 10) +const hashedPassword = hashSync(fx.toString('utf8'), genSaltSync()) test('verifySync hashed password from bcrypt should be true', (t) => { t.true(verifySync(fx, hashedPassword)) diff --git a/packages/bcrypt/binding.d.ts b/packages/bcrypt/binding.d.ts index dc017847..9bec35d4 100644 --- a/packages/bcrypt/binding.d.ts +++ b/packages/bcrypt/binding.d.ts @@ -3,18 +3,18 @@ /* auto-generated by NAPI-RS */ +export interface HashOptions { + salt?: Buffer + version?: string + cost?: number +} export const DEFAULT_COST: number -export function genSaltSync(round: number, version: string): string -export function genSalt(round: number, version: string, signal?: AbortSignal | undefined | null): Promise -export function hashSync( - input: string | Buffer, - cost?: number | undefined | null, - salt?: Buffer | undefined | null, -): string +export function genSaltSync(): Buffer +export function genSalt(signal?: AbortSignal | undefined | null): Promise +export function hashSync(input: string | Buffer, options?: HashOptions | undefined | null): string export function hash( input: string | Buffer, - cost?: number | undefined | null, - salt?: Buffer | undefined | null, + options?: HashOptions | undefined | null, signal?: AbortSignal | undefined | null, ): Promise export function verifySync(input: string | Buffer, hash: string | Buffer): boolean @@ -23,3 +23,9 @@ export function verify( hash: string | Buffer, signal?: AbortSignal | undefined | null, ): Promise +export namespace Version { + export const TWO_A: string + export const TWO_B: string + export const TWO_Y: string + export const TWO_X: string +} diff --git a/packages/bcrypt/binding.js b/packages/bcrypt/binding.js index f52563f4..62f88a8e 100644 --- a/packages/bcrypt/binding.js +++ b/packages/bcrypt/binding.js @@ -218,7 +218,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { DEFAULT_COST, genSaltSync, genSalt, hashSync, hash, verifySync, verify } = nativeBinding +const { DEFAULT_COST, genSaltSync, genSalt, hashSync, hash, verifySync, verify, Version } = nativeBinding module.exports.DEFAULT_COST = DEFAULT_COST module.exports.genSaltSync = genSaltSync @@ -227,3 +227,4 @@ module.exports.hashSync = hashSync module.exports.hash = hash module.exports.verifySync = verifySync module.exports.verify = verify +module.exports.Version = Version diff --git a/packages/bcrypt/src/hash_task.rs b/packages/bcrypt/src/hash_task.rs index f6c68e3b..c6b6b883 100644 --- a/packages/bcrypt/src/hash_task.rs +++ b/packages/bcrypt/src/hash_task.rs @@ -1,8 +1,33 @@ +use bcrypt::Version as BcryptVersion; 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; +use crate::salt_task::gen_salt; + +#[napi] +#[allow(non_snake_case)] +pub mod Version { + #[napi] + pub const TWO_A: &str = "2a"; + #[napi] + pub const TWO_B: &str = "2b"; + #[napi] + pub const TWO_Y: &str = "2y"; + #[napi] + pub const TWO_X: &str = "2x"; +} + +#[napi(object)] +#[derive(Default)] +pub struct HashOptions { + pub salt: Option, + pub version: Option, + pub cost: Option, +} + pub enum AsyncHashInput { String(String), Buffer(Ref), @@ -30,21 +55,13 @@ impl AsRef<[u8]> for AsyncHashInput { pub struct HashTask { buf: AsyncHashInput, - cost: u32, - salt: [u8; 16], + options: HashOptions, } impl HashTask { #[inline] - pub fn new(buf: AsyncHashInput, cost: u32, salt: [u8; 16]) -> HashTask { - HashTask { buf, cost, salt } - } - - #[inline] - 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()) - .map_err(|err| Error::new(Status::GenericFailure, format!("{}", err))) + pub fn new(buf: AsyncHashInput, options: HashOptions) -> HashTask { + HashTask { buf, options } } } @@ -54,10 +71,26 @@ impl Task for HashTask { type JsValue = String; fn compute(&mut self) -> Result { + let salt = if let Some(ref salt) = self.options.salt { + let mut s = [0; 16]; + s.copy_from_slice(salt.as_ref()); + s + } else { + gen_salt().map_err(|err| Error::new(Status::GenericFailure, format!("{}", err)))? + }; + let cost = self.options.cost.unwrap_or(bcrypt::DEFAULT_COST); 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) => bcrypt::hash_with_salt(s.as_bytes(), cost, salt), + AsyncHashInput::Buffer(buf) => bcrypt::hash_with_salt(buf.as_ref(), cost, salt), } + .map_err(|err| Error::new(Status::GenericFailure, format!("{}", err))) + .and_then(|hash_part| { + if let Some(ref version) = self.options.version { + Ok(hash_part.format_for_version(version_from_str(version)?)) + } else { + Ok(hash_part.to_string()) + } + }) } fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { @@ -71,3 +104,17 @@ impl Task for HashTask { Ok(()) } } + +#[inline] +fn version_from_str(version: &str) -> Result { + match version { + "2a" => Ok(BcryptVersion::TwoA), + "2b" => Ok(BcryptVersion::TwoB), + "2y" => Ok(BcryptVersion::TwoX), + "2x" => Ok(BcryptVersion::TwoY), + _ => Err(Error::new( + Status::InvalidArg, + format!("{} is not a valid version", version), + )), + } +} diff --git a/packages/bcrypt/src/lib.rs b/packages/bcrypt/src/lib.rs index 4b1d9dde..4a1738b6 100644 --- a/packages/bcrypt/src/lib.rs +++ b/packages/bcrypt/src/lib.rs @@ -4,14 +4,14 @@ /// Explicit extern crate to use allocator. extern crate global_alloc; -use bcrypt::Version; +use hash_task::HashOptions; use napi::bindgen_prelude::*; use napi::{Error, JsBuffer, Result, Status}; 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::gen_salt; use crate::verify_task::VerifyTask; mod hash_task; @@ -22,70 +22,40 @@ mod verify_task; pub const DEFAULT_COST: u32 = 12; #[napi] -pub fn gen_salt_sync(round: u32, version: String) -> Result { +pub fn gen_salt_sync() -> Result { let salt = gen_salt().map_err(|err| { Error::new( Status::GenericFailure, format!("Generate salt failed {}", err), ) })?; - Ok(format_salt( - round, - &version_from_str(version.as_str())?, - &salt, - )) + Ok(salt.to_vec().into()) } #[napi(js_name = "genSalt")] -pub fn gen_salt_js( - round: u32, - version: String, - signal: Option, -) -> Result> { - let task = salt_task::SaltTask { - round, - version: version_from_str(version.as_str())?, - }; +pub fn gen_salt_js(signal: Option) -> Result> { + let task = salt_task::SaltTask {}; Ok(AsyncTask::with_optional_signal(task, signal)) } #[napi] -pub fn hash_sync( - input: Either, - cost: Option, - salt: 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)))? - }; - match input { - Either::A(s) => HashTask::hash(s.as_bytes(), salt, cost.unwrap_or(DEFAULT_COST)), - Either::B(b) => HashTask::hash(b.as_ref(), salt, cost.unwrap_or(DEFAULT_COST)), - } +pub fn hash_sync(input: Either, options: Option) -> Result { + let mut task = HashTask::new( + AsyncHashInput::from_either(input)?, + options.unwrap_or_default(), + ); + task.compute() } #[napi] pub fn hash( input: Either, - cost: Option, - salt: Option, + options: 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), - salt, + options.unwrap_or_default(), ); Ok(AsyncTask::with_optional_signal(task, signal)) } @@ -117,17 +87,3 @@ pub fn verify( ); Ok(AsyncTask::with_optional_signal(task, signal)) } - -#[inline] -fn version_from_str(version: &str) -> Result { - match version { - "2a" => Ok(Version::TwoA), - "2b" => Ok(Version::TwoB), - "2y" => Ok(Version::TwoX), - "2x" => Ok(Version::TwoY), - _ => Err(Error::new( - Status::InvalidArg, - format!("{} is not a valid version", version), - )), - } -} diff --git a/packages/bcrypt/src/salt_task.rs b/packages/bcrypt/src/salt_task.rs index 253ba8fa..a88c5db5 100644 --- a/packages/bcrypt/src/salt_task.rs +++ b/packages/bcrypt/src/salt_task.rs @@ -1,9 +1,7 @@ use getrandom::getrandom; -use napi::{Env, Error, Result, Status, Task}; +use napi::{bindgen_prelude::Buffer, Env, Error, Result, Status, Task}; use napi_derive::napi; -use crate::Version; - #[inline] pub(crate) fn gen_salt() -> bcrypt::BcryptResult<[u8; 16]> { let mut s = [0u8; 16]; @@ -13,25 +11,37 @@ pub(crate) fn gen_salt() -> bcrypt::BcryptResult<[u8; 16]> { Ok(s) } -#[inline] -pub(crate) fn format_salt(rounds: u32, version: &Version, salt: &[u8; 16]) -> String { - format!( - "${}${:0>2}${}", - version, - rounds, - base64::encode_config(salt, base64::BCRYPT) - ) +#[napi] +pub struct Salt { + pub(crate) inner: [u8; 16], + pub(crate) version: bcrypt::Version, + pub(crate) cost: u32, } -pub struct SaltTask { - pub(crate) round: u32, - pub(crate) version: Version, +#[napi] +impl Salt { + #[napi] + pub fn version(&self) -> String { + format!("{}", self.version) + } + + #[napi] + pub fn cost(&self) -> u32 { + self.cost + } + + #[napi] + pub fn to_string(&self) -> String { + base64::encode_config(self.inner, base64::BCRYPT) + } } +pub struct SaltTask {} + #[napi] impl Task for SaltTask { - type Output = String; - type JsValue = String; + type Output = [u8; 16]; + type JsValue = Buffer; fn compute(&mut self) -> Result { let random = gen_salt().map_err(|err| { @@ -40,10 +50,10 @@ impl Task for SaltTask { format!("Generate salt failed {}", err), ) })?; - Ok(format_salt(self.round, &self.version, &random)) + Ok(random) } fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { - Ok(output) + Ok(output.to_vec().into()) } }