From 30e3da6475bf0b8c6a8d40388bd50a7c235debff Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 24 Nov 2022 09:43:22 +0530 Subject: [PATCH 01/74] refactor: rewrite in esm --- .github/COMMIT_CONVENTION.md | 70 ---- .github/CONTRIBUTING.md | 46 --- .github/ISSUE_TEMPLATE/bug_report.md | 29 -- .github/ISSUE_TEMPLATE/feature_request.md | 28 -- .github/PULL_REQUEST_TEMPLATE.md | 28 -- .github/labels.json | 170 ++++++++ .github/workflows/test.yml | 19 +- .husky/commit-msg | 7 +- adonis-typings/container.ts | 15 - adonis-typings/hash.ts | 223 ---------- adonis-typings/index.ts | 11 - bin/{japaTypes.ts => japa_types.ts} | 0 bin/test.ts | 5 +- config.ts | 37 -- index.ts | 16 + package.json | 142 ++++--- providers/HashProvider.ts | 23 -- src/Drivers/Argon.ts | 96 ----- src/Drivers/Bcrypt.ts | 70 ---- src/Drivers/Fake.ts | 41 -- src/Drivers/Scrypt.ts | 221 ---------- src/Hash/index.ts | 190 --------- src/define_config.ts | 46 +++ src/drivers/argon.ts | 335 +++++++++++++++ src/drivers/bcrypt.ts | 272 +++++++++++++ src/drivers/fake.ts | 48 +++ src/drivers/scrypt.ts | 264 ++++++++++++ src/exceptions/invalid_hash_config.ts | 18 + src/hash.ts | 58 +++ src/hash_manager.ts | 202 ++++++++++ src/helpers.ts | 54 +++ src/legacy/bcrypt_base64.cjs | 103 +++++ src/phc_formatter.ts | 44 ++ src/types.ts | 233 +++++++++++ src/utils.ts | 22 - standalone.ts | 10 - test-helpers/index.ts | 15 - test/argon2.spec.ts | 130 ------ test/bcrypt.spec.ts | 61 --- test/fake.spec.ts | 31 -- test/hash-provider.spec.ts | 67 --- test/hash.spec.ts | 169 -------- test/scrypt.spec.ts | 232 ----------- tests/define_config.spec.ts | 59 +++ tests/drivers/argon2.spec.ts | 471 ++++++++++++++++++++++ tests/drivers/bcrypt.spec.ts | 267 ++++++++++++ tests/drivers/fake.spec.ts | 41 ++ tests/drivers/scrypt.spec.ts | 334 +++++++++++++++ tests/hash.spec.ts | 38 ++ tests/hash_manager.spec.ts | 270 +++++++++++++ tests/phc_formatter.spec.ts | 184 +++++++++ tsconfig.json | 32 +- 52 files changed, 3633 insertions(+), 1964 deletions(-) delete mode 100644 .github/COMMIT_CONVENTION.md delete mode 100644 .github/CONTRIBUTING.md delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/labels.json delete mode 100644 adonis-typings/container.ts delete mode 100644 adonis-typings/hash.ts delete mode 100644 adonis-typings/index.ts rename bin/{japaTypes.ts => japa_types.ts} (100%) delete mode 100644 config.ts create mode 100644 index.ts delete mode 100644 providers/HashProvider.ts delete mode 100644 src/Drivers/Argon.ts delete mode 100644 src/Drivers/Bcrypt.ts delete mode 100644 src/Drivers/Fake.ts delete mode 100644 src/Drivers/Scrypt.ts delete mode 100644 src/Hash/index.ts create mode 100644 src/define_config.ts create mode 100644 src/drivers/argon.ts create mode 100644 src/drivers/bcrypt.ts create mode 100644 src/drivers/fake.ts create mode 100644 src/drivers/scrypt.ts create mode 100644 src/exceptions/invalid_hash_config.ts create mode 100644 src/hash.ts create mode 100644 src/hash_manager.ts create mode 100644 src/helpers.ts create mode 100644 src/legacy/bcrypt_base64.cjs create mode 100644 src/phc_formatter.ts create mode 100644 src/types.ts delete mode 100644 src/utils.ts delete mode 100644 standalone.ts delete mode 100644 test-helpers/index.ts delete mode 100644 test/argon2.spec.ts delete mode 100644 test/bcrypt.spec.ts delete mode 100644 test/fake.spec.ts delete mode 100644 test/hash-provider.spec.ts delete mode 100644 test/hash.spec.ts delete mode 100644 test/scrypt.spec.ts create mode 100644 tests/define_config.spec.ts create mode 100644 tests/drivers/argon2.spec.ts create mode 100644 tests/drivers/bcrypt.spec.ts create mode 100644 tests/drivers/fake.spec.ts create mode 100644 tests/drivers/scrypt.spec.ts create mode 100644 tests/hash.spec.ts create mode 100644 tests/hash_manager.spec.ts create mode 100644 tests/phc_formatter.spec.ts diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md deleted file mode 100644 index fc852af..0000000 --- a/.github/COMMIT_CONVENTION.md +++ /dev/null @@ -1,70 +0,0 @@ -## Git Commit Message Convention - -> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). - -Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. - -``` js -/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ -``` - -## Commit Message Format -A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: - -> The **scope** is optional - -``` -feat(router): add support for prefix - -Prefix makes it easier to append a path to a group of routes -``` - -1. `feat` is type. -2. `router` is scope and is optional -3. `add support for prefix` is the subject -4. The **body** is followed by a blank line. -5. The optional **footer** can be added after the body, followed by a blank line. - -## Types -Only one type can be used at a time and only following types are allowed. - -- feat -- fix -- docs -- style -- refactor -- perf -- test -- workflow -- ci -- chore -- types -- build - -If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. - -### Revert -If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. - -## Scope -The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. - -## Subject -The subject contains succinct description of the change: - -- use the imperative, present tense: "change" not "changed" nor "changes". -- don't capitalize first letter -- no dot (.) at the end - -## Body - -Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". -The body should include the motivation for the change and contrast this with previous behavior. - -## Footer - -The footer should contain any information about **Breaking Changes** and is also the place to -reference GitHub issues that this commit **Closes**. - -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. - diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index f0c5446..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributing - -AdonisJS is a community driven project. You are free to contribute in any of the following ways. - -- [Coding style](coding-style) -- [Fix bugs by creating PR's](fix-bugs-by-creating-prs) -- [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) -- [Report security issues](report-security-issues) -- [Be a part of the community](be-a-part-of-community) - -## Coding style - -Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. - -## Fix bugs by creating PR's - -We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. - -Go through the following points, before creating a new PR. - -1. Create an issue discussing the bug or short-coming in the framework. -2. Once approved, go ahead and fork the REPO. -3. Make sure to start from the `develop`, since this is the upto date branch. -4. Make sure to keep commits small and relevant. -5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. -6. Once done with all the changes, create a PR against the `develop` branch. - -## Share an RFC for new features or big changes - -Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. - -### What is an RFC? - -RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). - -In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. - -The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. - -## Report security issues - -All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. - -## Be a part of community - -We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e65000c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Report identified bugs ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -- Lots of raised issues are directly not bugs but instead are design decisions taken by us. -- Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. -- Ensure the issue isn't already reported. -- Ensure you are reporting the bug in the correct repo. - -*Delete the above section and the instructions in the sections below before submitting* - -## Package version - - -## Node.js and npm version - - -## Sample Code (to reproduce the issue) - - -## BONUS (a sample repo to reproduce the issue) - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index abd44a5..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature request -about: Propose changes for adding a new feature ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -## Consider an RFC - -Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if - -- Feature introduces a breaking change -- Demands lots of time and changes in the current code base. - -*Delete the above section and the instructions in the sections below before submitting* - -## Why this feature is required (specific use-cases will be appreciated)? - - -## Have you tried any other work arounds? - - -## Are you willing to work on it with little guidance? - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index fa5ee2e..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Proposed changes - -Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. - -## Types of changes - -What types of changes does your code introduce? - -_Put an `x` in the boxes that apply_ - -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -## Checklist - -_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - -- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/hash/blob/master/.github/CONTRIBUTING.md) doc -- [ ] Lint and unit tests pass locally with my changes -- [ ] I have added tests that prove my fix is effective or that my feature works. -- [ ] I have added necessary documentation (if appropriate) - -## Further comments - -If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..ba001c6 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,170 @@ +[ + { + "name": "Priority: Critical", + "color": "ea0056", + "description": "The issue needs urgent attention", + "aliases": [] + }, + { + "name": "Priority: High", + "color": "5666ed", + "description": "Look into this issue before picking up any new work", + "aliases": [] + }, + { + "name": "Priority: Medium", + "color": "f4ff61", + "description": "Try to fix the issue for the next patch/minor release", + "aliases": [] + }, + { + "name": "Priority: Low", + "color": "87dfd6", + "description": "Something worth considering, but not a top priority for the team", + "aliases": [] + }, + { + "name": "Semver: Alpha", + "color": "008480", + "description": "Will make it's way to the next alpha version of the package", + "aliases": [] + }, + { + "name": "Semver: Major", + "color": "ea0056", + "description": "Has breaking changes", + "aliases": [] + }, + { + "name": "Semver: Minor", + "color": "fbe555", + "description": "Mainly new features and improvements", + "aliases": [] + }, + { + "name": "Semver: Next", + "color": "5666ed", + "description": "Will make it's way to the bleeding edge version of the package", + "aliases": [] + }, + { + "name": "Semver: Patch", + "color": "87dfd6", + "description": "A bug fix", + "aliases": [] + }, + { + "name": "Status: Abandoned", + "color": "ffffff", + "description": "Dropped and not into consideration", + "aliases": ["wontfix"] + }, + { + "name": "Status: Accepted", + "color": "e5fbf2", + "description": "The proposal or the feature has been accepted for the future versions", + "aliases": [] + }, + { + "name": "Status: Blocked", + "color": "ea0056", + "description": "The work on the issue or the PR is blocked. Check comments for reasoning", + "aliases": [] + }, + { + "name": "Status: Completed", + "color": "008672", + "description": "The work has been completed, but not released yet", + "aliases": [] + }, + { + "name": "Status: In Progress", + "color": "73dbc4", + "description": "Still banging the keyboard", + "aliases": ["in progress"] + }, + { + "name": "Status: On Hold", + "color": "f4ff61", + "description": "The work was started earlier, but is on hold now. Check comments for reasoning", + "aliases": ["On Hold"] + }, + { + "name": "Status: Review Needed", + "color": "fbe555", + "description": "Review from the core team is required before moving forward", + "aliases": [] + }, + { + "name": "Status: Awaiting More Information", + "color": "89f8ce", + "description": "Waiting on the issue reporter or PR author to provide more information", + "aliases": [] + }, + { + "name": "Status: Need Contributors", + "color": "7057ff", + "description": "Looking for contributors to help us move forward with this issue or PR", + "aliases": [] + }, + { + "name": "Type: Bug", + "color": "ea0056", + "description": "The issue has indentified a bug", + "aliases": ["bug"] + }, + { + "name": "Type: Security", + "color": "ea0056", + "description": "Spotted security vulnerability and is a top priority for the core team", + "aliases": [] + }, + { + "name": "Type: Duplicate", + "color": "00837e", + "description": "Already answered or fixed previously", + "aliases": ["duplicate"] + }, + { + "name": "Type: Enhancement", + "color": "89f8ce", + "description": "Improving an existing feature", + "aliases": ["enhancement"] + }, + { + "name": "Type: Feature Request", + "color": "483add", + "description": "Request to add a new feature to the package", + "aliases": [] + }, + { + "name": "Type: Invalid", + "color": "dbdbdb", + "description": "Doesn't really belong here. Maybe use discussion threads?", + "aliases": ["invalid"] + }, + { + "name": "Type: Question", + "color": "eceafc", + "description": "Needs clarification", + "aliases": ["help wanted", "question"] + }, + { + "name": "Type: Documentation Change", + "color": "7057ff", + "description": "Documentation needs some improvements", + "aliases": ["documentation"] + }, + { + "name": "Type: Dependencies Update", + "color": "00837e", + "description": "Bump dependencies", + "aliases": ["dependencies"] + }, + { + "name": "Good First Issue", + "color": "008480", + "description": "Want to contribute? Just filter by this label", + "aliases": ["good first issue"] + } +] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b778ca..2d9bc9e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,20 +3,5 @@ on: - push - pull_request jobs: - linux: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: - - 14.15.4 - - 17.x - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: Install - run: npm install - - name: Run tests - run: npm test + test: + uses: adonisjs/.github/.github/workflows/test.yml@main diff --git a/.husky/commit-msg b/.husky/commit-msg index 4654c12..4002db7 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,3 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit diff --git a/adonis-typings/container.ts b/adonis-typings/container.ts deleted file mode 100644 index 015d840..0000000 --- a/adonis-typings/container.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/Application' { - import { HashContract } from '@ioc:Adonis/Core/Hash' - export interface ContainerBindings { - 'Adonis/Core/Hash': HashContract - } -} diff --git a/adonis-typings/hash.ts b/adonis-typings/hash.ts deleted file mode 100644 index 4b1e3ea..0000000 --- a/adonis-typings/hash.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/Hash' { - import { ApplicationContract } from '@ioc:Adonis/Core/Application' - import { ManagerContract } from '@poppinss/manager' - - /** - * Every driver must implement the Hash driver - * contract - */ - export interface HashDriverContract { - ids?: string[] - params?: any - - /** - * Hash plain text value using the default mapping - */ - make(value: string): Promise - - /** - * Check the hash against the current config to find it needs - * to be re-hashed or not - */ - needsReHash?(hashedValue: string): boolean - - /** - * Verify plain value against the hashed value to find if it's - * valid or not - */ - verify(hashedValue: string, plainValue: string): Promise - } - - /** - * Shape of bcrypt config - */ - export type BcryptConfig = { - driver: 'bcrypt' - rounds: number - } - - /** - * Bcrypt driver contract - */ - export interface BcryptContract extends HashDriverContract { - ids: ['bcrypt'] - params: { - rounds: 'r' - } - } - - /** - * Shape of argon2 config - */ - export type ArgonConfig = { - driver: 'argon2' - variant: 'd' | 'i' | 'id' - iterations: number - memory: number - parallelism: number - saltSize: number - } - - /** - * Argon2 driver contract - */ - export interface ArgonContract extends HashDriverContract { - ids: ['argon2d', 'argon2i', 'argon2id'] - params: { - iterations: 't' - memory: 'm' - parallelism: 'p' - } - } - - /** - * Shape of scrypt config - */ - export type ScryptConfig = { - driver: 'scrypt' - - /** - * CPU/memory cost parameter. Must be a power of two greater than one. - * Default: 16384 - */ - cost: number - - /** - * Block size parameter. - * Default: 8 - */ - blockSize: number - - /** - * Parallelization parameter. - * Default: 1 - */ - parallelization: number - - /** - * Size of the salt. - * Minimum: 16 - * Default: 16 - */ - saltSize: number - - /** - * Memory upper bound. - * Default: 16777216 - */ - maxMemory: number - - /** - * Desired key length in bytes. - * Default: 64 - */ - keyLength: number - } - - /** - * Scrypt driver contract - */ - export interface ScryptContract extends HashDriverContract { - ids: ['scrypt'] - params: { - cost: 'n' - blockSize: 'r' - parallelization: 'p' - } - } - - export interface FakeContract extends HashDriverContract { - ids: ['fake'] - needsReHash(hashedValue: string): boolean - } - - /** - * Default list of available drivers. One can you reference this type - * to setup the `HashersList`. - * - * We will remove this later. Make sure all stubs are not using this - * type. - */ - export interface HashDrivers { - bcrypt: { - config: BcryptConfig - implementation: BcryptContract - } - argon2: { - config: ArgonConfig - implementation: ArgonContract - } - scrypt: { - config: ScryptConfig - implementation: ScryptContract - } - } - - /** - * List of hash mappings used by the app. Using declaration merging, one - * must extend this interface. - * - * MUST BE SET IN THE USER LAND. - */ - export interface HashersList {} - - /** - * Shape of config accepted by the Hash module. - */ - export interface HashConfig { - default: keyof HashersList - list: { [P in keyof HashersList]: HashersList[P]['config'] } - } - - /** - * Hash mananger interface - */ - export interface HashContract - extends ManagerContract< - ApplicationContract, - HashDriverContract, - HashDriverContract, - { [P in keyof HashersList]: HashersList[P]['implementation'] } - > { - readonly isFaked: boolean - - /** - * Hash plain text value using the default mapping - */ - make(value: string): ReturnType - - /** - * Fake hash - */ - fake(): void - - /** - * Remove fake - */ - restore(): void - - /** - * Verify plain value against the hashed value to find if it's - * valid or not - */ - verify(hashedValue: string, plainValue: string): ReturnType - - /** - * Check the hash against the current config to find it needs - * to be re-hashed or not - */ - needsReHash(hashedValue: string): boolean - } - - const Hash: HashContract - export default Hash -} diff --git a/adonis-typings/index.ts b/adonis-typings/index.ts deleted file mode 100644 index 9d1f9f3..0000000 --- a/adonis-typings/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// -/// diff --git a/bin/japaTypes.ts b/bin/japa_types.ts similarity index 100% rename from bin/japaTypes.ts rename to bin/japa_types.ts diff --git a/bin/test.ts b/bin/test.ts index 5aba7ce..5b398e1 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,4 +1,5 @@ import { assert } from '@japa/assert' +import { expectTypeOf } from '@japa/expect-type' import { specReporter } from '@japa/spec-reporter' import { runFailedTests } from '@japa/run-failed-tests' import { processCliArgs, configure, run } from '@japa/runner' @@ -19,8 +20,8 @@ import { processCliArgs, configure, run } from '@japa/runner' configure({ ...processCliArgs(process.argv.slice(2)), ...{ - files: ['test/**/*.spec.ts'], - plugins: [assert(), runFailedTests()], + files: ['tests/**/*.spec.ts'], + plugins: [assert(), runFailedTests(), expectTypeOf()], reporters: [specReporter()], importer: (filePath: string) => import(filePath), }, diff --git a/config.ts b/config.ts deleted file mode 100644 index de0fef3..0000000 --- a/config.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { HashDrivers } from '@ioc:Adonis/Core/Hash' - -/** - * Expected shape of the config accepted by the "hashConfig" - * method - */ -type HashConfig = { - list: { - [name: string]: { - [K in keyof HashDrivers]: HashDrivers[K]['config'] & { driver: K } - }[keyof HashDrivers] - } -} - -/** - * Define config for the Hash module - */ -export function hashConfig(config: T): T { - return config -} - -/** - * Pull hashers list from the config defined inside the "config/hash.ts" - * file - */ -export type InferListFromConfig = { - [K in keyof T['list']]: HashDrivers[T['list'][K]['driver']] -} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..6efed4c --- /dev/null +++ b/index.ts @@ -0,0 +1,16 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { Hash } from './src/hash.js' +export { Argon } from './src/drivers/argon.js' +export { Bcrypt } from './src/drivers/bcrypt.js' +export { Scrypt } from './src/drivers/scrypt.js' +export { HashManager } from './src/hash_manager.js' +export { defineConfig } from './src/define_config.js' +export { PhcFormatter } from './src/phc_formatter.js' diff --git a/package.json b/package.json index bc4d0ca..afc7722 100644 --- a/package.json +++ b/package.json @@ -2,121 +2,92 @@ "name": "@adonisjs/hash", "version": "7.2.2", "description": "Multi driver hash module with support for PHC string formats", - "main": "build/providers/HashProvider", + "main": "build/index.js", + "type": "module", "files": [ - "build/adonis-typings", - "build/providers", "build/src", - "build/config.d.ts", - "build/config.js", - "build/standalone.d.ts", - "build/standalone.js" + "build/index.d.ts", + "build/index.js" ], + "exports": { + ".": "./build/index.js", + "./types": "./build/src/types.js" + }, "scripts": { - "mrm": "mrm --preset=@adonisjs/mrm-preset", "pretest": "npm run lint", - "test": "node -r @adonisjs/require-ts/build/register bin/test.ts", - "clean": "del build", + "test": "c8 npm run vscode:test", + "clean": "del-cli build", "compile": "npm run lint && npm run clean && tsc", "build": "npm run compile", - "commit": "git-cz", - "release": "np --message=\"chore(release): %s\"", + "release": "np", "version": "npm run build", "format": "prettier --write .", "prepublishOnly": "npm run build", "lint": "eslint . --ext=.ts", - "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/hash" - }, - "repository": { - "type": "git", - "url": "git+ssh://git@github.com/poppinss/hash.git" + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/hash", + "vscode:test": "node --loader=ts-node/esm bin/test.ts" }, "keywords": [ "hash", "bcrypt", "argon2" ], - "author": "poppinss,virk", + "author": "adonisjs,virk", "license": "MIT", - "bugs": { - "url": "https://github.com/poppinss/hash/issues" - }, - "homepage": "https://github.com/poppinss/hash#readme", "devDependencies": { - "@adonisjs/application": "^5.2.5", - "@adonisjs/mrm-preset": "^5.0.3", - "@adonisjs/require-ts": "^2.0.13", + "@adonisjs/application": "^5.3.0", + "@commitlint/cli": "^17.3.0", + "@commitlint/config-conventional": "^17.3.0", "@japa/assert": "^1.3.6", + "@japa/expect-type": "^1.0.2", "@japa/run-failed-tests": "^1.1.0", "@japa/runner": "^2.2.2", "@japa/spec-reporter": "^1.3.2", "@poppinss/dev-utils": "^2.0.3", + "@swc/core": "^1.3.19", + "@types/bcrypt": "^5.0.0", "@types/node": "^18.11.9", + "@types/sinon": "^10.0.13", "argon2": "^0.30.2", "bcrypt": "^5.0.1", - "commitizen": "^4.2.5", - "cz-conventional-changelog": "^3.3.0", + "c8": "^7.12.0", "del-cli": "^5.0.0", - "eslint": "^8.27.0", + "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", - "eslint-plugin-adonis": "^2.1.1", + "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.2.0", "husky": "^8.0.2", - "mrm": "^4.1.13", "np": "^7.6.2", - "phc-argon2": "^1.1.3", - "phc-bcrypt": "^1.0.8", "prettier": "^2.7.1", - "typescript": "^4.8.4" - }, - "nyc": { - "exclude": [ - "test" - ], - "extension": [ - ".ts" - ] - }, - "husky": { - "hooks": { - "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" - } - }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, - "np": { - "contents": ".", - "anyBranch": false + "sinon": "^14.0.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.3" }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/manager": "^5.0.2", - "@poppinss/utils": "^5.0.0" + "@poppinss/utils": "^6.0.1-0" }, "peerDependencies": { - "@adonisjs/application": "^5.0.0" + "argon2": "^0.30.2", + "bcrypt": "^5.0.1" }, - "publishConfig": { - "access": "public", - "tag": "latest" + "peerDependenciesMeta": { + "argon2": { + "optional": true + }, + "bcrypt": { + "optional": true + } }, - "mrmConfig": { - "core": true, - "license": "MIT", - "services": [ - "github-actions" - ], - "minNodeVersion": "14.15.4", - "probotApps": [ - "stale", - "lock" - ], - "runGhActionsOnWindows": false + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/poppinss/hash.git" }, + "bugs": { + "url": "https://github.com/poppinss/hash/issues" + }, + "homepage": "https://github.com/poppinss/hash#readme", "eslintConfig": { "extends": [ "plugin:adonis/typescriptPackage", @@ -146,5 +117,30 @@ "bracketSpacing": true, "arrowParens": "always", "printWidth": 100 + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "publishConfig": { + "access": "public", + "tag": "next" + }, + "np": { + "message": "chore(release): %s", + "tag": "next", + "branch": "main", + "anyBranch": false + }, + "c8": { + "reporter": [ + "text", + "html" + ], + "exclude": [ + "tests/**", + "src/legacy/**" + ] } } diff --git a/providers/HashProvider.ts b/providers/HashProvider.ts deleted file mode 100644 index 8f6bbf7..0000000 --- a/providers/HashProvider.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -export default class HashProvider { - constructor(protected app: ApplicationContract) {} - public static needsApplication = true - - public register() { - this.app.container.singleton('Adonis/Core/Hash', () => { - const config = this.app.container.resolveBinding('Adonis/Core/Config').get('hash', {}) - const { Hash } = require('../src/Hash') - return new Hash(this, config) - }) - } -} diff --git a/src/Drivers/Argon.ts b/src/Drivers/Argon.ts deleted file mode 100644 index f1e5ed7..0000000 --- a/src/Drivers/Argon.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import phc from '@phc/format' -import argon2 from 'phc-argon2' -import { ArgonConfig, ArgonContract } from '@ioc:Adonis/Core/Hash' - -/** - * Hash driver built on top of argon hashing algorithm. The driver adheres - * to `phc` string format. - */ -export class Argon implements ArgonContract { - /** - * A list of ids to find if hash belongs to this driver - * or not. - */ - public ids: ArgonContract['ids'] = ['argon2d', 'argon2i', 'argon2id'] - - /** - * A list of params encoded to the hash value. - */ - public params: ArgonContract['params'] = { - iterations: 't', - memory: 'm', - parallelism: 'p', - } - - /** - * The current argon version in use - */ - public version = 19 - - constructor(private config: ArgonConfig) {} - - /** - * Hash a value using argon algorithm. The options can be used to override - * default settings. - */ - public make(value: string) { - return argon2.hash(value, this.config) - } - - /** - * Verifies the hash against a plain value to find if it's - * a valid hash or not. - */ - public verify(hashedValue: string, plainValue: string): Promise { - return argon2.verify(hashedValue, plainValue) - } - - /** - * Returns a boolean telling if the hash needs a rehash or not. The rehash is - * required when - * - * 1. The argon2 version is changed - * 2. Number of iterations are changed. - * 3. The memory value is changed. - * 4. The parellelism value is changed. - * 5. The argon variant is changed. - */ - public needsReHash(value: string): boolean { - const deserialized = phc.deserialize(value) - if (!this.ids.includes(deserialized.id)) { - throw new Error('value is not an argon2 hash') - } - - /** - * Version mis-match - */ - if (deserialized.version !== this.version) { - return true - } - - /** - * Variant mis-match - */ - if (deserialized.id !== `argon2${this.config.variant}`) { - return true - } - - /** - * Check for params mis-match - */ - return !!Object.keys(this.params).find((key) => { - return deserialized.params[this.params[key]] !== this.config![key] - }) - } -} diff --git a/src/Drivers/Bcrypt.ts b/src/Drivers/Bcrypt.ts deleted file mode 100644 index 88a7e4a..0000000 --- a/src/Drivers/Bcrypt.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import phc from '@phc/format' -import bcrypt from 'phc-bcrypt' -import { BcryptConfig, BcryptContract } from '@ioc:Adonis/Core/Hash' - -/** - * Generates and verifies hash using Bcrypt as underlying - * algorigthm. - */ -export class Bcrypt implements BcryptContract { - public ids: BcryptContract['ids'] = ['bcrypt'] - public params: BcryptContract['params'] = { rounds: 'r' } - public version = 98 - - constructor(private config: BcryptConfig) {} - - /** - * Returns hash for a given value - */ - public make(value: string) { - return bcrypt.hash(value, this.config) - } - - /** - * Verify hash to know if two values are same. - */ - public verify(hashedValue: string, plainValue: string): Promise { - return bcrypt.verify(hashedValue, plainValue) - } - - /** - * Returns a boolean telling if hash needs a rehash. Returns true when - * one of the original params have been changed. - */ - public needsReHash(value: string): boolean { - const deserialized = phc.deserialize(value) - - /** - * Phc formatted Bycrpt hash - */ - if (deserialized.id === 'bcrypt') { - if (this.version !== deserialized.version) { - return true - } - - return !!Object.keys(this.params).find((key) => { - return deserialized.params[this.params[key]] !== this.config![key] - }) - } - - /** - * Re-format non phc formatted bcrypt hashes. - */ - if (value.startsWith('$2b') || value.startsWith('$2a')) { - return true - } - - throw new Error('value is not a bcrypt hash') - } -} diff --git a/src/Drivers/Fake.ts b/src/Drivers/Fake.ts deleted file mode 100644 index 4bb7202..0000000 --- a/src/Drivers/Fake.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { FakeContract } from '@ioc:Adonis/Core/Hash' - -/** - * Generates and verifies hash using no algorigthm. - */ -export class Fake implements FakeContract { - public ids: FakeContract['ids'] = ['fake'] - - /** - * Returns hash for a given value - */ - public make(value: string) { - return Promise.resolve(value) - } - - /** - * Verify hash to know if two values are same. - */ - public verify(hashedValue: string, plainValue: string): Promise { - return Promise.resolve(hashedValue === plainValue) - } - - /** - * Returns a boolean telling if hash needs a rehash. Returns true when - * one of the original params have been changed. - */ - public needsReHash(_value: string): boolean { - return false - } -} diff --git a/src/Drivers/Scrypt.ts b/src/Drivers/Scrypt.ts deleted file mode 100644 index f85150e..0000000 --- a/src/Drivers/Scrypt.ts +++ /dev/null @@ -1,221 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import phc from '@phc/format' -import { scrypt, timingSafeEqual } from 'crypto' -import type { BinaryLike, ScryptOptions } from 'crypto' -import type { ScryptConfig, ScryptContract } from '@ioc:Adonis/Core/Hash' - -import { kMaxUint24, randomBytesAsync } from '../utils' - -const defaultConfig = Object.freeze({ - cost: 16384, - blockSize: 8, - parallelization: 1, - saltSize: 16, - keyLength: 64, - maxMemory: 128 * 16384 * 8, -}) - -function scryptAsync( - password: BinaryLike, - salt: BinaryLike, - keyLength: number, - options: ScryptOptions -): Promise { - return new Promise((resolve, reject) => { - scrypt(password, salt, keyLength, options, (error, derivedKey) => { - if (error) { - reject(error) - } else { - resolve(derivedKey) - } - }) - }) -} - -/** - * Hash driver built on top of scrypt hashing algorithm. The driver adheres - * to `phc` string format. - */ -export class Scrypt implements ScryptContract { - /** - * A list of ids to find if hash belongs to this driver - * or not. - */ - public ids: ScryptContract['ids'] = ['scrypt'] - - /** - * A list of params encoded to the hash value. - */ - public params: ScryptContract['params'] = { - cost: 'n', - blockSize: 'r', - parallelization: 'p', - } - - constructor(private readonly config: ScryptConfig) { - // Cost Validation - if (config.cost < 1 || config.cost % 2 !== 0) { - throw new TypeError("The 'cost' option must be a power of 2 greater than 1") - } - - // Parallelization Validation - if (config.parallelization < 1 || config.parallelization > kMaxUint24) { - throw new TypeError( - `The 'parallelism' option must be in the range (1 <= parallelism <= ${kMaxUint24})` - ) - } - - // Memory Validation - const maxMemory = 128 * config.cost * config.blockSize - if (maxMemory > config.maxMemory) { - throw new TypeError( - `The 'maxmem' option must be less than ${maxMemory}, found ${config.maxMemory}` - ) - } - - // Salt Size Validation - if (config.saltSize < 16 || config.saltSize > 1024) { - throw new TypeError("The 'saltSize' option must be in the range (8 <= saltSize <= 1024)") - } - - // Key Length Validation - if (config.keyLength < 64 || config.keyLength > 128) { - throw new TypeError("The 'keylen' option must be in the range (64 <= keylen <= 128)") - } - - this.config = Object.assign({}, defaultConfig, config) - } - - /** - * Hash a value using scrypt algorithm. The options can be used to override - * default settings. - */ - public async make(value: string) { - const salt = await randomBytesAsync(this.config.saltSize) - - const derivedKey = await scryptAsync(value, salt, this.config.keyLength, this.config) - - return phc.serialize({ - id: this.ids[0], - params: { - n: this.config.cost, - r: this.config.blockSize, - p: this.config.parallelization, - }, - salt, - hash: derivedKey, - }) - } - - /** - * Verifies the hash against a plain value to find if it's - * a valid hash or not. The hash must be a valid `phc` string - */ - public async verify(hashedValue: string, plainValue: string): Promise { - let deserializedHash: phc.ParsedHash - - try { - deserializedHash = phc.deserialize(hashedValue) - } catch (error) { - throw new TypeError('The hash must be a valid phc string') - } - - // Identifier Validation - if (!this.ids.includes(deserializedHash.id)) { - throw new TypeError(`Incompatible ${deserializedHash.id} identifier found in the hash`) - } - - // Parameters Existence Validation - if (typeof deserializedHash.params !== 'object') { - throw new TypeError('The param section cannot be empty') - } - - // Cost Validation - if ( - typeof deserializedHash.params.n !== 'number' || - !Number.isInteger(deserializedHash.params.n) - ) { - throw new TypeError("The 'n' param must be an integer") - } - - // Cost Validation - if (deserializedHash.params.n < 1 || deserializedHash.params.n % 2 !== 0) { - throw new TypeError("The 'n' param must be a power of 2 greater than 1") - } - - // Block size Validation - if ( - typeof deserializedHash.params.r !== 'number' || - !Number.isInteger(deserializedHash.params.r) - ) { - throw new TypeError("The 'r' param must be an integer") - } - - // Parallelization Validation - if ( - typeof deserializedHash.params.p !== 'number' || - !Number.isInteger(deserializedHash.params.p) - ) { - throw new TypeError("The 'p' param must be an integer") - } - - // Parallelization Validation - if (deserializedHash.params.p < 1 || deserializedHash.params.p > kMaxUint24) { - throw new TypeError(`The 'p' param must be in the range (1 <= parallelism <= ${kMaxUint24})`) - } - - // Salt Validation - if (typeof deserializedHash.salt === 'undefined') { - throw new TypeError('No salt found in the given string') - } - - // Hash Validation - if (typeof deserializedHash.hash === 'undefined') { - throw new TypeError('No hash found in the given string') - } - - const derivedKey = await scryptAsync( - plainValue, - deserializedHash.salt, - deserializedHash.hash.length, - { - maxmem: this.config.maxMemory, - cost: deserializedHash.params.n, - blockSize: deserializedHash.params.r, - parallelization: deserializedHash.params.p, - } - ) - - return timingSafeEqual(deserializedHash.hash, derivedKey) - } - - /** - * Returns a boolean telling if hash needs a rehash. Returns true when - * one of the original params have been changed. - */ - public needsReHash(hashedValue: string): boolean { - let deserializedHash: phc.ParsedHash - - try { - deserializedHash = phc.deserialize(hashedValue) - } catch (error) { - return true - } - - if (!this.ids.includes(deserializedHash.id)) { - throw new Error('Value is not a scrypt hash') - } - - return Object.keys(this.params).some((key) => { - return deserializedHash.params[this.params[key]] !== this.config![key] - }) - } -} diff --git a/src/Hash/index.ts b/src/Hash/index.ts deleted file mode 100644 index 99ff88d..0000000 --- a/src/Hash/index.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { Manager } from '@poppinss/manager' -import { ManagerConfigValidator } from '@poppinss/utils' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -import { - HashConfig, - FakeContract, - HashContract, - HashDriverContract, - HashersList, -} from '@ioc:Adonis/Core/Hash' - -/** - * The Hash module exposes the API to hash values using an underlying - * Hash driver. - */ -export class Hash - extends Manager< - ApplicationContract, - HashDriverContract, - HashDriverContract, - { [P in keyof HashersList]: HashersList[P]['implementation'] } - > - implements HashContract -{ - /** - * Reference to fake driver. Created when `Hash.fake` is called - */ - private fakeDriver: FakeContract | undefined - - protected singleton = true - - /** - * A boolean to know, if hash module is running in fake - * mode or not - */ - public get isFaked(): boolean { - return !!this.fakeDriver - } - - constructor(application: ApplicationContract, public config: Config) { - super(application) - this.validateConfig() - } - - /** - * Validate config - */ - private validateConfig() { - const validator = new ManagerConfigValidator(this.config, 'hash', 'config/hash') - validator.validateDefault('default') - validator.validateList('list', 'default') - } - - /** - * Pulling the default driver name from the user config. - */ - protected getDefaultMappingName() { - return this.config.default! - } - - /** - * Returns the config for a mapping - */ - protected getMappingConfig(name: keyof HashersList) { - return this.config.list[name] - } - - /** - * Returns the driver name for a mapping - */ - protected getMappingDriver(name: keyof HashersList): string | undefined { - const config = this.getMappingConfig(name) - return config ? (config as any).driver : undefined - } - - /** - * Creating bcrypt driver. The manager will call this method anytime - * someone will ask for the `bcrypt` driver. - */ - protected createBcrypt(_: string, config: any) { - const { Bcrypt } = require('../Drivers/Bcrypt') - return new Bcrypt(config) - } - - /** - * Creating argon driver. The manager will call this method anytime - * someone will ask for the `argon` driver. - */ - protected createArgon2(_: string, config: any) { - const { Argon } = require('../Drivers/Argon') - return new Argon(config) - } - - /** - * Creating scrypt driver. The manager will call this method anytime - * someone will ask for the `scrypt` driver. - */ - protected createScrypt(_: string, config: any) { - const { Scrypt } = require('../Drivers/Scrypt') - return new Scrypt(config) - } - - /** - * Creating fake driver. The manager will call this method anytime - * someone will ask for the `fake` driver. - */ - protected createFake() { - const { Fake } = require('../Drivers/Fake') - return new Fake() - } - - /** - * Initiate faking hash calls. All methods invoked on the main hash - * module and the underlying drivers will be faked using the - * fake driver. - * - * To restore the fake. Run the `Hash.restore` method. - */ - public fake() { - this.fakeDriver = this.fakeDriver || this.createFake() - } - - /** - * Restore fake - */ - public restore() { - this.fakeDriver = undefined - } - - /** - * Hash value using the default driver - */ - public make(value: string) { - if (this.fakeDriver) { - return this.fakeDriver.make(value) - } - - return (this.use() as any).make(value) - } - - /** - * Verify value using the default driver - */ - public verify(hashedValue: string, plainValue: string) { - if (this.fakeDriver) { - return this.fakeDriver.verify(hashedValue, plainValue) - } - - return (this.use() as any).verify(hashedValue, plainValue) - } - - /** - * Find if value needs to be re-hashed as per the default driver. - */ - public needsReHash(hashedValue: string) { - if (this.fakeDriver) { - return this.fakeDriver.needsReHash(hashedValue) - } - - const driver = this.use() as any - if (typeof driver.needsReHash !== 'function') { - return false - } - - return driver.needsReHash(hashedValue) - } - - /** - * Pull pre-configured driver instance - */ - public use(name?: K): ReturnType { - if (this.fakeDriver) { - return this.fakeDriver as ReturnType - } - - return (name ? super.use(name) : super.use()) as ReturnType - } -} diff --git a/src/define_config.ts b/src/define_config.ts new file mode 100644 index 0000000..2061ffb --- /dev/null +++ b/src/define_config.ts @@ -0,0 +1,46 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { ManagerDriversConfig, HashManagerConfig } from './types.js' +import { InvalidHashConfigException } from './exceptions/invalid_hash_config.js' + +/** + * Define configuration for the hash manager + */ +export function defineConfig>( + config: HashManagerConfig +): HashManagerConfig { + /** + * List should always be provided + */ + if (!config.list) { + throw new InvalidHashConfigException('Missing "list" property in hash config') + } + + /** + * Default property should be provided when list + * has one or more items + */ + if (Object.keys(config.list).length && !config.default) { + throw new InvalidHashConfigException( + 'Missing "default" property in hash config. Specify a default hasher' + ) + } + + /** + * The default hasher should be mentioned in the list + */ + if (config.default && !config.list[config.default]) { + throw new InvalidHashConfigException( + `Missing "list.${String(config.default)}". It is referenced by the "default" property` + ) + } + + return config +} diff --git a/src/drivers/argon.ts b/src/drivers/argon.ts new file mode 100644 index 0000000..4f68774 --- /dev/null +++ b/src/drivers/argon.ts @@ -0,0 +1,335 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type argon2 from 'argon2' +import { safeEqual } from '@poppinss/utils' + +import { PhcFormatter } from '../phc_formatter.js' +import { + MAX_UINT24, + MAX_UINT32, + EnumValidator, + RangeValidator, + randomBytesAsync, +} from '../helpers.js' +import type { ArgonConfig, ArgonVariants, HashDriverContract } from '../types.js' + +/** + * Hash driver built on top of "argon2" hash algorigthm. Under the hood + * we make use of the "argon2" npm package. + * + * The Argon implementation uses the PHC formatting for creating + * and verifying hashes. + * + * ```ts + * const argon = new Argon({}) + * + * await argon.make('secret') + * // $argon2id$v=19$t=3,m=4096,p=1$drxJBWzWahR5tMubp+a1Sw$L/Oh2uw6QKW77i/KQ8eGuOt3ui52hEmmKlu1KBVBxiM + * ``` + */ +export class Argon implements HashDriverContract { + /** + * Lazily loaded argon2 binding. Since it is a peer dependency + * we cannot import it at top level + */ + #binding?: typeof argon2 + + /** + * Config with defaults merged + */ + #config: Required + + /** + * Formatter to serialize and deserialize phc string + */ + #phcFormatter = new PhcFormatter<{ t: number; m: number; p: number }>() + + /** + * Supported variants + */ + #variants: { [K in ArgonVariants]: 0 | 1 | 2 } = { + i: 0, + d: 1, + id: 2, + } + + /** + * A list of supported argon ids + */ + #ids = ['argon2d', 'argon2i', 'argon2id'] + + constructor(config: ArgonConfig) { + this.#config = { + version: 0x13, + variant: 'id', + iterations: 3, + memory: 65536, + parallelism: 4, + saltSize: 16, + hashLength: 32, + ...config, + } + + this.#validateConfig() + } + + /** + * Dynamically importing underlying binding + */ + async #importBinding() { + if (this.#binding) { + return this.#binding + } + + this.#binding = await import('argon2') + return this.#binding + } + + /** + * Validate configuration options + */ + #validateConfig() { + RangeValidator.validate('iterations', this.#config.iterations, [2, MAX_UINT32]) + RangeValidator.validate('parallelism', this.#config.parallelism, [1, MAX_UINT24]) + RangeValidator.validate('memory', this.#config.memory, [ + 8 * this.#config.parallelism, + MAX_UINT32, + ]) + + EnumValidator.validate('variant', this.#config.variant, Object.keys(this.#variants)) + RangeValidator.validate('saltSize', this.#config.saltSize, [8, 1024]) + RangeValidator.validate('hashLength', this.#config.hashLength, [4, MAX_UINT32]) + EnumValidator.validate('version', this.#config.version, [0x10, 0x13]) + + Object.freeze(this.#config) + } + + /** + * Validate phc hash string + */ + #validatePhcString(phcString: string) { + const phcNode = this.#phcFormatter.deserialize(phcString) + + /** + * Old argon strings without version + */ + if (!phcNode.version) { + phcNode.version = 0x10 + } + + /** + * Validate top level properties to exist + */ + if (!phcNode.params) { + throw new TypeError(`No "params" found in the phc string`) + } + if (!phcNode.salt) { + throw new TypeError(`No "salt" found in the phc string`) + } + if (!phcNode.hash) { + throw new TypeError(`No "hash" found in the phc string`) + } + RangeValidator.validate('salt.byteLength', phcNode.salt.byteLength, [8, 1024]) + RangeValidator.validate('hash.byteLength', phcNode.hash.byteLength, [4, MAX_UINT32]) + + /** + * Validate id + */ + EnumValidator.validate('id', phcNode.id, this.#ids) + + /** + * Validate variant and extract it + */ + const variant = phcNode.id.split('argon2')[1] as ArgonVariants + EnumValidator.validate('variant', variant, Object.keys(this.#variants)) + + /** + * Validate rest of the properties + */ + EnumValidator.validate('version', phcNode.version, [0x10, 0x13]) + RangeValidator.validate('t', phcNode.params.t, [1, MAX_UINT32]) + RangeValidator.validate('p', phcNode.params.p, [1, MAX_UINT24]) + RangeValidator.validate('m', phcNode.params.m, [8 * phcNode.params.p, MAX_UINT32]) + + return { + id: phcNode.id, + version: phcNode.version!, + hash: phcNode.hash, + salt: phcNode.salt, + params: { + t: phcNode.params.t, + m: phcNode.params.m, + p: phcNode.params.p, + }, + variant: variant, + } + } + + /** + * Check if the value is a valid hash. This method just checks + * for the formatting of the hash. + * + * ```ts + * argon.isValidHash('hello world') // false + * argon.isValidHash('$argon2id$v=19$t=3,m=4096,p=1$drxJBWzWahR5tMubp+a1Sw$L/Oh2uw6QKW77i/KQ8eGuOt3ui52hEmmKlu1KBVBxiM') + * ``` + */ + isValidHash(value: string): boolean { + try { + this.#validatePhcString(value) + return true + } catch { + return false + } + } + + /** + * Hash a plain text value + * + * ```ts + * const hash = await argon.make('password') + * ``` + */ + async make(value: string) { + const driver = await this.#importBinding() + const salt = await randomBytesAsync(this.#config.saltSize) + + /** + * Generate hash + */ + const hash = await driver.hash(value, { + salt, + version: this.#config.version, + type: this.#variants[this.#config.variant], + timeCost: this.#config.iterations, + memoryCost: this.#config.memory, + parallelism: this.#config.parallelism, + hashLength: this.#config.hashLength, + raw: true, + }) + + /** + * Serialize hash + */ + return this.#phcFormatter.serialize(salt, hash, { + id: `argon2${this.#config.variant}`, + version: this.#config.version, + params: { + t: this.#config.iterations, + m: this.#config.memory, + p: this.#config.parallelism, + }, + }) + } + + /** + * Verify the plain text value against an existing hash + * + * ```ts + * if (await argon.verify(hash, plainText)) { + * + * } + * ``` + */ + async verify(hashedValue: string, plainValue: string): Promise { + const driver = await this.#importBinding() + + try { + /** + * De-serialize hash and ensure all Phc string properties + * to exist. + */ + const phcNode = this.#validatePhcString(hashedValue) + + /** + * Generate a new hash with the same properties + * as the existing hash + */ + const newHash = await driver.hash(plainValue, { + salt: phcNode.salt, + version: phcNode.version, + type: this.#variants[phcNode.variant], + timeCost: phcNode.params.t, + memoryCost: phcNode.params.m, + parallelism: phcNode.params.p, + hashLength: phcNode.hash.byteLength, + raw: true, + }) + + /** + * Ensure both are equal + */ + return safeEqual(newHash, phcNode.hash) + } catch { + return false + } + } + + /** + * Find if the hash value needs a rehash or not. The rehash is + * required when. + * + * 1. The argon2 version is changed + * 2. Number of iterations are changed + * 3. The memory value is changed + * 4. The parellelism value is changed + * 5. The argon variant is changed + * + * ```ts + * const isValid = await argon.verify(hash, plainText) + * + * // Plain password is valid and hash needs a rehash + * if (isValid && await argon.needsReHash(hash)) { + * const newHash = await argon.make(plainText) + * } + * ``` + */ + needsReHash(value: string): boolean { + const phcNode = this.#phcFormatter.deserialize(value) + if (!this.#ids.includes(phcNode.id)) { + throw new TypeError('Value is not a valid argon hash') + } + + /** + * If config version is separate from hash version, then a + * re-hash is needed + */ + if (phcNode.version !== this.#config.version) { + return true + } + + /** + * If config variant is not same as the hash variant, then a + * re-hash is needed + */ + if (phcNode.id !== `argon2${this.#config.variant}`) { + return true + } + + /** + * Make sure all the encoded params are same as the config. + * Otherwise a re-hash is needed + */ + if (!phcNode.params) { + return true + } + if (phcNode.params.m !== this.#config.memory) { + return true + } + if (phcNode.params.t !== this.#config.iterations) { + return true + } + if (phcNode.params.p !== this.#config.parallelism) { + return true + } + + return false + } +} diff --git a/src/drivers/bcrypt.ts b/src/drivers/bcrypt.ts new file mode 100644 index 0000000..c03f005 --- /dev/null +++ b/src/drivers/bcrypt.ts @@ -0,0 +1,272 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// @ts-expect-error +import * as bcryptBase64 from '../legacy/bcrypt_base64.cjs' + +import type bcrypt from 'bcrypt' +import { safeEqual } from '@poppinss/utils' +import { PhcFormatter } from '../phc_formatter.js' +import type { HashDriverContract, BcryptConfig } from '../types.js' +import { EnumValidator, randomBytesAsync, RangeValidator } from '../helpers.js' + +/** + * Hash driver built on top of "bcrypt" hash algorigthm. Under the hood + * we make use of the "bcrypt" npm package. + * + * The Bcrypt implementation uses the PHC formatting for creating + * and verifying hashes. + * + * ```ts + * const bcrypt = new Bcrypt({}) + * + * await bcrypt.make('secret') + * // $bcrypt$v=98$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8 + * ``` + */ +export class Bcrypt implements HashDriverContract { + /** + * Lazily loaded bcrypt binding. Since it is a peer dependency + * we cannot import it at top level + */ + #binding?: typeof bcrypt + + /** + * Config with defaults merged + */ + #config: Required + + /** + * Formatter to serialize and deserialize phc string + */ + #phcFormatter = new PhcFormatter<{ r: number }>() + + constructor(config: BcryptConfig) { + this.#config = { + rounds: 10, + saltSize: 16, + version: 0x62, + ...config, + } + + this.#validateConfig() + } + + /** + * Dynamically importing underlying binding + */ + async #importBinding() { + if (this.#binding) { + return this.#binding + } + + this.#binding = await import('bcrypt') + return this.#binding + } + + /** + * Generates salt for bcrypt + */ + #generateBcryptSalt(salt: Buffer, version: number, rounds: number) { + const bcryptVersionCharCode = String.fromCharCode(version) + const paddedRounds = rounds > 9 ? `${rounds}` : `0${rounds}` + return `$2${bcryptVersionCharCode}$${paddedRounds}$${bcryptBase64.encode(salt)}` + } + + /** + * Validate config + */ + #validateConfig() { + RangeValidator.validate('rounds', this.#config.rounds, [4, 31]) + RangeValidator.validate('saltSize', this.#config.saltSize, [8, 1024]) + EnumValidator.validate('version', this.#config.version, [0x61, 0x62]) + Object.freeze(this.#config) + } + + /** + * Validate phc hash string + */ + #validatePhcString(phcString: string) { + const phcNode = this.#phcFormatter.deserialize(phcString) + + /** + * Old bcrypt strings without version + */ + if (!phcNode.version) { + phcNode.version = 0x61 + } + + /** + * Validate top level properties to exist + */ + if (phcNode.id !== 'bcrypt') { + throw new TypeError(`Invalid "id" found in the phc string`) + } + if (!phcNode.params) { + throw new TypeError(`No "params" found in the phc string`) + } + if (!phcNode.salt) { + throw new TypeError(`No "salt" found in the phc string`) + } + if (!phcNode.hash) { + throw new TypeError(`No "hash" found in the phc string`) + } + if (!phcNode.hash.byteLength) { + throw new TypeError(`No "hash" found in the phc string`) + } + RangeValidator.validate('salt.byteLength', phcNode.salt.byteLength, [8, 1024]) + + /** + * Validate rest of the properties + */ + EnumValidator.validate('version', phcNode.version, [0x61, 0x62]) + RangeValidator.validate('r', phcNode.params.r, [4, 31]) + + return { + id: phcNode.id, + version: phcNode.version!, + hash: phcNode.hash, + salt: phcNode.salt, + params: { + r: phcNode.params.r, + }, + } + } + + /** + * Check if the value is a valid hash. This method just checks + * for the formatting of the hash. + * + * ```ts + * bcrypt.isValidHash('hello world') // false + * bcrypt.isValidHash('$bcrypt$v=98$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8') + * ``` + */ + isValidHash(value: string): boolean { + try { + this.#validatePhcString(value) + return true + } catch { + return false + } + } + + /** + * Hash a plain text value + * + * ```ts + * const hash = await bcrypt.make('password') + * ``` + */ + async make(value: string) { + const driver = await this.#importBinding() + + /** + * Generate salt including bcrypt formatted salt + */ + const salt = await randomBytesAsync(this.#config.saltSize) + const bcryptSalt = this.#generateBcryptSalt(salt, this.#config.version, this.#config.rounds) + + /** + * Generate hash + */ + const bcryptHash = await driver.hash(value, bcryptSalt) + const hash = bcryptBase64.decode(bcryptHash.split(bcryptSalt)[1]) + + return this.#phcFormatter.serialize(salt, hash, { + id: 'bcrypt', + version: this.#config.version, + params: { + r: this.#config.rounds, + }, + }) + } + + /** + * Verify the plain text value against an existing hash + * + * ```ts + * if (await bcrypt.verify(hash, plainText)) { + * + * } + * ``` + */ + async verify(hashedValue: string, plainValue: string): Promise { + const driver = await this.#importBinding() + + try { + if (hashedValue.startsWith('$2b') || hashedValue.startsWith('$2a')) { + return await driver.compare(plainValue, hashedValue) + } + + /** + * De-serialize hash and ensure all Phc string properties + * to exist. + */ + const phcNode = this.#validatePhcString(hashedValue) + const bcryptSalt = this.#generateBcryptSalt(phcNode.salt, phcNode.version, phcNode.params.r) + + const bcryptHash = await driver.hash(plainValue, bcryptSalt) + const hash = bcryptBase64.decode(bcryptHash.split(bcryptSalt)[1]) + + return safeEqual(hash, phcNode.hash) + } catch { + return false + } + } + + /** + * Find if the hash value needs a rehash or not. The rehash is + * required when. + * + * 1. The bcrypt version is changed + * 2. Number of rounds are changed + * 3. Bcrypt hash is not using MCF hash format + * + * ```ts + * const isValid = await bcrypt.verify(hash, plainText) + * + * // Plain password is valid and hash needs a rehash + * if (isValid && await bcrypt.needsReHash(hash)) { + * const newHash = await bcrypt.make(plainText) + * } + * ``` + */ + needsReHash(value: string): boolean { + if (value.startsWith('$2b') || value.startsWith('$2a')) { + return true + } + + const phcNode = this.#phcFormatter.deserialize(value) + if (phcNode.id !== 'bcrypt') { + throw new TypeError('Value is not a valid bcrypt hash') + } + + /** + * If config version is separate from hash version, then a + * re-hash is needed + */ + if (phcNode.version !== this.#config.version) { + return true + } + + /** + * Make sure all the encoded params are same as the config. + * Otherwise a re-hash is needed + */ + if (!phcNode.params) { + return true + } + if (phcNode.params.r !== this.#config.rounds) { + return true + } + + return false + } +} diff --git a/src/drivers/fake.ts b/src/drivers/fake.ts new file mode 100644 index 0000000..cb6ae41 --- /dev/null +++ b/src/drivers/fake.ts @@ -0,0 +1,48 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { HashDriverContract } from '../types.js' + +/** + * The fake implementation does not generate any hash and + * performs verification using the plain text equality + * check. + * + * The fake driver is useful for testing. + */ +export class Fake implements HashDriverContract { + /** + * Always returns true + */ + isValidHash(_: string): boolean { + return true + } + + /** + * Returns the value as it is + */ + async make(value: string) { + return value + } + + /** + * Checks the hash and the plain text value using + * equality check + */ + async verify(hashedValue: string, plainValue: string): Promise { + return hashedValue === plainValue + } + + /** + * Always returns false + */ + needsReHash(_: string): boolean { + return false + } +} diff --git a/src/drivers/scrypt.ts b/src/drivers/scrypt.ts new file mode 100644 index 0000000..097755a --- /dev/null +++ b/src/drivers/scrypt.ts @@ -0,0 +1,264 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { safeEqual } from '@poppinss/utils' + +import { PhcFormatter } from '../phc_formatter.js' +import type { ScryptConfig, HashDriverContract } from '../types.js' +import { randomBytesAsync, RangeValidator, scryptAsync, MAX_UINT32 } from '../helpers.js' + +/** + * Hash driver built on top of "scrypt" hash algorigthm. Under the hood + * we make use of the Node.js crypto module + * + * The Scrypt implementation uses the PHC formatting for creating + * and verifying hashes. + * + * ```ts + * const scrypt = new Scrypt({}) + * + * await scrypt.make('secret') + * // $scrypt$n=16384,r=8,p=1$iILKD1gVSx6bqualYqyLBQ$DNzIISdmTQS6sFdQ1tJ3UCZ7Uun4uGHNjj0x8FHOqB0pf2LYsu9Xaj5MFhHg21qBz8l5q/oxpeV+ZkgTAj+OzQ + * ``` + */ +export class Scrypt implements HashDriverContract { + /** + * Config with defaults merged + */ + #config: Required + + /** + * Formatter to serialize and deserialize phc string + */ + #phcFormatter = new PhcFormatter<{ + n: number + r: number + p: number + }>() + + constructor(config: ScryptConfig) { + this.#config = { + cost: 16384, + blockSize: 8, + parallelization: 1, + saltSize: 16, + keyLength: 64, + maxMemory: 32 * 1024 * 1024, + ...config, + } + + this.#validateConfig() + } + + /** + * Validate config + */ + #validateConfig() { + RangeValidator.validate('blockSize', this.#config.blockSize, [1, MAX_UINT32]) + RangeValidator.validate('cost', this.#config.cost, [2, MAX_UINT32]) + RangeValidator.validate('parallelization', this.#config.parallelization, [ + 1, + Math.floor(((Math.pow(2, 32) - 1) * 32) / (128 * this.#config.blockSize)), + ]) + + RangeValidator.validate('saltSize', this.#config.saltSize, [8, 1024]) + RangeValidator.validate('keyLength', this.#config.keyLength, [64, 128]) + RangeValidator.validate('maxMemory', this.#config.maxMemory, [ + 128 * this.#config.cost * this.#config.blockSize + 1, + MAX_UINT32, + ]) + + Object.freeze(this.#config) + } + + /** + * Validate phc hash string + */ + #validatePhcString(phcString: string) { + const phcNode = this.#phcFormatter.deserialize(phcString) + + /** + * Validate top level properties to exist + */ + if (phcNode.id !== 'scrypt') { + throw new TypeError(`Invalid "id" found in the phc string`) + } + if (!phcNode.params) { + throw new TypeError(`No "params" found in the phc string`) + } + if (!phcNode.salt) { + throw new TypeError(`No "salt" found in the phc string`) + } + if (!phcNode.hash) { + throw new TypeError(`No "hash" found in the phc string`) + } + RangeValidator.validate('hash.byteLength', phcNode.hash.byteLength, [64, 128]) + RangeValidator.validate('salt.byteLength', phcNode.salt.byteLength, [8, 1024]) + + /** + * blockSize + */ + RangeValidator.validate('r', phcNode.params.r, [1, MAX_UINT32]) + + /** + * Cost + */ + RangeValidator.validate('n', phcNode.params.n, [1, MAX_UINT32]) + + /** + * Parallelization + */ + RangeValidator.validate('p', phcNode.params.p, [ + 1, + Math.floor(((Math.pow(2, 32) - 1) * 32) / (128 * phcNode.params.r)), + ]) + + return { + id: phcNode.id, + hash: phcNode.hash, + salt: phcNode.salt, + params: { + r: phcNode.params.r, + n: phcNode.params.n, + p: phcNode.params.p, + }, + } + } + + /** + * Check if the value is a valid hash. This method just checks + * for the formatting of the hash. + * + * ```ts + * scrypt.isValidHash('hello world') // false + * scrypt.isValidHash('$scrypt$n=16384,r=8,p=1$iILKD1gVSx6bqualYqyLBQ$DNzIISdmTQS6sFdQ1tJ3UCZ7Uun4uGHNjj0x8FHOqB0pf2LYsu9Xaj5MFhHg21qBz8l5q/oxpeV+ZkgTAj+OzQ') + * ``` + */ + isValidHash(value: string): boolean { + try { + this.#validatePhcString(value) + return true + } catch { + return false + } + } + + /** + * Hash a plain text value + * + * ```ts + * const hash = await scrypt.make('password') + * ``` + */ + async make(value: string) { + const salt = await randomBytesAsync(this.#config.saltSize) + + /** + * Generate hash + */ + const hash = await scryptAsync(value, salt, this.#config.keyLength, { + cost: this.#config.cost, + blockSize: this.#config.blockSize, + parallelization: this.#config.parallelization, + maxmem: this.#config.maxMemory, + }) + + /** + * Serialize hash + */ + return this.#phcFormatter.serialize(salt, hash, { + id: 'scrypt', + params: { + n: this.#config.cost, + r: this.#config.blockSize, + p: this.#config.parallelization, + }, + }) + } + + /** + * Verify the plain text value against an existing hash + * + * ```ts + * if (await scrypt.verify(hash, plainText)) { + * + * } + * ``` + */ + async verify(hashedValue: string, plainValue: string): Promise { + try { + /** + * De-serialize hash and ensure all Phc string properties + * to exist. + */ + const phcNode = this.#validatePhcString(hashedValue) + + /** + * Generate a new hash with the same properties + * as the existing hash + */ + const newHash = await scryptAsync(plainValue, phcNode.salt, phcNode.hash.byteLength, { + cost: phcNode.params.n, + blockSize: phcNode.params.r, + parallelization: phcNode.params.p, + maxmem: this.#config.maxMemory, + }) + + /** + * Ensure both are equal + */ + return safeEqual(newHash, phcNode.hash) + } catch { + return false + } + } + + /** + * Find if the hash value needs a rehash or not. The rehash is + * required when. + * + * 1. The cost value is changed + * 2. The blockSize value is changed + * 3. The parallelization value is changed + * + * ```ts + * const isValid = await scrypt.verify(hash, plainText) + * + * // Plain password is valid and hash needs a rehash + * if (isValid && await scrypt.needsReHash(hash)) { + * const newHash = await scrypt.make(plainText) + * } + * ``` + */ + needsReHash(value: string): boolean { + const phcNode = this.#phcFormatter.deserialize(value) + if (phcNode.id !== 'scrypt') { + throw new TypeError('Value is not a valid scrypt hash') + } + + /** + * Make sure all the encoded params are same as the config. + * Otherwise a re-hash is needed + */ + if (!phcNode.params) { + return true + } + if (phcNode.params.n !== this.#config.cost) { + return true + } + if (phcNode.params.r !== this.#config.blockSize) { + return true + } + if (phcNode.params.p !== this.#config.parallelization) { + return true + } + + return false + } +} diff --git a/src/exceptions/invalid_hash_config.ts b/src/exceptions/invalid_hash_config.ts new file mode 100644 index 0000000..46554e7 --- /dev/null +++ b/src/exceptions/invalid_hash_config.ts @@ -0,0 +1,18 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Exception } from '@poppinss/utils' + +/** + * Exception raised when the hash config is invalid + */ +export class InvalidHashConfigException extends Exception { + static code = 'E_INVALID_HASH_CONFIG' + static status = 500 +} diff --git a/src/hash.ts b/src/hash.ts new file mode 100644 index 0000000..79cd237 --- /dev/null +++ b/src/hash.ts @@ -0,0 +1,58 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { HashDriverContract } from './types.js' + +/** + * Hash and verify values using a dedicated hash driver. The Hash + * works as an adapter across different drivers. + * + * ```ts + * const hash = new Hash(new Argon()) + * const hashedPassword = await hash.make('secret') + * + * const isValid = await hash.verify(hashedPassword, 'secret') + * console.log(isValid) + * ``` + */ +export class Hash implements HashDriverContract { + #driver: HashDriverContract + constructor(driver: HashDriverContract) { + this.#driver = driver + } + + /** + * Check if the value is a valid hash. This method just checks + * for the formatting of the hash + */ + isValidHash(value: string): boolean { + return this.#driver.isValidHash(value) + } + + /** + * Hash plain text value + */ + make(value: string): Promise { + return this.#driver.make(value) + } + + /** + * Verify the plain text value against an existing hash + */ + verify(hashedValue: string, plainValue: string): Promise { + return this.#driver.verify(hashedValue, plainValue) + } + + /** + * Find if the hash value needs a rehash or not. + */ + needsReHash(hashedValue: string): boolean { + return this.#driver.needsReHash(hashedValue) + } +} diff --git a/src/hash_manager.ts b/src/hash_manager.ts new file mode 100644 index 0000000..6f0da7f --- /dev/null +++ b/src/hash_manager.ts @@ -0,0 +1,202 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Hash } from './hash.js' +import { Argon } from './drivers/argon.js' +import { Bcrypt } from './drivers/bcrypt.js' +import { Scrypt } from './drivers/scrypt.js' +import type { + HashDriverContract, + HashManagerConfig, + HashManagerDrivers, + ManagerDriverFactory, + ManagerDriversConfig, +} from './types.js' +import { Fake } from './drivers/fake.js' + +/** + * HashManager implements the manager/builder pattern to create a use multiple + * hashing algorithm without self managing hash instance. + * + * ```ts + * const manager = new HashManager({ + * default: 'argon', + * list: { + * argon: { + * driver: 'argon2', + * }, + * bcrypt: { + * driver: 'bcrypt', + * } + * } + * }) + * ``` + */ +export class HashManager> + implements HashDriverContract +{ + /** + * Hash manager config with the list of hashers in + * use + */ + #config: HashManagerConfig + + /** + * Fake hasher + */ + #fakeHasher?: Hash + + /** + * Cache of hashers + */ + #hashersCache: Partial> = {} + + /** + * Drivers implementations. Cannot be async, since the "use" + * method is not async + */ + #drivers: { [Driver in keyof HashManagerDrivers]?: ManagerDriverFactory } = { + bcrypt: (config) => new Bcrypt(config), + argon2: (config) => new Argon(config), + scrypt: (config) => new Scrypt(config), + } + + constructor(config: HashManagerConfig) { + this.#config = config + } + + /** + * Creates an instance of a hash driver + */ + #createDriver( + name: Driver, + config: { driver: Driver } & HashManagerDrivers[Driver]['config'] + ): HashManagerDrivers[Driver]['implementation'] { + /** + * Ensure the driver exists + */ + const driver = this.#drivers[name] + if (!driver) { + throw new Error( + `Unknown hash driver "${name}". Make sure the driver is registered with HashManager` + ) + } + + /** + * Use cache or create an instance of the driver + */ + return driver(config) + } + + /** + * Use one of the registered hashers to hash values. + * + * ```ts + * manager.use() // returns default hasher + * manager.use('argon') + * ``` + */ + use(hasher?: Hasher): Hash { + let hasherToUse: keyof KnownHashers | undefined = hasher || this.#config.default + if (!hasherToUse) { + throw new Error('Cannot create hash instance. No default hasher is defined in the config') + } + + /** + * Use fake hasher if exists + */ + if (this.#fakeHasher) { + return this.#fakeHasher + } + + const config = this.#config.list[hasherToUse] + + /** + * Use cached copy if exists + */ + const cachedHasher = this.#hashersCache[config.driver] + if (cachedHasher) { + return cachedHasher + } + + /** + * Create a new instance of Hash class with the selected + * driver and cache it + */ + const hash = new Hash(this.#createDriver(config.driver, config)) + this.#hashersCache[config.driver] = hash + return hash + } + + /** + * Fake hash drivers to disable hashing values + */ + fake(): void { + if (!this.#fakeHasher) { + this.#fakeHasher = new Hash(new Fake()) + } + } + + /** + * Restore fake + */ + restore() { + this.#fakeHasher = undefined + } + + /** + * Extend manager to add custom drivers. The driver typings must be + * registered first (if using typescript). + * + * ```ts + * manager.extend('bcrypt', (config) => { + * return new Bcrypt(config) + * }) + * ``` + */ + extend( + driver: Driver, + factory: ManagerDriverFactory + ) { + /** + * Using any because of this issue + * https://github.com/microsoft/TypeScript/issues/13995 + */ + this.#drivers[driver] = factory as any + } + + /** + * Check if the value is a valid hash. This method just checks + * for the formatting of the hash + */ + isValidHash(value: string): boolean { + return this.use().isValidHash(value) + } + + /** + * Hash plain text value + */ + make(value: string): Promise { + return this.use().make(value) + } + + /** + * Verify the plain text value against an existing hash + */ + verify(hashedValue: string, plainValue: string): Promise { + return this.use().verify(hashedValue, plainValue) + } + + /** + * Find if the hash value needs a rehash or not. + */ + needsReHash(hashedValue: string): boolean { + return this.use().needsReHash(hashedValue) + } +} diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..3357401 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,54 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { promisify } from 'node:util' +import { randomBytes, scrypt, type ScryptOptions } from 'node:crypto' + +export const MAX_UINT32 = 2 ** 32 - 1 +export const MAX_UINT24 = 2 ** 24 - 1 + +/** + * Validates a number to be within a given range. + */ +export class RangeValidator { + static validate(label: string, value: unknown, range: [number, number]) { + if (typeof value !== 'number' || !Number.isInteger(value)) { + throw new TypeError(`The "${label}" option must be an integer`) + } + + const [min, max] = range + + if (value < min || value > max) { + throw new TypeError( + `The "${label}" option must be in the range (${min} <= ${label} <= ${max})` + ) + } + } +} + +/** + * Validates a value to be one of the allowed values + */ +export class EnumValidator { + static validate(label: string, value: unknown, allowedValues: any[]) { + if (!allowedValues.includes(value)) { + throw new TypeError(`The "${label}" option must be one of: ${allowedValues}`) + } + } +} + +/** + * Async function to generate random bytes + */ +export const randomBytesAsync = promisify(randomBytes) + +/** + * Async version of scrypt. + */ +export const scryptAsync = promisify(scrypt) diff --git a/src/legacy/bcrypt_base64.cjs b/src/legacy/bcrypt_base64.cjs new file mode 100644 index 0000000..8fa7169 --- /dev/null +++ b/src/legacy/bcrypt_base64.cjs @@ -0,0 +1,103 @@ +/* eslint-disable capitalized-comments,unicorn/number-literal-case,unicorn/no-abusive-eslint-disable */ + +/** + * Bcrypt does not use standard base64 encoding. + * https://hackernoon.com/the-bcrypt-protocol-is-kind-of-a-mess-4aace5eb31bd + */ + +/** + * bcrypt's own non-standard base64 dictionary. + **/ +const BASE64_CODE = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split('') + +/* eslint-disable prettier/prettier */ +const BASE64_INDEX = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + -1, -1, -1, -1, -1, +] +/* eslint-enable prettier/prettier */ + +/** + * Encodes a Buffer to base64 using the bcrypt's base64 dictionary. + * @param {Buffer} buff Buffer to encode. + * @returns {string} The buffer encoded as a string. + */ +function encode(buff) { + const len = buff.byteLength + let off = 0 + const stra = [] + + while (off < len) { + let c1 = buff[off++] & 0xff + stra.push(BASE64_CODE[(c1 >> 2) & 0x3f]) + c1 = (c1 & 0x03) << 4 + if (off >= len) { + stra.push(BASE64_CODE[c1 & 0x3f]) + break + } + let c2 = buff[off++] & 0xff + c1 |= (c2 >> 4) & 0x0f + stra.push(BASE64_CODE[c1 & 0x3f]) + c1 = (c2 & 0x0f) << 2 + if (off >= len) { + stra.push(BASE64_CODE[c1 & 0x3f]) + break + } + c2 = buff[off++] & 0xff + c1 |= (c2 >> 6) & 0x03 + stra.push(BASE64_CODE[c1 & 0x3f]) + stra.push(BASE64_CODE[c2 & 0x3f]) + } + return stra.join('') +} + +/** + * Decodes a base64 encoded string using the bcrypt's base64 dictionary. + * @param {string} str String to decode. + * @returns {Buffer} The string decoded as a Buffer. + * @inner + */ +function decode(str) { + let off = 0 + let olen = 0 + const slen = str.length + const stra = [] + const len = str.length + + while (off < slen - 1 && olen < len) { + let code = str.charCodeAt(off++) + const c1 = code < BASE64_INDEX.length ? BASE64_INDEX[code] : -1 + code = str.charCodeAt(off++) + const c2 = code < BASE64_INDEX.length ? BASE64_INDEX[code] : -1 + if (c1 === -1 || c2 === -1) break + let o = (c1 << 2) >>> 0 + o |= (c2 & 0x30) >> 4 + stra.push(String.fromCharCode(o)) + if (++olen >= len || off >= slen) break + code = str.charCodeAt(off++) + const c3 = code < BASE64_INDEX.length ? BASE64_INDEX[code] : -1 + if (c3 === -1) break + o = ((c2 & 0x0f) << 4) >>> 0 + o |= (c3 & 0x3c) >> 2 + stra.push(String.fromCharCode(o)) + if (++olen >= len || off >= slen) break + code = str.charCodeAt(off++) + const c4 = code < BASE64_INDEX.length ? BASE64_INDEX[code] : -1 + o = ((c3 & 0x03) << 6) >>> 0 + o |= c4 + stra.push(String.fromCharCode(o)) + ++olen + } + const buffa = [] + for (off = 0; off < olen; off++) buffa.push(stra[off].charCodeAt(0)) + return Buffer.from(buffa) +} + +module.exports = { + encode, + decode, +} diff --git a/src/phc_formatter.ts b/src/phc_formatter.ts new file mode 100644 index 0000000..36f0222 --- /dev/null +++ b/src/phc_formatter.ts @@ -0,0 +1,44 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// @ts-expect-error +import phc from '@phc/format' +import { PhcNode } from './types.js' + +/** + * Phc formatter is used to serialize a hash to a phc string and + * deserialize it back to a PHC object. + */ +export class PhcFormatter< + Params extends Record = Record +> { + /** + * Serialize salt and hash with predefined options. + */ + serialize( + salt: Buffer, + hash: Buffer, + options: { id: string; params?: Params; version?: number | string } + ): string { + return phc.serialize({ + id: options.id, + version: options.version, + params: options.params, + salt, + hash, + }) + } + + /** + * Deserialize a PHC string to an object + */ + deserialize(phcString: string): PhcNode { + return phc.deserialize(phcString) + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..47c2337 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,233 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { Argon } from './drivers/argon.js' +import type { Bcrypt } from './drivers/bcrypt.js' +import type { Scrypt } from './drivers/scrypt.js' + +/** + * The contract Hash drivers should adhere to + */ +export interface HashDriverContract { + /** + * Check if the value is a valid hash. This method just checks + * for the formatting of the hash + */ + isValidHash(value: string): boolean + + /** + * Hash plain text value + */ + make(value: string): Promise + + /** + * Verify the plain text value against an existing hash + */ + verify(hashedValue: string, plainValue: string): Promise + + /** + * Find if the hash value needs a rehash or not. + */ + needsReHash(hashedValue: string): boolean +} + +/** + * Shape of deserialized phc node + */ +export type PhcNode< + Params extends Record = Record +> = { + id: string + salt: Buffer + hash: Buffer + version?: number + params?: Params +} + +/** + * Available argon variants + */ +export type ArgonVariants = 'd' | 'i' | 'id' + +/** + * Shape of argon2 config + */ +export type ArgonConfig = { + /** + * The argon hash type to use. + * https://github.com/ranisalt/node-argon2/wiki/Options#type + * + * Default is id + */ + variant?: ArgonVariants + + /** + * The argon2 version to use. The latest version is better. + * + * Default is 19 + */ + version?: 0x10 | 0x13 + + /** + * Iterations increases hash strength at the cost of time + * required to compute. + * https://github.com/ranisalt/node-argon2/wiki/Options#timecost + * + * Default is 3 + */ + iterations?: number + + /** + * The amount of memory to be used by the hash function, in KiB + * https://github.com/ranisalt/node-argon2/wiki/Options#memorycost. + * + * Default is 65536 + */ + memory?: number + + /** + * The amount of threads to compute the hash on + * https://github.com/ranisalt/node-argon2/wiki/Options#parallelism. + * + * Default is 4 + */ + parallelism?: number + + /** + * The size (in bytes) for the auto generated hash salt. + * + * Default is 16 + */ + saltSize?: number + + /** + * Maximum length for the raw hash in bytes. The serialized output will always + * be longer than the raw hash. + * + * Default is 32 + */ + hashLength?: number +} + +/** + * Shape of bcrypt config + */ +export type BcryptConfig = { + /** + * The cost of processing the data + * https://www.npmjs.com/package/bcrypt#a-note-on-rounds + * + * Default is 10 + */ + rounds?: number + + /** + * The size (in bytes) for the auto generated hash salt. + * + * Default is 16 + */ + saltSize?: number + + /** + * The bcrypt version to use. The latest version is better + * + * Default is 98 + */ + version?: 0x61 | 0x62 +} + +/** + * Shape of scrypt config + */ +export type ScryptConfig = { + /** + * CPU/memory cost parameter. Must be a power of two greater than one. + * + * Default is 16384 + */ + cost?: number + + /** + * Block size parameter. + * + * Default is 8 + */ + blockSize?: number + + /** + * Parallelization parameter. + * + * Default is 1 + */ + parallelization?: number + + /** + * Size of the salt. + * + * Default is 16 + */ + saltSize?: number + + /** + * Memory upper bound. + * + * Default is 33554432 + */ + maxMemory?: number + + /** + * Desired key length in bytes. + * + * Default is 64 + */ + keyLength?: number +} + +/** + * Known hash drivers. One can extend the interface to add + * custom drivers as well + */ +export interface HashManagerDrivers { + bcrypt: { + config: BcryptConfig + implementation: Bcrypt + } + argon2: { + config: ArgonConfig + implementation: Argon + } + scrypt: { + config: ScryptConfig + implementation: Scrypt + } +} + +/** + * Union of config extracted from known hash drivers + */ +export type ManagerDriversConfig = { + [K in keyof HashManagerDrivers]: { driver: K } & HashManagerDrivers[K]['config'] +}[keyof HashManagerDrivers] + +/** + * Factory function to return the driver implementation. The method + * cannot be async, because the API that calls this method is not + * async in first place. + */ +export type ManagerDriverFactory = ( + config: { driver: K } & HashManagerDrivers[K]['config'] +) => HashManagerDrivers[K]['implementation'] + +/** + * Config accepted by the hash manager + */ +export type HashManagerConfig> = { + default?: keyof KnownHashers + list: KnownHashers +} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 3828696..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { randomBytes } from 'crypto' - -export const kMaxUint24 = 16777215 // 2**24 - 1 -export const kMaxUint31 = 2147483647 // 2**31 - 1 - -export function randomBytesAsync(size: number): Promise { - if (size < 0 || size > kMaxUint31) { - return Promise.reject( - new TypeError(`The 'length' parameter must be in the range (0 <= length <= ${kMaxUint31})`) - ) - } - - return new Promise((resolve, reject) => { - randomBytes(size, (error, buffer) => { - if (error) { - reject(error) - } else { - resolve(buffer) - } - }) - }) -} diff --git a/standalone.ts b/standalone.ts deleted file mode 100644 index b943d64..0000000 --- a/standalone.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -export { Hash } from './src/Hash' diff --git a/test-helpers/index.ts b/test-helpers/index.ts deleted file mode 100644 index cb710c6..0000000 --- a/test-helpers/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Scrypt } from '../src/Drivers/Scrypt' -import type { ScryptConfig } from '@ioc:Adonis/Core/Hash' - -export function scryptFactory(options?: Partial): Scrypt { - return new Scrypt({ - driver: 'scrypt', - cost: 2048, - blockSize: 8, - parallelization: 1, - saltSize: 16, - maxMemory: 32 * 1024 * 1024, - keyLength: 64, - ...options, - }) -} diff --git a/test/argon2.spec.ts b/test/argon2.spec.ts deleted file mode 100644 index 5a47408..0000000 --- a/test/argon2.spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import phc from '@phc/format' -import argon2 from 'argon2' -import { Argon } from '../src/Drivers/Argon' - -test.group('Argon', () => { - test('hash value', async ({ assert }) => { - const argon = new Argon({ - driver: 'argon2', - variant: 'id', - iterations: 3, - memory: 4096, - parallelism: 1, - saltSize: 16, - }) - - const hashed = await argon.make('hello-world') - const values = phc.deserialize(hashed) - - assert.equal(values.id, 'argon2id') - assert.equal(values.version, 19) - assert.deepEqual(values.params, { t: 3, m: 4096, p: 1 }) - assert.lengthOf(values.salt, 16) - }) - - test('verify hash value', async ({ assert }) => { - const argon = new Argon({ - driver: 'argon2', - variant: 'id', - iterations: 3, - memory: 4096, - parallelism: 1, - saltSize: 16, - }) - - const hashed = await argon.make('hello-world') - let matches = await argon.verify(hashed, 'hello-world') - assert.isTrue(matches) - - matches = await argon.verify(hashed, 'hi-world') - assert.isFalse(matches) - }) - - test('return true for needsRehash when variant is different', async ({ assert }) => { - const argon = new Argon({ - driver: 'argon2', - variant: 'id', - iterations: 3, - memory: 4096, - parallelism: 1, - saltSize: 16, - }) - - const argon1 = new Argon({ - driver: 'argon2', - variant: 'i', - iterations: 3, - memory: 4096, - parallelism: 1, - saltSize: 16, - }) - - const hashed = await argon.make('hello-world') - assert.isTrue(argon1.needsReHash(hashed)) - assert.isFalse(argon.needsReHash(hashed)) - }) - - test('return true for needsRehash when version is different', async ({ assert }) => { - const argon = new Argon({ - driver: 'argon2', - variant: 'id', - iterations: 3, - memory: 4096, - parallelism: 1, - saltSize: 16, - }) - - const hashed = await argon.make('hello-world') - assert.isTrue(argon.needsReHash(hashed.replace('$v=19', '$v=18'))) - }) - - test('return true for needsRehash when one of the params is different', async ({ assert }) => { - const argon = new Argon({ - driver: 'argon2', - variant: 'id', - iterations: 3, - memory: 4096, - parallelism: 1, - saltSize: 16, - }) - - const argon1 = new Argon({ - driver: 'argon2', - variant: 'id', - iterations: 1, - memory: 4096, - parallelism: 1, - saltSize: 16, - }) - - const hashed = await argon.make('hello-world') - assert.isTrue(argon1.needsReHash(hashed)) - assert.isFalse(argon.needsReHash(hashed)) - }) - - test('return true for needsRehash when hash value is not formatted as a phc string', async ({ - assert, - }) => { - const hash = await argon2.hash('hello-world') - const argon = new Argon({ - driver: 'argon2', - variant: 'id', - iterations: 1, - memory: 4096, - parallelism: 1, - saltSize: 16, - }) - - assert.isTrue(argon.needsReHash(hash)) - }) -}) diff --git a/test/bcrypt.spec.ts b/test/bcrypt.spec.ts deleted file mode 100644 index 8eacf56..0000000 --- a/test/bcrypt.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import phc from '@phc/format' -import PlainBcrypt from 'bcrypt' -import { Bcrypt } from '../src/Drivers/Bcrypt' - -test.group('Bcrypt', () => { - test('hash value using defaults', async ({ assert }) => { - const bcrypt = new Bcrypt({ rounds: 10, driver: 'bcrypt' }) - const hashed = await bcrypt.make('hello-world') - const values = phc.deserialize(hashed) - - assert.equal(values.id, 'bcrypt') - assert.equal(values.version, 98) - assert.deepEqual(values.params, { r: 10 }) - assert.lengthOf(values.salt, 16) - }) - - test('verify hashed value', async ({ assert }) => { - const bcrypt = new Bcrypt({ rounds: 10, driver: 'bcrypt' }) - const hashed = await bcrypt.make('hello-world') - - let matched = await bcrypt.verify(hashed, 'hello-world') - assert.isTrue(matched) - - matched = await bcrypt.verify(hashed, 'hi-world') - assert.isFalse(matched) - }) - - test('return true for needsRehash when version mismatch', async ({ assert }) => { - const bcrypt = new Bcrypt({ rounds: 10, driver: 'bcrypt' }) - - const hashed = await bcrypt.make('hello-world') - assert.isTrue(bcrypt.needsReHash(hashed.replace('$v=98', '$v=20'))) - }) - - test('return true for needsRehash when one of the params are different', async ({ assert }) => { - const bcrypt = new Bcrypt({ rounds: 10, driver: 'bcrypt' }) - const bcrypt2 = new Bcrypt({ rounds: 11, driver: 'bcrypt' }) - - const hashed = await bcrypt.make('hello-world') - assert.isTrue(bcrypt2.needsReHash(hashed)) - assert.isFalse(bcrypt.needsReHash(hashed)) - }) - - test('return true for needsRehash when hash value is not formatted as a phc string', async ({ - assert, - }) => { - const hash = await PlainBcrypt.hash('hello-world', 10) - const bcrypt = new Bcrypt({ rounds: 10, driver: 'bcrypt' }) - assert.isTrue(bcrypt.needsReHash(hash)) - }) -}) diff --git a/test/fake.spec.ts b/test/fake.spec.ts deleted file mode 100644 index 5878a9d..0000000 --- a/test/fake.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { Fake } from '../src/Drivers/Fake' - -test.group('Fake', () => { - test('hash value', async ({ assert }) => { - const driver = new Fake() - const hashed = await driver.make('hello-world') - - assert.equal(hashed, 'hello-world') - }) - - test('verify hashed value', async ({ assert }) => { - const driver = new Fake() - const hashed = await driver.make('hello-world') - - let matched = await driver.verify(hashed, 'hello-world') - assert.isTrue(matched) - - matched = await driver.verify(hashed, 'hi-world') - assert.isFalse(matched) - }) -}) diff --git a/test/hash-provider.spec.ts b/test/hash-provider.spec.ts deleted file mode 100644 index f999aeb..0000000 --- a/test/hash-provider.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * @adonisjs/events - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' -import { Application } from '@adonisjs/application' - -import { Hash } from '../src/Hash' -const fs = new Filesystem(join(__dirname, 'app')) - -async function setup(setupConfig: boolean = true) { - await fs.add('.env', '') - await fs.fsExtra.ensureDir(join(fs.basePath, 'config')) - - if (setupConfig) { - await fs.add( - 'config/hash.ts', - ` - const hashConfig = { - default: 'bcrypt', - list: { - bcrypt: {} - } - } - export default hashConfig - ` - ) - } - - const app = new Application(fs.basePath, 'web', { - providers: ['../../providers/HashProvider'], - }) - - app.setup() - app.registerProviders() - await app.bootProviders() - - return app -} - -test.group('Hash Provider', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('register hash provider', async ({ assert }) => { - const app = await setup() - assert.instanceOf(app.container.use('Adonis/Core/Hash'), Hash) - assert.deepEqual(app.container.use('Adonis/Core/Hash'), app.container.use('Adonis/Core/Hash')) - }) - - test('raise error when hash config is missing', async ({ assert }) => { - const app = await setup(false) - const fn = () => app.container.use('Adonis/Core/Hash') - assert.throws( - fn, - 'Invalid "hash" config. Missing value for "default". Make sure to set it inside the "config/hash" file' - ) - }) -}) diff --git a/test/hash.spec.ts b/test/hash.spec.ts deleted file mode 100644 index aad2505..0000000 --- a/test/hash.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { Application } from '@adonisjs/application' - -import { Hash } from '../src/Hash' -import { Fake } from '../src/Drivers/Fake' -import { Argon } from '../src/Drivers/Argon' -import { Bcrypt } from '../src/Drivers/Bcrypt' - -const config = { - default: 'bcrypt' as const, - list: { - argon: { - driver: 'argon2' as const, - memory: 1, - parallelism: 1, - variant: 'id' as 'id', - saltSize: 16, - iterations: 2, - }, - bcrypt: { - driver: 'bcrypt' as const, - rounds: 10, - }, - fake: { - driver: 'fake' as const, - }, - }, -} - -test.group('Hash', () => { - test('hash value using the default driver', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - const hashedValue = await hash.make('hello-world') - assert.match(hashedValue, /^\$bcrypt/) - }) - - test('verify value using the default driver', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - const hashedValue = await hash.make('hello-world') - const isSame = await hash.verify(hashedValue, 'hello-world') - assert.isTrue(isSame) - }) - - test('find if value needsReHash for the default driver', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - const hashedValue = await hash.make('hello-world') - const needsReHash = hash.needsReHash(hashedValue) - assert.isFalse(needsReHash) - }) - - test('create named driver', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - assert.instanceOf(hash.use('bcrypt' as any), Bcrypt) - assert.instanceOf(hash.use('argon' as any), Argon) - }) - - test('add custom driver', async ({ assert }) => { - const hash = new Hash( - new Application(__dirname, 'web', {}), - Object.assign({}, config, { - list: { - bcrypt: {}, - foo: { - driver: 'my-algo', - }, - }, - }) as any - ) - - class MyAlgo { - public ids = [] - public params = {} - - public async hash(): Promise { - return 'foo' - } - - public async make(): Promise { - return 'foo' - } - - public async verify(): Promise { - return true - } - - public needsReHash(): boolean { - return true - } - } - - hash.extend('my-algo', () => { - return new MyAlgo() - }) - - assert.instanceOf(hash.use('foo' as any), MyAlgo) - }) - - test('raise exception when default hasher is missing', async ({ assert }) => { - const hash = () => new Hash(new Application(__dirname, 'web', {}), {} as any) - assert.throws( - hash, - 'Invalid "hash" config. Missing value for "default". Make sure to set it inside the "config/hash" file' - ) - }) - - test('raise exception when list is missing', async ({ assert }) => { - const hash = () => new Hash(new Application(__dirname, 'web', {}), { default: 'bcrypt' } as any) - assert.throws( - hash, - 'Invalid "hash" config. Missing value for "list". Make sure to set it inside the "config/hash" file' - ) - }) - - test('raise exception when default hasher value is missing inside list', async ({ assert }) => { - const hash = () => - new Hash(new Application(__dirname, 'web', {}), { default: 'bcrypt', list: {} } as any) - assert.throws( - hash, - 'Invalid "hash" config. "bcrypt" is not defined inside "list". Make sure to set it inside the "config/hash" file' - ) - }) - - test('fake hash.make calls', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - hash.fake() - const hashedValue = await hash.make('hello-world') - assert.equal(hashedValue, 'hello-world') - }) - - test('fake hash.verify calls', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - hash.fake() - const isVerified = await hash.verify('hello-world', 'hello-world') - assert.isTrue(isVerified) - }) - - test('fake hash.needsReHash calls', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - hash.fake() - const needsReHash = hash.needsReHash('hello-world') - assert.isFalse(needsReHash) - }) - - test('return the fake instance when trying to use a named driver', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - hash.fake() - assert.instanceOf(hash.use('bcrypt' as any), Fake) - }) - - test('restore fake calls', async ({ assert }) => { - const hash = new Hash(new Application(__dirname, 'web', {}), config as any) - hash.fake() - const hashedValue = await hash.make('hello-world') - assert.equal(hashedValue, 'hello-world') - - hash.restore() - const hashedValueReal = await hash.make('hello-world') - assert.match(hashedValueReal, /^\$bcrypt/) - }) -}) diff --git a/test/scrypt.spec.ts b/test/scrypt.spec.ts deleted file mode 100644 index 96a55bf..0000000 --- a/test/scrypt.spec.ts +++ /dev/null @@ -1,232 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import phc from '@phc/format' -import { scryptFactory } from '../test-helpers' -import { kMaxUint24 } from '../src/utils' - -test.group('Scrypt', () => { - test('hash value', async ({ assert }) => { - const scrypt = scryptFactory() - - const hashed = await scrypt.make('Romain Lanz') - const values = phc.deserialize(hashed) - - assert.equal(values.id, 'scrypt') - assert.deepEqual(values.params, { n: 2048, r: 8, p: 1 }) - assert.lengthOf(values.salt, 16) - }) - - test('verify hash value', async ({ assert }) => { - const scrypt = scryptFactory() - - const hashed = await scrypt.make('Romain Lanz') - let matches = await scrypt.verify(hashed, 'Romain Lanz') - assert.isTrue(matches) - - matches = await scrypt.verify(hashed, 'Romain') - assert.isFalse(matches) - }) - - test('return true for needsRehash when one of the params is different', async ({ assert }) => { - const scrypt = scryptFactory() - const scrypt2 = scryptFactory({ - parallelization: 2, - }) - - const hashed = await scrypt.make('Romain Lanz') - assert.isTrue(scrypt2.needsReHash(hashed)) - assert.isFalse(scrypt.needsReHash(hashed)) - }) - - test('return true for needsRehash when hash value is not formatted as a phc string', async ({ - assert, - }) => { - const hash = - '46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c937333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e' - const scrypt = scryptFactory() - - assert.isTrue(scrypt.needsReHash(hash)) - }) - - test('should throw exception when hash value is not formatted as a phc string', async ({ - assert, - }) => { - const hash = - '46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c937333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e' - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, 'The hash must be a valid phc string') - }) - - test('should throw exception when the identifier is not compatible', async ({ assert }) => { - const hash = phc.serialize({ - id: 'bcrypt', - params: { n: 1024, r: 8, p: 1 }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, 'Incompatible bcrypt identifier found in the hash') - }) - - test('should throw exception when there is no params', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, 'The param section cannot be empty') - }) - - test('should throw exception when param "n" is not a integer', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 'foo', r: 8, p: 1 }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `The 'n' param must be an integer`) - }) - - test('should throw exception when param "r" is not a integer', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 1024, r: 'foo', p: 1 }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `The 'r' param must be an integer`) - }) - - test('should throw exception when param "p" is not a integer', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 1024, r: 8, p: 'foo' }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `The 'p' param must be an integer`) - }) - - test('should throw exception when param "n" is not a power of 2', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 1025, r: 8, p: 1 }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `The 'n' param must be a power of 2 greater than 1`) - }) - - test('should throw exception when param "n" is less than 2', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 1, r: 8, p: 1 }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `The 'n' param must be a power of 2 greater than 1`) - }) - - test('should throw exception when param "p" is less than 1', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 1024, r: 8, p: 0 }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `The 'p' param must be in the range (1 <= parallelism <= ${kMaxUint24})`) - }) - - test('should throw exception when param "p" is greater than 2^24-1', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 1024, r: 8, p: 16777216 }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `The 'p' param must be in the range (1 <= parallelism <= ${kMaxUint24})`) - }) - - test('should throw exception when the salt is not defined', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 1024, r: 8, p: 1 }, - hash: Buffer.from('7333a314068577835b6f44f34f75758b6de3161696fade65731f5548ea09d95e'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `No salt found in the given string`) - }) - - test('should throw exception when the hash is not defined', async ({ assert }) => { - const hash = phc.serialize({ - id: 'scrypt', - params: { n: 1024, r: 8, p: 1 }, - salt: Buffer.from('46219dec36aeeb9587836be851a8147ce0837b1bef30b28400cf1decce027c93'), - }) - - const scrypt = scryptFactory() - - await assert.rejects(async () => { - await scrypt.verify(hash, 'Romain Lanz') - }, `No hash found in the given string`) - }) -}) diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts new file mode 100644 index 0000000..9bc0547 --- /dev/null +++ b/tests/define_config.spec.ts @@ -0,0 +1,59 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { defineConfig } from '../src/define_config.js' + +test.group('Define config', () => { + test('define config for hash manager', async ({ assert }) => { + const config = defineConfig({ + default: 'bcrypt', + list: { + bcrypt: { + driver: 'bcrypt', + rounds: 10, + }, + }, + }) + + assert.deepEqual(config, { + default: 'bcrypt', + list: { + bcrypt: { + driver: 'bcrypt', + rounds: 10, + }, + }, + }) + }) + + test('fail when list is not defined', async ({ assert }) => { + // @ts-expect-error + assert.throws(() => defineConfig({}), 'Missing "list" property in hash config') + }) + + test('allow empty list', async ({ assert }) => { + assert.deepEqual(defineConfig({ list: {} }), { list: {} }) + }) + + test('fail when default property is not mentioned in the list', async ({ assert }) => { + assert.throws( + // @ts-expect-error + () => defineConfig({ default: 'bcrypt', list: {} }), + 'Missing "list.bcrypt". It is referenced by the "default" property' + ) + }) + + test('fail when default property is not defined but list has hashers', async ({ assert }) => { + assert.throws( + () => defineConfig({ list: { bcrypt: { driver: 'bcrypt' } } }), + 'Missing "default" property in hash config. Specify a default hasher' + ) + }) +}) diff --git a/tests/drivers/argon2.spec.ts b/tests/drivers/argon2.spec.ts new file mode 100644 index 0000000..a791a47 --- /dev/null +++ b/tests/drivers/argon2.spec.ts @@ -0,0 +1,471 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import argon2 from 'argon2' +import { test } from '@japa/runner' +import { Argon } from '../../src/drivers/argon.js' +import { PhcFormatter } from '../../src/phc_formatter.js' + +test.group('argon | validate config', () => { + test('validate iterations property', async ({ assert }) => { + assert.throws( + () => + new Argon({ + variant: 'id', + iterations: 1, + memory: 4096, + parallelism: 1, + saltSize: 16, + }), + 'The "iterations" option must be in the range (2 <= iterations <= 4294967295)' + ) + }) + + test('validate memory property', async ({ assert }) => { + assert.throws( + () => + new Argon({ + variant: 'id', + iterations: 3, + memory: 4, + parallelism: 1, + saltSize: 16, + }), + 'The "memory" option must be in the range (8 <= memory <= 4294967295)' + ) + }) + + test('validate parallelism property', async ({ assert }) => { + assert.throws( + () => + new Argon({ + variant: 'id', + iterations: 3, + memory: 4, + parallelism: 0, + saltSize: 16, + }), + 'The "parallelism" option must be in the range (1 <= parallelism <= 16777215)' + ) + }) + + test('validate salt size property', async ({ assert }) => { + assert.throws( + () => + new Argon({ + variant: 'id', + iterations: 3, + memory: 10, + parallelism: 1, + saltSize: 4, + }), + 'The "saltSize" option must be in the range (8 <= saltSize <= 1024)' + ) + + assert.throws( + () => + new Argon({ + variant: 'id', + iterations: 3, + memory: 10, + parallelism: 1, + saltSize: 4096, + }), + 'The "saltSize" option must be in the range (8 <= saltSize <= 1024)' + ) + }) + + test('validate variant property', async ({ assert }) => { + assert.throws( + () => + new Argon({ + // @ts-expect-error + variant: 'foo', + iterations: 3, + memory: 10, + parallelism: 1, + saltSize: 8, + }), + 'The "variant" option must be one of: i,d,id' + ) + }) + + test('validate version property', async ({ assert }) => { + assert.throws( + () => + new Argon({ + variant: 'id', + // @ts-expect-error + version: 1, + iterations: 3, + memory: 10, + parallelism: 1, + saltSize: 8, + }), + 'The "version" option must be one of: 16,19' + ) + }) +}) + +test.group('argon | hash', () => { + test('hash value and serialize to a phc string', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hashed = await argon.make('hello-world') + const values = new PhcFormatter().deserialize(hashed) + + assert.properties(values, ['hash', 'salt']) + assert.equal(values.id, 'argon2id') + assert.equal(values.version, 19) + assert.deepEqual(values.params, { t: 3, m: 4096, p: 1 }) + assert.lengthOf(values.salt, 16) + }) +}) + +test.group('argon | verify', () => { + test('verify hash value', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hashed = await argon.make('hello-world') + let matches = await argon.verify(hashed, 'hello-world') + assert.isTrue(matches) + }) + + test('verify hash value hash using different config', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + saltSize: 16, + }) + + const argon1 = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hashed = await argon.make('hello-world') + let matches = await argon1.verify(hashed, 'hello-world') + assert.isTrue(matches) + }) + + test('verify hash hashed using argon2 directly', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + saltSize: 16, + }) + + const hashed = await argon2.hash('hello-world') + let matches = await argon.verify(hashed, 'hello-world') + assert.isTrue(matches) + }) + + test('validate argon strings with no version (old argon strings had no version)', async ({ + assert, + }) => { + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + version: 19, + saltSize: 16, + }) + + const hashed = await argon2.hash('hello-world', { version: 16 }) + + let matches = await argon.verify(hashed.replace(/\$v=16/, ''), 'hello-world') + assert.isTrue(matches) + }) + + test('should verify a precomputed hash', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$argon2id$v=19$m=4096,t=3,p=1$PcEZHj1maR/+ZQynyJHWZg$2jEN4xcww7CYp1jakZB1rxbYsZ55XH2HgjYRtdZtubI' + + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + version: 19, + saltSize: 16, + }) + + assert.isTrue(await argon.verify(hash, 'password')) + }) + + test('fail verification when value is formatted as phc string', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + saltSize: 16, + }) + + const hashed = await argon2.hash('hello-world', { raw: true }) + let matches = await argon.verify(hashed.toString(), 'hello-world') + assert.isFalse(matches) + }) + + test('fail when params section is empty', async ({ assert }) => { + const wrong = + '$argon2id$v=19$PcEZHj1maR/+ZQynyJHWZg$2jEN4xcww7CYp1jakZB1rxbYsZ55XH2HgjYRtdZtubI' + + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + version: 19, + saltSize: 16, + }) + + assert.isFalse(await argon.verify(wrong, 'password')) + }) + + test('fail when hash params are tampered', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + saltSize: 16, + }) + + const hashed = await argon.make('hello-world') + + assert.isFalse(await argon.verify(hashed.replace(/m=4096/, 'm=8'), 'hello-world')) + assert.isFalse(await argon.verify(hashed.replace(/m=4096/, `m=${2 ** 32 + 4}`), 'hello-world')) + + assert.isFalse(await argon.verify(hashed.replace(/v=19/, 'v=22'), 'hello-world')) + assert.isFalse(await argon.verify(hashed.replace(/v=19/, 'v=16'), 'hello-world')) + + assert.isFalse(await argon.verify(hashed.replace(/p=2/, 'p=1'), 'hello-world')) + assert.isFalse(await argon.verify(hashed.replace(/p=2/, 'p=0'), 'hello-world')) + + assert.isFalse(await argon.verify(hashed.replace(/t=4/, 't=1'), 'hello-world')) + + assert.isFalse(await argon.verify(hashed.replace(/argon2id/, 'argon2i'), 'hello-world')) + assert.isFalse(await argon.verify(hashed.replace(/argon2id/, 'argo'), 'hello-world')) + }) + + test('fail when salt is missing', async ({ assert }) => { + const wrong = '$argon2id$v=19$m=4096,t=3,p=1' + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + saltSize: 16, + }) + + assert.isFalse(await argon.verify(wrong, 'password')) + }) + + test('fail when salt is empty', async ({ assert }) => { + const wrong = '$argon2id$v=19$m=4096,t=3,p=1$$' + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + saltSize: 16, + }) + + assert.isFalse(await argon.verify(wrong, 'password')) + }) + + test('fail when hash is missing', async ({ assert }) => { + const wrong = '$argon2id$v=19$m=4096,t=3,p=1$PcEZHj1maR/+ZQynyJHWZg' + + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + saltSize: 16, + }) + + assert.isFalse(await argon.verify(wrong, 'password')) + }) + + test('fail when hash is empty', async ({ assert }) => { + const wrong = '$argon2id$v=19$m=4096,t=3,p=1$PcEZHj1maR/+ZQynyJHWZg$' + + const argon = new Argon({ + variant: 'id', + iterations: 4, + memory: 4096, + parallelism: 2, + saltSize: 16, + }) + + assert.isFalse(await argon.verify(wrong, 'password')) + }) +}) + +test.group('argon | needsRehash', () => { + test('return true when variant is different', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const argon1 = new Argon({ + variant: 'i', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hashed = await argon.make('hello-world') + assert.isTrue(argon1.needsReHash(hashed)) + assert.isFalse(argon.needsReHash(hashed)) + }) + + test('return true when version is different', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hashed = await argon.make('hello-world') + assert.isTrue(argon.needsReHash(hashed.replace('$v=19', '$v=18'))) + }) + + test('return true when all of the params are missing', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 2, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hash = '$argon2id$v=19$PcEZHj1maR/+ZQynyJHWZg$2jEN4xcww7CYp1jakZB1rxbYsZ55XH2HgjYRtdZtubI' + assert.isTrue(argon.needsReHash(hash)) + }) + + test('return true when one of the params are different', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hash = + '$argon2id$v=19$m=4096,t=3,p=1$PcEZHj1maR/+ZQynyJHWZg$2jEN4xcww7CYp1jakZB1rxbYsZ55XH2HgjYRtdZtubI' + + assert.isFalse(argon.needsReHash(hash)) + assert.isTrue(argon.needsReHash(hash.replace('m=4096', 'm=1024'))) + assert.isTrue(argon.needsReHash(hash.replace('t=3', 't=2'))) + assert.isTrue(argon.needsReHash(hash.replace('p=1', 'p=2'))) + }) + + test('throw error when value is not a valid phc string', async ({ assert }) => { + const hash = await argon2.hash('hello-world', { raw: true }) + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + await assert.rejects( + () => argon.needsReHash(hash.toString()), + 'pchstr must contain a $ as first char' + ) + }) + + test('throw error when not a valid argon identifier', async ({ assert }) => { + const hash = + '$bcrypt$v=19$m=4096,t=3,p=1$PcEZHj1maR/+ZQynyJHWZg$2jEN4xcww7CYp1jakZB1rxbYsZ55XH2HgjYRtdZtubI' + + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + await assert.rejects(() => argon.needsReHash(hash), 'Value is not a valid argon hash') + }) + + test('return true when using argon2 directly', async ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hashed = await argon2.hash('hello-world') + + assert.isTrue(argon.needsReHash(hashed)) + assert.isFalse(argon2.needsRehash(hashed)) + }) +}) + +test.group('argon | isValidHash', () => { + test('check if value formatted as a valid argon2 hash', ({ assert }) => { + const argon = new Argon({ + variant: 'id', + iterations: 3, + memory: 4096, + parallelism: 1, + saltSize: 16, + }) + + const hash = + '$argon2id$v=19$m=4096,t=3,p=1$PcEZHj1maR/+ZQynyJHWZg$2jEN4xcww7CYp1jakZB1rxbYsZ55XH2HgjYRtdZtubI' + + assert.isTrue(argon.isValidHash(hash)) + /** + * Non versionized hashes are allowed + */ + assert.isTrue(argon.isValidHash(hash.replace('$v=19', ''))) + + assert.isFalse(argon.isValidHash('hello world')) + assert.isFalse(argon.isValidHash(hash.replace('$m=4096', ''))) + assert.isFalse(argon.isValidHash(hash.replace('p=1', ''))) + }) +}) diff --git a/tests/drivers/bcrypt.spec.ts b/tests/drivers/bcrypt.spec.ts new file mode 100644 index 0000000..6e5dca8 --- /dev/null +++ b/tests/drivers/bcrypt.spec.ts @@ -0,0 +1,267 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import bcryptDialect from 'bcrypt' + +import { Bcrypt } from '../../src/drivers/bcrypt.js' +import { PhcFormatter } from '../../src/phc_formatter.js' + +test.group('bcrypt | validate config', () => { + test('validate rounds property', ({ assert }) => { + assert.throws( + () => new Bcrypt({ rounds: 1 }), + 'The "rounds" option must be in the range (4 <= rounds <= 31)' + ) + assert.throws( + () => new Bcrypt({ rounds: 32 }), + 'The "rounds" option must be in the range (4 <= rounds <= 31)' + ) + }) + + test('validate salt size property', ({ assert }) => { + assert.throws( + () => new Bcrypt({ saltSize: 4 }), + 'The "saltSize" option must be in the range (8 <= saltSize <= 1024)' + ) + assert.throws( + () => new Bcrypt({ saltSize: 4096 }), + 'The "saltSize" option must be in the range (8 <= saltSize <= 1024)' + ) + }) + + test('validate version property', ({ assert }) => { + assert.throws( + // @ts-expect-error + () => new Bcrypt({ version: 92 }), + 'The "version" option must be one of: 97,98' + ) + }) +}) + +test.group('bcrypt | hash', () => { + test('hash value and serialize as a phc string', async ({ assert }) => { + const bcrypt = new Bcrypt({ rounds: 10 }) + const hashed = await bcrypt.make('hello-world') + const values = new PhcFormatter().deserialize(hashed) + + assert.equal(values.id, 'bcrypt') + assert.equal(values.version, 98) + assert.deepEqual(values.params, { r: 10 }) + assert.lengthOf(values.salt, 16) + }) +}) + +test.group('bcrypt | verify', () => { + test('verify hash value', async ({ assert }) => { + const bcrypt = new Bcrypt({ rounds: 10 }) + const hash = await bcrypt.make('hello-world') + + assert.isTrue(await bcrypt.verify(hash, 'hello-world')) + }) + + test('verify with rounds less than 10', async ({ assert }) => { + const bcrypt = new Bcrypt({ rounds: 9 }) + assert.isTrue(await bcrypt.verify(await bcrypt.make('hello-world'), 'hello-world')) + }) + + test('verify hash using different config', async ({ assert }) => { + const bcrypt = new Bcrypt({ rounds: 10 }) + const bcrypt1 = new Bcrypt({ rounds: 8, saltSize: 32 }) + const hash = await bcrypt.make('hello-world') + + assert.isTrue(await bcrypt1.verify(hash, 'hello-world')) + }) + + test('verify bcrypt mcf hash', async ({ assert }) => { + const bcrypt = new Bcrypt({ rounds: 10 }) + const hash = await bcryptDialect.hash('hello-world', 10) + + assert.isTrue(await bcrypt.verify(hash, 'hello-world')) + }) + + test('fail when mcf hash is invalid', async ({ assert }) => { + const bcrypt = new Bcrypt({ rounds: 10 }) + const hash = await bcryptDialect.hash('hello-world', 10) + + assert.isFalse(await bcrypt.verify(hash, 'hi-world')) + }) + + test('verify a precomputed hash', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=98$r=10$tAe1bhm5zoo0Sx7ZfrCd7w$0T4Cf8htpt/8FbjK+cErdaTh8T6ClYQ' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isTrue(await bcrypt.verify(hash, 'password')) + }) + + test('fail if the identifier is unsupported', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcript$v=98$r=10$tAe1bhm5zoo0Sx7ZfrCd7w$0T4Cf8htpt/8FbjK+cErdaTh8T6ClYQ' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) + + test('fail if the version is not supported', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=10$r=10$tAe1bhm5zoo0Sx7ZfrCd7w$0T4Cf8htpt/8FbjK+cErdaTh8T6ClYQ' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) + + test('fail if the param section is empty', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=98$tAe1bhm5zoo0Sx7ZfrCd7w$0T4Cf8htpt/8FbjK+cErdaTh8T6ClYQ' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) + + test("fail if the 'r' parameter is missing", async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=98$i=12$tAe1bhm5zoo0Sx7ZfrCd7w$0T4Cf8htpt/8FbjK+cErdaTh8T6ClYQ' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) + + test("fail if the 'r' parameter is out of range", async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=98$r=0$tAe1bhm5zoo0Sx7ZfrCd7w$0T4Cf8htpt/8FbjK+cErdaTh8T6ClYQ' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) + + test('fail if salt is missing', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=98$r=8' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) + + test('fail if salt is empty', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=98$r=8$$' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) + + test('fail if hash is missing', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=98$r=8$aM15713r3Xsvxbi31lqr1Q' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) + + test('fail if hash is empty', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$bcrypt$v=98$r=8$aM15713r3Xsvxbi31lqr1Q$' + const bcrypt = new Bcrypt({ rounds: 10 }) + + assert.isFalse(await bcrypt.verify(hash, 'password')) + }) +}) + +test.group('bcrypt | needsRehash', () => { + test('return true when version is different', async ({ assert }) => { + const bcrypt = new Bcrypt({ + version: 98, + }) + + const hashed = await bcrypt.make('hello-world') + assert.isTrue(bcrypt.needsReHash(hashed.replace('$v=98', '$v=97'))) + }) + + test('return true rounds param is different', async ({ assert }) => { + const bcrypt = new Bcrypt({ + rounds: 10, + }) + + const bcrypt1 = new Bcrypt({ + rounds: 8, + }) + + const hashed = await bcrypt.make('hello-world') + assert.isTrue(bcrypt1.needsReHash(hashed)) + assert.isFalse(bcrypt.needsReHash(hashed)) + }) + + test('return true when params are missing', async ({ assert }) => { + const bcrypt = new Bcrypt({ + rounds: 10, + }) + + const hash = '$bcrypt$v=98$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8' + assert.isTrue(bcrypt.needsReHash(hash)) + }) + + test('return true when version is missing', async ({ assert }) => { + const bcrypt = new Bcrypt({ + rounds: 10, + }) + + const hash = '$bcrypt$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8' + assert.isTrue(bcrypt.needsReHash(hash)) + }) + + test('return true for bcrypt mcf hash', async ({ assert }) => { + const bcrypt = new Bcrypt({ + version: 98, + }) + + const hashed = await bcryptDialect.hash('hello-world', 10) + assert.isTrue(bcrypt.needsReHash(hashed)) + }) + + test('throw error when value is not a valid phc string', async ({ assert }) => { + const bcrypt = new Bcrypt({ + rounds: 10, + }) + + await assert.rejects( + () => bcrypt.needsReHash('hello world'), + 'pchstr must contain a $ as first char' + ) + }) + + test('throw error when identifier is invalid', async ({ assert }) => { + const bcrypt = new Bcrypt({ + rounds: 10, + }) + + const hash = '$argon2id$v=98$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8' + await assert.rejects(() => bcrypt.needsReHash(hash), 'Value is not a valid bcrypt hash') + }) +}) + +test.group('argon | isValidHash', () => { + test('check if value formatted as a valid bcrypt hash', ({ assert }) => { + const bcrypt = new Bcrypt({ + version: 98, + }) + + const hash = '$bcrypt$v=98$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8' + + assert.isTrue(bcrypt.isValidHash(hash)) + + /** + * Non versionized hashes are allowed + */ + assert.isTrue(bcrypt.isValidHash(hash.replace('$v=98', ''))) + + assert.isFalse(bcrypt.isValidHash('hello world')) + assert.isFalse(bcrypt.isValidHash(hash.replace('$r=10', ''))) + }) +}) diff --git a/tests/drivers/fake.spec.ts b/tests/drivers/fake.spec.ts new file mode 100644 index 0000000..a7b093f --- /dev/null +++ b/tests/drivers/fake.spec.ts @@ -0,0 +1,41 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Fake } from '../../src/drivers/fake.js' + +test.group('Fake', () => { + test('hash value', async ({ assert }) => { + const driver = new Fake() + const hashed = await driver.make('hello-world') + + assert.equal(hashed, 'hello-world') + }) + + test('verify hashed value', async ({ assert }) => { + const driver = new Fake() + const hashed = await driver.make('hello-world') + + assert.isTrue(await driver.verify(hashed, 'hello-world')) + assert.isFalse(await driver.verify(hashed, 'hi-world')) + }) + + test('always return true from "isValidHash"', async ({ assert }) => { + const driver = new Fake() + const hashed = await driver.make('hello-world') + assert.isTrue(driver.isValidHash(hashed)) + assert.isTrue(driver.isValidHash('hi-world')) + }) + + test('always return true from "needsReHash"', async ({ assert }) => { + const driver = new Fake() + const hashed = await driver.make('hello-world') + assert.isFalse(driver.needsReHash(hashed)) + }) +}) diff --git a/tests/drivers/scrypt.spec.ts b/tests/drivers/scrypt.spec.ts new file mode 100644 index 0000000..1b66eaf --- /dev/null +++ b/tests/drivers/scrypt.spec.ts @@ -0,0 +1,334 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Scrypt } from '../../src/drivers/scrypt.js' +import { PhcFormatter } from '../../src/phc_formatter.js' + +test.group('argon | scrypt config', () => { + test('validate block size property', async ({ assert }) => { + assert.throws( + () => + new Scrypt({ + blockSize: -1, + }), + 'The "blockSize" option must be in the range (1 <= blockSize <= 4294967295)' + ) + + assert.throws( + () => + new Scrypt({ + blockSize: 2 ** 32 + 1, + }), + 'The "blockSize" option must be in the range (1 <= blockSize <= 4294967295)' + ) + }) + + test('validate cost property', async ({ assert }) => { + assert.throws( + () => + new Scrypt({ + cost: 1, + }), + 'The "cost" option must be in the range (2 <= cost <= 4294967295)' + ) + + // assert.throws( + // () => + // new Scrypt({ + // + // blockSize: 3, + // }), + // 'The "blockSize" option must be in the range (1 <= blockSize <= 4294967295)' + // ) + }) + + test('validate parallelization property', async ({ assert }) => { + assert.throws( + () => + new Scrypt({ + parallelization: 0, + }), + 'The "parallelization" option must be in the range (1 <= parallelization <= 134217727)' + ) + }) + + test('validate saltSize property', async ({ assert }) => { + assert.throws( + () => + new Scrypt({ + saltSize: 4, + }), + 'The "saltSize" option must be in the range (8 <= saltSize <= 1024)' + ) + + assert.throws( + () => + new Scrypt({ + saltSize: 4096, + }), + 'The "saltSize" option must be in the range (8 <= saltSize <= 1024)' + ) + }) + + test('validate keyLength property', async ({ assert }) => { + assert.throws( + () => + new Scrypt({ + keyLength: 32, + }), + 'The "keyLength" option must be in the range (64 <= keyLength <= 128)' + ) + + assert.throws( + () => + new Scrypt({ + keyLength: 256, + }), + 'The "keyLength" option must be in the range (64 <= keyLength <= 128)' + ) + }) +}) + +test.group('scrypt | hash', () => { + test('hash value', async ({ assert }) => { + const scrypt = new Scrypt({}) + + const hashed = await scrypt.make('hello-world') + const values = new PhcFormatter().deserialize(hashed) + + assert.equal(values.id, 'scrypt') + assert.deepEqual(values.params, { n: 16384, r: 8, p: 1 }) + assert.lengthOf(values.salt, 16) + }) +}) + +test.group('scrypt | verify', () => { + test('verify hash value', async ({ assert }) => { + const scrypt = new Scrypt({}) + + const hashed = await scrypt.make('hello-world') + + assert.isTrue(await scrypt.verify(hashed, 'hello-world')) + assert.isFalse(await scrypt.verify(hashed, 'Romain')) + }) + + test('verify hash value hash using different config', async ({ assert }) => { + const scrypt = new Scrypt({ + maxMemory: 128 * 16438 * 8, + }) + const scrypt1 = new Scrypt({}) + + const hashed = await scrypt.make('hello-world') + assert.isTrue(await scrypt1.verify(hashed, 'hello-world')) + }) + + test('should verify a precomputed hash', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$n=16384,r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isTrue(await scrypt.verify(hash, 'password')) + }) + + test('fail when params are missing', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail if the identifier is unsupported', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$script$n=16384,r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail if the "n" parameter is missing', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail if the "n" parameter is out of range', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$n=0,r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail if the "r" parameter is missing', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$n=16384,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail if the "r" parameter is out of range', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$n=16384,r=-1,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail if the "p" parameter is missing', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$n=16384,r=8$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail if the "p" parameter is out of range', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$n=16384,r=8,p=-1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail when salt is missing', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$scrypt$n=16384,r=8,p=1' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail when salt is empty', async ({ assert }) => { + // Precomputed hash for "password" + const hash = + '$scrypt$n=16384,r=8,p=1$$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail when hash is missing', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$scrypt$n=16384,r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) + + test('fail when hash is empty', async ({ assert }) => { + // Precomputed hash for "password" + const hash = '$scrypt$n=16384,r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg$' + + const scrypt = new Scrypt({}) + assert.isFalse(await scrypt.verify(hash, 'password')) + }) +}) + +test.group('scrypt | needsRehash', () => { + test('return true when cost is different', async ({ assert }) => { + const scrypt = new Scrypt({ + cost: 16384, + }) + + const scrypt1 = new Scrypt({ + cost: 3304, + }) + + const hashed = await scrypt.make('hello-world') + assert.isTrue(scrypt1.needsReHash(hashed)) + assert.isFalse(scrypt.needsReHash(hashed)) + }) + + test('return true when block size is different', async ({ assert }) => { + const scrypt = new Scrypt({ + blockSize: 8, + }) + + const scrypt1 = new Scrypt({ + blockSize: 4, + }) + + const hashed = await scrypt.make('hello-world') + assert.isTrue(scrypt1.needsReHash(hashed)) + assert.isFalse(scrypt.needsReHash(hashed)) + }) + + test('return true when parallelization is different', async ({ assert }) => { + const scrypt = new Scrypt({ + parallelization: 2, + }) + + const scrypt1 = new Scrypt({ + parallelization: 1, + }) + + const hashed = await scrypt.make('hello-world') + assert.isTrue(scrypt1.needsReHash(hashed)) + assert.isFalse(scrypt.needsReHash(hashed)) + }) + + test('return true when params are missing', async ({ assert }) => { + const scrypt = new Scrypt({ + cost: 16384, + }) + + const hash = + '$scrypt$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + assert.isTrue(scrypt.needsReHash(hash)) + }) + + test('throw error when value is not a valid phc string', async ({ assert }) => { + const scrypt = new Scrypt({ + cost: 16384, + }) + + await assert.rejects(() => scrypt.needsReHash('foo'), 'pchstr must contain a $ as first char') + }) + + test('throw error when identifier is invalid', async ({ assert }) => { + const scrypt = new Scrypt({ + cost: 16384, + }) + + const hash = + '$script$n=16384,r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + await assert.rejects(() => scrypt.needsReHash(hash), 'Value is not a valid scrypt hash') + }) +}) + +test.group('argon | isValidHash', () => { + test('check if value formatted as a valid argon2 hash', ({ assert }) => { + const hash = + '$scrypt$n=16384,r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' + + const scrypt = new Scrypt({}) + + assert.isTrue(scrypt.isValidHash(hash)) + assert.isFalse(scrypt.isValidHash('hello world')) + assert.isFalse(scrypt.isValidHash(hash.replace('$n=16384', ''))) + assert.isFalse(scrypt.isValidHash(hash.replace('r=8', ''))) + assert.isFalse(scrypt.isValidHash(hash.replace('p=1', ''))) + }) +}) diff --git a/tests/hash.spec.ts b/tests/hash.spec.ts new file mode 100644 index 0000000..8cac490 --- /dev/null +++ b/tests/hash.spec.ts @@ -0,0 +1,38 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Hash } from '../src/hash.js' +import { Argon } from '../src/drivers/argon.js' + +test.group('Hash', () => { + test('hash text using a driver', async ({ assert }) => { + const argon = new Argon({}) + const hash = new Hash(argon) + + const hashedValue = await hash.make('secret') + assert.isTrue(hash.isValidHash(hashedValue)) + }) + + test('verify hash using a driver', async ({ assert }) => { + const argon = new Argon({}) + const hash = new Hash(argon) + + const hashedValue = await hash.make('secret') + assert.isTrue(await hash.verify(hashedValue, 'secret')) + }) + + test('check if hash needs to be rehashed using a driver', async ({ assert }) => { + const argon = new Argon({}) + const hash = new Hash(argon) + + const hashedValue = await hash.make('secret') + assert.isFalse(hash.needsReHash(hashedValue)) + }) +}) diff --git a/tests/hash_manager.spec.ts b/tests/hash_manager.spec.ts new file mode 100644 index 0000000..5be1b06 --- /dev/null +++ b/tests/hash_manager.spec.ts @@ -0,0 +1,270 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import sinon from 'sinon' +import { test } from '@japa/runner' +import { Hash } from '../src/hash.js' +import { HashManager } from '../src/hash_manager.js' +import { HashDriverContract } from '../src/types.js' + +test.group('Hash manager', () => { + test('create hash instance from the manager', ({ assert, expectTypeOf }) => { + const manager = new HashManager({ + default: 'argon', + list: { + argon: { + driver: 'argon2', + }, + bcrypt: { + driver: 'bcrypt', + }, + scrypt: { + driver: 'scrypt', + }, + }, + }) + + expectTypeOf(manager.use) + .parameter(0) + .toEqualTypeOf<'argon' | 'scrypt' | 'bcrypt' | undefined>() + + expectTypeOf(manager.use('argon')).toEqualTypeOf() + expectTypeOf(manager.use('bcrypt')).toEqualTypeOf() + expectTypeOf(manager.use('scrypt')).toEqualTypeOf() + + assert.instanceOf(manager.use('argon'), Hash) + assert.instanceOf(manager.use('bcrypt'), Hash) + assert.instanceOf(manager.use('scrypt'), Hash) + }) + + test('cache hash instance', ({ assert, expectTypeOf }) => { + const manager = new HashManager({ + default: 'argon', + list: { + argon: { + driver: 'argon2', + }, + bcrypt: { + driver: 'bcrypt', + }, + scrypt: { + driver: 'scrypt', + }, + }, + }) + + expectTypeOf(manager.use) + .parameter(0) + .toEqualTypeOf<'argon' | 'scrypt' | 'bcrypt' | undefined>() + + expectTypeOf(manager.use('argon')).toEqualTypeOf() + expectTypeOf(manager.use('bcrypt')).toEqualTypeOf() + expectTypeOf(manager.use('scrypt')).toEqualTypeOf() + + assert.strictEqual(manager.use('argon'), manager.use('argon')) + assert.strictEqual(manager.use('bcrypt'), manager.use('bcrypt')) + assert.strictEqual(manager.use('scrypt'), manager.use('scrypt')) + }) + + test('use default hasher', ({ assert }) => { + const manager = new HashManager({ + default: 'argon', + list: { + argon: { + driver: 'argon2', + }, + bcrypt: { + driver: 'bcrypt', + }, + scrypt: { + driver: 'scrypt', + }, + }, + }) + + assert.strictEqual(manager.use(), manager.use('argon')) + assert.notStrictEqual(manager.use(), manager.use('bcrypt')) + assert.notStrictEqual(manager.use(), manager.use('scrypt')) + }) + + test('fail when default hasher is not configured', ({ assert }) => { + const manager = new HashManager({ + list: { + argon: { + driver: 'argon2', + }, + bcrypt: { + driver: 'bcrypt', + }, + scrypt: { + driver: 'scrypt', + }, + }, + }) + + assert.throws( + () => manager.use(), + 'Cannot create hash instance. No default hasher is defined in the config' + ) + }) + + test('fail when driver is unknown', ({ assert, expectTypeOf }) => { + const manager = new HashManager({ + default: 'pdkf', + list: { + pdkf: { + // @ts-expect-error + driver: 'pdkf', + }, + }, + }) + + assert.throws( + () => manager.use('pdkf'), + 'Unknown hash driver "pdkf". Make sure the driver is registered with HashManager' + ) + + // @ts-expect-error + expectTypeOf(manager.use).parameters.toEqualTypeOf<['pdkf']>() + }) + + test('fake all hashers', async ({ assert }) => { + const manager = new HashManager({ + default: 'argon', + list: { + argon: { + driver: 'argon2', + }, + bcrypt: { + driver: 'bcrypt', + }, + scrypt: { + driver: 'scrypt', + }, + }, + }) + + manager.fake() + assert.equal(await manager.use('argon').make('hello-world'), 'hello-world') + assert.equal(await manager.use('bcrypt').make('hello-world'), 'hello-world') + assert.equal(await manager.use('scrypt').make('hello-world'), 'hello-world') + + manager.restore() + assert.notEqual(await manager.use('argon').make('hello-world'), 'hello-world') + assert.notEqual(await manager.use('bcrypt').make('hello-world'), 'hello-world') + assert.notEqual(await manager.use('scrypt').make('hello-world'), 'hello-world') + }) + + test('extend to add custom drivers', async ({ assert, cleanup }) => { + const manager = new HashManager({ + default: 'passwords', + list: { + passwords: { + // @ts-expect-error + driver: 'md5', + }, + }, + }) + + class Md5 implements HashDriverContract { + async make(value: string): Promise { + return value + } + + async verify(hashedValue: string, plainValue: string): Promise { + return hashedValue === plainValue + } + + isValidHash(value: string): boolean { + return !!value + } + + needsReHash(hashedValue: string): boolean { + return !hashedValue + } + } + const md5 = new Md5() + const make = sinon.spy(md5, 'make') + const verify = sinon.spy(md5, 'verify') + const needsReHash = sinon.spy(md5, 'needsReHash') + const isValidHash = sinon.spy(md5, 'isValidHash') + + cleanup(() => { + make.restore() + verify.restore() + needsReHash.restore() + isValidHash.restore() + }) + + // @ts-expect-error + manager.extend('md5', () => md5) + + await manager.use('passwords').make('hello-world') + assert.isTrue(make.calledOnce) + assert.isTrue(make.calledWith('hello-world')) + assert.equal(await make.returnValues[0], 'hello-world') + + await manager.use('passwords').verify('hello-world', 'hello-world') + assert.isTrue(verify.calledOnce) + assert.isTrue(verify.calledWith('hello-world', 'hello-world')) + assert.equal(await verify.returnValues[0], true) + + manager.use('passwords').needsReHash('hello-world') + assert.isTrue(needsReHash.calledOnce) + assert.isTrue(needsReHash.calledWith('hello-world')) + assert.equal(needsReHash.returnValues[0], false) + + manager.use('passwords').isValidHash('hello-world') + assert.isTrue(isValidHash.calledOnce) + assert.isTrue(isValidHash.calledWith('hello-world')) + assert.equal(isValidHash.returnValues[0], true) + }) + + test('hash text using the default driver', async ({ assert }) => { + const manager = new HashManager({ + default: 'argon', + list: { + argon: { + driver: 'argon2', + }, + }, + }) + + const hashedValue = await manager.make('secret') + assert.isTrue(manager.isValidHash(hashedValue)) + }) + + test('verify hash using the default driver', async ({ assert }) => { + const manager = new HashManager({ + default: 'argon', + list: { + argon: { + driver: 'argon2', + }, + }, + }) + + const hashedValue = await manager.make('secret') + assert.isTrue(await manager.verify(hashedValue, 'secret')) + }) + + test('check if hash needs to be rehashed using the default driver', async ({ assert }) => { + const manager = new HashManager({ + default: 'argon', + list: { + argon: { + driver: 'argon2', + }, + }, + }) + + const hashedValue = await manager.make('secret') + assert.isFalse(manager.needsReHash(hashedValue)) + }) +}) diff --git a/tests/phc_formatter.spec.ts b/tests/phc_formatter.spec.ts new file mode 100644 index 0000000..8ab5852 --- /dev/null +++ b/tests/phc_formatter.spec.ts @@ -0,0 +1,184 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { PhcFormatter } from '../src/phc_formatter.js' + +test.group('Phc formatter', () => { + test('serialize salt and hash as a phc string', ({ assert }) => { + const formatter = new PhcFormatter() + + const salt = 'iHSDPHzUhPzK7rCcJgOFfg' + const hash = + 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g' + + const options = { + id: 'argon2i', + version: 19, + params: { + m: 120, + t: 5000, + p: 2, + }, + } + + const output = formatter.serialize( + Buffer.from(salt, 'base64'), + Buffer.from(hash, 'base64'), + options + ) + + assert.equal(output, `$argon2i$v=19$m=120,t=5000,p=2$${salt}$${hash}`) + }) + + test('serialize salt and hash as a phc string without params', ({ assert }) => { + const formatter = new PhcFormatter() + + const salt = 'iHSDPHzUhPzK7rCcJgOFfg' + const hash = + 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g' + + const options = { + id: 'argon2i', + version: 19, + } + + const output = formatter.serialize( + Buffer.from(salt, 'base64'), + Buffer.from(hash, 'base64'), + options + ) + + assert.equal(output, `$argon2i$v=19$${salt}$${hash}`) + }) + + test('serialize salt and hash as a phc string without version', ({ assert }) => { + const formatter = new PhcFormatter() + + const salt = 'iHSDPHzUhPzK7rCcJgOFfg' + const hash = + 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g' + + const options = { + id: 'argon2i', + params: { + m: 120, + t: 5000, + p: 2, + }, + } + + const output = formatter.serialize( + Buffer.from(salt, 'base64'), + Buffer.from(hash, 'base64'), + options + ) + assert.equal(output, `$argon2i$m=120,t=5000,p=2$${salt}$${hash}`) + }) + + test('deserialize phc string', ({ assert }) => { + const formatter = new PhcFormatter() + + const salt = 'iHSDPHzUhPzK7rCcJgOFfg' + const hash = + 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g' + + const options = { + id: 'argon2i', + version: 19, + params: { + m: 120, + t: 5000, + p: 2, + }, + } + + const output = formatter.serialize( + Buffer.from(salt, 'base64'), + Buffer.from(hash, 'base64'), + options + ) + + assert.deepEqual(formatter.deserialize(output), { + id: 'argon2i', + version: 19, + params: { + m: 120, + t: 5000, + p: 2, + }, + salt: Buffer.from(salt, 'base64'), + hash: Buffer.from(hash, 'base64'), + }) + }) + + test('deserialize phc string with version', ({ assert }) => { + const formatter = new PhcFormatter() + + const salt = 'iHSDPHzUhPzK7rCcJgOFfg' + const hash = + 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g' + + const options = { + id: 'argon2i', + params: { + m: 120, + t: 5000, + p: 2, + }, + } + + const output = formatter.serialize( + Buffer.from(salt, 'base64'), + Buffer.from(hash, 'base64'), + options + ) + + assert.deepEqual(formatter.deserialize(output), { + id: 'argon2i', + params: { + m: 120, + t: 5000, + p: 2, + }, + salt: Buffer.from(salt, 'base64'), + hash: Buffer.from(hash, 'base64'), + }) + }) + + test('deserialize phc string without params', ({ assert }) => { + const formatter = new PhcFormatter() + + const salt = 'iHSDPHzUhPzK7rCcJgOFfg' + const hash = + 'J4moa2MM0/6uf3HbY2Tf5Fux8JIBTwIhmhxGRbsY14qhTltQt+Vw3b7tcJNEbk8ium8AQfZeD4tabCnNqfkD1g' + + const options = { id: 'argon2i' } + + const output = formatter.serialize( + Buffer.from(salt, 'base64'), + Buffer.from(hash, 'base64'), + options + ) + + assert.deepEqual(formatter.deserialize(output), { + id: 'argon2i', + salt: Buffer.from(salt, 'base64'), + hash: Buffer.from(hash, 'base64'), + }) + }) + + test('raise error when phc string is not valid', ({ assert }) => { + const formatter = new PhcFormatter() + assert.throws( + () => formatter.deserialize('hello world'), + 'pchstr must contain a $ as first char' + ) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 7a759c9..0604247 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,30 @@ { - "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", - "files": [ - "./node_modules/@adonisjs/application/build/adonis-typings/index.d.ts" - ] + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "lib": ["ESNext"], + "noUnusedLocals": true, + "noUnusedParameters": true, + "isolatedModules": true, + "removeComments": true, + "declaration": true, + "rootDir": "./", + "outDir": "./build", + "esModuleInterop": true, + "strictNullChecks": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strictPropertyInitialization": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "skipLibCheck": true, + "types": ["@types/node"] + }, + "include": ["./**/*"], + "exclude": ["./node_modules", "./build"], + "ts-node": { + "swc": true + } } From ac3fe3b5572c1f53948964d4c4eb5b1e99f56a00 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 24 Nov 2022 09:46:52 +0530 Subject: [PATCH 02/74] docs(README): update docs --- README.md | 46 +++++++++++++++------------------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 7ca20ba..09218d7 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,25 @@ -
- -
+# @adonisjs/events
-
-

AdonisJS Hash Provider

-

A multi driver hash provider to hash values (usually passwords). The hash output follows the PHC format spec.

-
+[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url] -
+## Introduction +AdonisJS hash is a driver based hashing library. We ship with `argon2`, `bcrypt` and `scrypt` drivers. The generated hash from all the drivers are formatted using the [PHC string format](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md) -
+## Official Documentation +The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/hash) -[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url] +## Contributing +One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. + +We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. + +## Code of Conduct +In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). -
- - - -
- Built with ❤︎ by Harminder Virk -
+## License +AdonisJS hash is open-sourced software licensed under the [MIT license](LICENSE.md). [gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/hash/test?style=for-the-badge [gh-workflow-url]: https://github.com/adonisjs/hash/actions/workflows/test.yml "Github action" From 739fd7d2b1c7ee7cc542747572ff0ce74bfc8eef Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 24 Nov 2022 09:47:22 +0530 Subject: [PATCH 03/74] ci: normalize test import path for windows --- bin/test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/test.ts b/bin/test.ts index 5b398e1..ee6d2e9 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,4 +1,5 @@ import { assert } from '@japa/assert' +import { pathToFileURL } from 'node:url' import { expectTypeOf } from '@japa/expect-type' import { specReporter } from '@japa/spec-reporter' import { runFailedTests } from '@japa/run-failed-tests' @@ -23,7 +24,7 @@ configure({ files: ['tests/**/*.spec.ts'], plugins: [assert(), runFailedTests(), expectTypeOf()], reporters: [specReporter()], - importer: (filePath: string) => import(filePath), + importer: (filePath: string) => import(pathToFileURL(filePath).href), }, }) From 9548b5c9a657e27548a10cbd9acfc52a6ec1e7c3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 24 Nov 2022 09:52:28 +0530 Subject: [PATCH 04/74] chore(release): 8.0.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afc7722..56d2a84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "7.2.2", + "version": "8.0.0-0", "description": "Multi driver hash module with support for PHC string formats", "main": "build/index.js", "type": "module", From eb32848ad1bb969e16e8f20f445e06ee28e55075 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 29 Nov 2022 17:45:43 +0530 Subject: [PATCH 05/74] chore: update dependencies --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 56d2a84..fdb2ff5 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@adonisjs/application": "^5.3.0", "@commitlint/cli": "^17.3.0", "@commitlint/config-conventional": "^17.3.0", "@japa/assert": "^1.3.6", @@ -44,7 +43,7 @@ "@japa/runner": "^2.2.2", "@japa/spec-reporter": "^1.3.2", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.19", + "@swc/core": "^1.3.20", "@types/bcrypt": "^5.0.0", "@types/node": "^18.11.9", "@types/sinon": "^10.0.13", @@ -60,7 +59,7 @@ "husky": "^8.0.2", "np": "^7.6.2", "prettier": "^2.7.1", - "sinon": "^14.0.2", + "sinon": "^15.0.0", "ts-node": "^10.9.1", "typescript": "^4.9.3" }, From df53264ef8eb42e46b5457d93b6e046efe8e787e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 29 Nov 2022 17:46:39 +0530 Subject: [PATCH 06/74] fix: build bcrypt_base64.cjs file --- src/drivers/bcrypt.ts | 1 - tsconfig.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/drivers/bcrypt.ts b/src/drivers/bcrypt.ts index c03f005..92ed9c1 100644 --- a/src/drivers/bcrypt.ts +++ b/src/drivers/bcrypt.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ -// @ts-expect-error import * as bcryptBase64 from '../legacy/bcrypt_base64.cjs' import type bcrypt from 'bcrypt' diff --git a/tsconfig.json b/tsconfig.json index 0604247..5c0a0dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "noUnusedParameters": true, "isolatedModules": true, "removeComments": true, + "allowJs": true, "declaration": true, "rootDir": "./", "outDir": "./build", From f3fbd38fb9598e1f9a0175480fb4eccd3c55cfa0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 29 Nov 2022 17:50:27 +0530 Subject: [PATCH 07/74] chore(release): 8.0.1-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fdb2ff5..b901c80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.0.0-0", + "version": "8.0.1-0", "description": "Multi driver hash module with support for PHC string formats", "main": "build/index.js", "type": "module", From a40e8ca4b96f354832f11f64faac3cdb9831fb35 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 1 Dec 2022 22:46:52 +0530 Subject: [PATCH 08/74] chore: update dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b901c80..7be0815 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,9 @@ "@japa/runner": "^2.2.2", "@japa/spec-reporter": "^1.3.2", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.20", + "@swc/core": "^1.3.21", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.11.9", + "@types/node": "^18.11.10", "@types/sinon": "^10.0.13", "argon2": "^0.30.2", "bcrypt": "^5.0.1", @@ -65,7 +65,7 @@ }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.0.1-0" + "@poppinss/utils": "^6.1.0-0" }, "peerDependencies": { "argon2": "^0.30.2", From 46611148c1d5e8b6e841ccb402634cc67caa1c2e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 1 Dec 2022 22:53:37 +0530 Subject: [PATCH 09/74] refactor: remove InvalidHashConfigException in favor of RuntimeException --- src/define_config.ts | 8 ++++---- src/exceptions/invalid_hash_config.ts | 18 ------------------ 2 files changed, 4 insertions(+), 22 deletions(-) delete mode 100644 src/exceptions/invalid_hash_config.ts diff --git a/src/define_config.ts b/src/define_config.ts index 2061ffb..4334819 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -7,8 +7,8 @@ * file that was distributed with this source code. */ +import { RuntimeException } from '@poppinss/utils' import type { ManagerDriversConfig, HashManagerConfig } from './types.js' -import { InvalidHashConfigException } from './exceptions/invalid_hash_config.js' /** * Define configuration for the hash manager @@ -20,7 +20,7 @@ export function defineConfig Date: Thu, 1 Dec 2022 22:53:50 +0530 Subject: [PATCH 10/74] refactor: add debug calls to hash manager --- src/debug.ts | 11 +++++++++++ src/hash_manager.ts | 15 ++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/debug.ts diff --git a/src/debug.ts b/src/debug.ts new file mode 100644 index 0000000..9588893 --- /dev/null +++ b/src/debug.ts @@ -0,0 +1,11 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { debuglog } from 'node:util' +export default debuglog('adonisjs:hash') diff --git a/src/hash_manager.ts b/src/hash_manager.ts index 6f0da7f..26d1bf4 100644 --- a/src/hash_manager.ts +++ b/src/hash_manager.ts @@ -11,6 +11,7 @@ import { Hash } from './hash.js' import { Argon } from './drivers/argon.js' import { Bcrypt } from './drivers/bcrypt.js' import { Scrypt } from './drivers/scrypt.js' + import type { HashDriverContract, HashManagerConfig, @@ -18,7 +19,10 @@ import type { ManagerDriverFactory, ManagerDriversConfig, } from './types.js' + +import debug from './debug.js' import { Fake } from './drivers/fake.js' +import { RuntimeException } from '@poppinss/utils' /** * HashManager implements the manager/builder pattern to create a use multiple @@ -69,6 +73,7 @@ export class HashManager) { this.#config = config + debug('creating hash manager. config: %o', this.#config) } /** @@ -105,7 +110,9 @@ export class HashManager(hasher?: Hasher): Hash { let hasherToUse: keyof KnownHashers | undefined = hasher || this.#config.default if (!hasherToUse) { - throw new Error('Cannot create hash instance. No default hasher is defined in the config') + throw new RuntimeException( + 'Cannot create hash instance. No default hasher is defined in the config' + ) } /** @@ -122,6 +129,7 @@ export class HashManager Date: Fri, 2 Dec 2022 16:19:33 +0530 Subject: [PATCH 11/74] chore: use cross-env to set env variables --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7be0815..7141433 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "scripts": { "pretest": "npm run lint", - "test": "c8 npm run vscode:test", + "test": "cross-env NODE_DEBUG=adonisjs:hash c8 npm run vscode:test", "clean": "del-cli build", "compile": "npm run lint && npm run clean && tsc", "build": "npm run compile", @@ -50,6 +50,7 @@ "argon2": "^0.30.2", "bcrypt": "^5.0.1", "c8": "^7.12.0", + "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", From 7092f9643fd323f11eb09128bc2ed359d098994c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 2 Dec 2022 16:20:32 +0530 Subject: [PATCH 12/74] refactor: reword debug calls --- src/hash_manager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hash_manager.ts b/src/hash_manager.ts index 26d1bf4..2415aa7 100644 --- a/src/hash_manager.ts +++ b/src/hash_manager.ts @@ -73,7 +73,7 @@ export class HashManager) { this.#config = config - debug('creating hash manager. config: %o', this.#config) + debug('creating hash manager. config: %O', this.#config) } /** @@ -137,7 +137,7 @@ export class HashManager Date: Fri, 2 Dec 2022 16:22:32 +0530 Subject: [PATCH 13/74] chore(release): 8.1.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7141433..3145218 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.0.1-0", + "version": "8.1.0-0", "description": "Multi driver hash module with support for PHC string formats", "main": "build/index.js", "type": "module", From 76b4569161962699166903b84c8a6b7d175b39fc Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 10 Dec 2022 14:10:10 +0530 Subject: [PATCH 14/74] chore: update dependencies --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3145218..8d66eb1 100644 --- a/package.json +++ b/package.json @@ -43,26 +43,26 @@ "@japa/runner": "^2.2.2", "@japa/spec-reporter": "^1.3.2", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.21", + "@swc/core": "^1.3.22", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.11.10", + "@types/node": "^18.11.12", "@types/sinon": "^10.0.13", "argon2": "^0.30.2", "bcrypt": "^5.0.1", "c8": "^7.12.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.28.0", + "eslint": "^8.29.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.2.0", "husky": "^8.0.2", "np": "^7.6.2", - "prettier": "^2.7.1", + "prettier": "^2.8.1", "sinon": "^15.0.0", "ts-node": "^10.9.1", - "typescript": "^4.9.3" + "typescript": "^4.9.4" }, "dependencies": { "@phc/format": "^1.0.0", From 915e6442dbd1a95ed32dac88075b207c94bd8ec2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 10 Dec 2022 14:49:08 +0530 Subject: [PATCH 15/74] refactor: convert Hash class to an abstract class --- src/drivers/argon.ts | 7 +++-- src/drivers/bcrypt.ts | 8 ++++-- src/drivers/fake.ts | 4 +-- src/drivers/scrypt.ts | 7 +++-- src/hash.ts | 62 ++++++++++++++++++++++--------------------- src/hash_manager.ts | 4 +-- tests/hash.spec.ts | 38 -------------------------- 7 files changed, 52 insertions(+), 78 deletions(-) delete mode 100644 tests/hash.spec.ts diff --git a/src/drivers/argon.ts b/src/drivers/argon.ts index 4f68774..c098734 100644 --- a/src/drivers/argon.ts +++ b/src/drivers/argon.ts @@ -10,6 +10,7 @@ import type argon2 from 'argon2' import { safeEqual } from '@poppinss/utils' +import { Hash } from '../hash.js' import { PhcFormatter } from '../phc_formatter.js' import { MAX_UINT24, @@ -18,7 +19,7 @@ import { RangeValidator, randomBytesAsync, } from '../helpers.js' -import type { ArgonConfig, ArgonVariants, HashDriverContract } from '../types.js' +import type { ArgonConfig, ArgonVariants } from '../types.js' /** * Hash driver built on top of "argon2" hash algorigthm. Under the hood @@ -34,7 +35,7 @@ import type { ArgonConfig, ArgonVariants, HashDriverContract } from '../types.js * // $argon2id$v=19$t=3,m=4096,p=1$drxJBWzWahR5tMubp+a1Sw$L/Oh2uw6QKW77i/KQ8eGuOt3ui52hEmmKlu1KBVBxiM * ``` */ -export class Argon implements HashDriverContract { +export class Argon extends Hash { /** * Lazily loaded argon2 binding. Since it is a peer dependency * we cannot import it at top level @@ -66,6 +67,8 @@ export class Argon implements HashDriverContract { #ids = ['argon2d', 'argon2i', 'argon2id'] constructor(config: ArgonConfig) { + super() + this.#config = { version: 0x13, variant: 'id', diff --git a/src/drivers/bcrypt.ts b/src/drivers/bcrypt.ts index 92ed9c1..c00e897 100644 --- a/src/drivers/bcrypt.ts +++ b/src/drivers/bcrypt.ts @@ -11,8 +11,10 @@ import * as bcryptBase64 from '../legacy/bcrypt_base64.cjs' import type bcrypt from 'bcrypt' import { safeEqual } from '@poppinss/utils' + +import { Hash } from '../hash.js' +import type { BcryptConfig } from '../types.js' import { PhcFormatter } from '../phc_formatter.js' -import type { HashDriverContract, BcryptConfig } from '../types.js' import { EnumValidator, randomBytesAsync, RangeValidator } from '../helpers.js' /** @@ -29,7 +31,7 @@ import { EnumValidator, randomBytesAsync, RangeValidator } from '../helpers.js' * // $bcrypt$v=98$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8 * ``` */ -export class Bcrypt implements HashDriverContract { +export class Bcrypt extends Hash { /** * Lazily loaded bcrypt binding. Since it is a peer dependency * we cannot import it at top level @@ -47,6 +49,8 @@ export class Bcrypt implements HashDriverContract { #phcFormatter = new PhcFormatter<{ r: number }>() constructor(config: BcryptConfig) { + super() + this.#config = { rounds: 10, saltSize: 16, diff --git a/src/drivers/fake.ts b/src/drivers/fake.ts index cb6ae41..37e05af 100644 --- a/src/drivers/fake.ts +++ b/src/drivers/fake.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import type { HashDriverContract } from '../types.js' +import { Hash } from '../hash.js' /** * The fake implementation does not generate any hash and @@ -16,7 +16,7 @@ import type { HashDriverContract } from '../types.js' * * The fake driver is useful for testing. */ -export class Fake implements HashDriverContract { +export class Fake extends Hash { /** * Always returns true */ diff --git a/src/drivers/scrypt.ts b/src/drivers/scrypt.ts index 097755a..c3f3512 100644 --- a/src/drivers/scrypt.ts +++ b/src/drivers/scrypt.ts @@ -9,8 +9,9 @@ import { safeEqual } from '@poppinss/utils' +import { Hash } from '../hash.js' +import type { ScryptConfig } from '../types.js' import { PhcFormatter } from '../phc_formatter.js' -import type { ScryptConfig, HashDriverContract } from '../types.js' import { randomBytesAsync, RangeValidator, scryptAsync, MAX_UINT32 } from '../helpers.js' /** @@ -27,7 +28,7 @@ import { randomBytesAsync, RangeValidator, scryptAsync, MAX_UINT32 } from '../he * // $scrypt$n=16384,r=8,p=1$iILKD1gVSx6bqualYqyLBQ$DNzIISdmTQS6sFdQ1tJ3UCZ7Uun4uGHNjj0x8FHOqB0pf2LYsu9Xaj5MFhHg21qBz8l5q/oxpeV+ZkgTAj+OzQ * ``` */ -export class Scrypt implements HashDriverContract { +export class Scrypt extends Hash { /** * Config with defaults merged */ @@ -43,6 +44,8 @@ export class Scrypt implements HashDriverContract { }>() constructor(config: ScryptConfig) { + super() + this.#config = { cost: 16384, blockSize: 8, diff --git a/src/hash.ts b/src/hash.ts index 79cd237..d47d99c 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -10,49 +10,51 @@ import type { HashDriverContract } from './types.js' /** - * Hash and verify values using a dedicated hash driver. The Hash - * works as an adapter across different drivers. - * - * ```ts - * const hash = new Hash(new Argon()) - * const hashedPassword = await hash.make('secret') - * - * const isValid = await hash.verify(hashedPassword, 'secret') - * console.log(isValid) - * ``` + * Hash and verify values using a dedicated hash driver. The + * abstract implementation must be extended by the + * implementation drivers */ -export class Hash implements HashDriverContract { - #driver: HashDriverContract - constructor(driver: HashDriverContract) { - this.#driver = driver - } +export abstract class Hash implements HashDriverContract { + constructor() {} /** * Check if the value is a valid hash. This method just checks - * for the formatting of the hash + * for the formatting of the hash. */ - isValidHash(value: string): boolean { - return this.#driver.isValidHash(value) - } + abstract isValidHash(value: string): boolean /** - * Hash plain text value + * Hash a plain text value + * + * ```ts + * const hashedValue = await hash.make('password') + * ``` */ - make(value: string): Promise { - return this.#driver.make(value) - } + abstract make(value: string): Promise /** * Verify the plain text value against an existing hash + * + * ```ts + * if (await hash.verify(hashedValues, plainText)) { + * + * } + * ``` */ - verify(hashedValue: string, plainValue: string): Promise { - return this.#driver.verify(hashedValue, plainValue) - } + abstract verify(hashedValue: string, plainValue: string): Promise /** - * Find if the hash value needs a rehash or not. + * Find if the hash value needs a rehash or not. The rehash is + * required when. + * + * ```ts + * const isValid = await hash.verify(hashedValue, plainText) + * + * // Plain password is valid and hash needs a rehash + * if (isValid && await needs.needsReHash(hashedValue)) { + * const newHashedValue = await hash.make(plainText) + * } + * ``` */ - needsReHash(hashedValue: string): boolean { - return this.#driver.needsReHash(hashedValue) - } + abstract needsReHash(hashedValue: string): boolean } diff --git a/src/hash_manager.ts b/src/hash_manager.ts index 2415aa7..93ddc87 100644 --- a/src/hash_manager.ts +++ b/src/hash_manager.ts @@ -138,7 +138,7 @@ export class HashManager { - test('hash text using a driver', async ({ assert }) => { - const argon = new Argon({}) - const hash = new Hash(argon) - - const hashedValue = await hash.make('secret') - assert.isTrue(hash.isValidHash(hashedValue)) - }) - - test('verify hash using a driver', async ({ assert }) => { - const argon = new Argon({}) - const hash = new Hash(argon) - - const hashedValue = await hash.make('secret') - assert.isTrue(await hash.verify(hashedValue, 'secret')) - }) - - test('check if hash needs to be rehashed using a driver', async ({ assert }) => { - const argon = new Argon({}) - const hash = new Hash(argon) - - const hashedValue = await hash.make('secret') - assert.isFalse(hash.needsReHash(hashedValue)) - }) -}) From 410c7901fbf0affad82076c532f51039232a734e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 10 Dec 2022 14:53:05 +0530 Subject: [PATCH 16/74] Revert "refactor: convert Hash class to an abstract class" This reverts commit 915e6442dbd1a95ed32dac88075b207c94bd8ec2. --- src/drivers/argon.ts | 7 ++--- src/drivers/bcrypt.ts | 8 ++---- src/drivers/fake.ts | 4 +-- src/drivers/scrypt.ts | 7 ++--- src/hash.ts | 62 +++++++++++++++++++++---------------------- src/hash_manager.ts | 4 +-- tests/hash.spec.ts | 38 ++++++++++++++++++++++++++ 7 files changed, 78 insertions(+), 52 deletions(-) create mode 100644 tests/hash.spec.ts diff --git a/src/drivers/argon.ts b/src/drivers/argon.ts index c098734..4f68774 100644 --- a/src/drivers/argon.ts +++ b/src/drivers/argon.ts @@ -10,7 +10,6 @@ import type argon2 from 'argon2' import { safeEqual } from '@poppinss/utils' -import { Hash } from '../hash.js' import { PhcFormatter } from '../phc_formatter.js' import { MAX_UINT24, @@ -19,7 +18,7 @@ import { RangeValidator, randomBytesAsync, } from '../helpers.js' -import type { ArgonConfig, ArgonVariants } from '../types.js' +import type { ArgonConfig, ArgonVariants, HashDriverContract } from '../types.js' /** * Hash driver built on top of "argon2" hash algorigthm. Under the hood @@ -35,7 +34,7 @@ import type { ArgonConfig, ArgonVariants } from '../types.js' * // $argon2id$v=19$t=3,m=4096,p=1$drxJBWzWahR5tMubp+a1Sw$L/Oh2uw6QKW77i/KQ8eGuOt3ui52hEmmKlu1KBVBxiM * ``` */ -export class Argon extends Hash { +export class Argon implements HashDriverContract { /** * Lazily loaded argon2 binding. Since it is a peer dependency * we cannot import it at top level @@ -67,8 +66,6 @@ export class Argon extends Hash { #ids = ['argon2d', 'argon2i', 'argon2id'] constructor(config: ArgonConfig) { - super() - this.#config = { version: 0x13, variant: 'id', diff --git a/src/drivers/bcrypt.ts b/src/drivers/bcrypt.ts index c00e897..92ed9c1 100644 --- a/src/drivers/bcrypt.ts +++ b/src/drivers/bcrypt.ts @@ -11,10 +11,8 @@ import * as bcryptBase64 from '../legacy/bcrypt_base64.cjs' import type bcrypt from 'bcrypt' import { safeEqual } from '@poppinss/utils' - -import { Hash } from '../hash.js' -import type { BcryptConfig } from '../types.js' import { PhcFormatter } from '../phc_formatter.js' +import type { HashDriverContract, BcryptConfig } from '../types.js' import { EnumValidator, randomBytesAsync, RangeValidator } from '../helpers.js' /** @@ -31,7 +29,7 @@ import { EnumValidator, randomBytesAsync, RangeValidator } from '../helpers.js' * // $bcrypt$v=98$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8 * ``` */ -export class Bcrypt extends Hash { +export class Bcrypt implements HashDriverContract { /** * Lazily loaded bcrypt binding. Since it is a peer dependency * we cannot import it at top level @@ -49,8 +47,6 @@ export class Bcrypt extends Hash { #phcFormatter = new PhcFormatter<{ r: number }>() constructor(config: BcryptConfig) { - super() - this.#config = { rounds: 10, saltSize: 16, diff --git a/src/drivers/fake.ts b/src/drivers/fake.ts index 37e05af..cb6ae41 100644 --- a/src/drivers/fake.ts +++ b/src/drivers/fake.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { Hash } from '../hash.js' +import type { HashDriverContract } from '../types.js' /** * The fake implementation does not generate any hash and @@ -16,7 +16,7 @@ import { Hash } from '../hash.js' * * The fake driver is useful for testing. */ -export class Fake extends Hash { +export class Fake implements HashDriverContract { /** * Always returns true */ diff --git a/src/drivers/scrypt.ts b/src/drivers/scrypt.ts index c3f3512..097755a 100644 --- a/src/drivers/scrypt.ts +++ b/src/drivers/scrypt.ts @@ -9,9 +9,8 @@ import { safeEqual } from '@poppinss/utils' -import { Hash } from '../hash.js' -import type { ScryptConfig } from '../types.js' import { PhcFormatter } from '../phc_formatter.js' +import type { ScryptConfig, HashDriverContract } from '../types.js' import { randomBytesAsync, RangeValidator, scryptAsync, MAX_UINT32 } from '../helpers.js' /** @@ -28,7 +27,7 @@ import { randomBytesAsync, RangeValidator, scryptAsync, MAX_UINT32 } from '../he * // $scrypt$n=16384,r=8,p=1$iILKD1gVSx6bqualYqyLBQ$DNzIISdmTQS6sFdQ1tJ3UCZ7Uun4uGHNjj0x8FHOqB0pf2LYsu9Xaj5MFhHg21qBz8l5q/oxpeV+ZkgTAj+OzQ * ``` */ -export class Scrypt extends Hash { +export class Scrypt implements HashDriverContract { /** * Config with defaults merged */ @@ -44,8 +43,6 @@ export class Scrypt extends Hash { }>() constructor(config: ScryptConfig) { - super() - this.#config = { cost: 16384, blockSize: 8, diff --git a/src/hash.ts b/src/hash.ts index d47d99c..79cd237 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -10,51 +10,49 @@ import type { HashDriverContract } from './types.js' /** - * Hash and verify values using a dedicated hash driver. The - * abstract implementation must be extended by the - * implementation drivers + * Hash and verify values using a dedicated hash driver. The Hash + * works as an adapter across different drivers. + * + * ```ts + * const hash = new Hash(new Argon()) + * const hashedPassword = await hash.make('secret') + * + * const isValid = await hash.verify(hashedPassword, 'secret') + * console.log(isValid) + * ``` */ -export abstract class Hash implements HashDriverContract { - constructor() {} +export class Hash implements HashDriverContract { + #driver: HashDriverContract + constructor(driver: HashDriverContract) { + this.#driver = driver + } /** * Check if the value is a valid hash. This method just checks - * for the formatting of the hash. + * for the formatting of the hash */ - abstract isValidHash(value: string): boolean + isValidHash(value: string): boolean { + return this.#driver.isValidHash(value) + } /** - * Hash a plain text value - * - * ```ts - * const hashedValue = await hash.make('password') - * ``` + * Hash plain text value */ - abstract make(value: string): Promise + make(value: string): Promise { + return this.#driver.make(value) + } /** * Verify the plain text value against an existing hash - * - * ```ts - * if (await hash.verify(hashedValues, plainText)) { - * - * } - * ``` */ - abstract verify(hashedValue: string, plainValue: string): Promise + verify(hashedValue: string, plainValue: string): Promise { + return this.#driver.verify(hashedValue, plainValue) + } /** - * Find if the hash value needs a rehash or not. The rehash is - * required when. - * - * ```ts - * const isValid = await hash.verify(hashedValue, plainText) - * - * // Plain password is valid and hash needs a rehash - * if (isValid && await needs.needsReHash(hashedValue)) { - * const newHashedValue = await hash.make(plainText) - * } - * ``` + * Find if the hash value needs a rehash or not. */ - abstract needsReHash(hashedValue: string): boolean + needsReHash(hashedValue: string): boolean { + return this.#driver.needsReHash(hashedValue) + } } diff --git a/src/hash_manager.ts b/src/hash_manager.ts index 93ddc87..2415aa7 100644 --- a/src/hash_manager.ts +++ b/src/hash_manager.ts @@ -138,7 +138,7 @@ export class HashManager { + test('hash text using a driver', async ({ assert }) => { + const argon = new Argon({}) + const hash = new Hash(argon) + + const hashedValue = await hash.make('secret') + assert.isTrue(hash.isValidHash(hashedValue)) + }) + + test('verify hash using a driver', async ({ assert }) => { + const argon = new Argon({}) + const hash = new Hash(argon) + + const hashedValue = await hash.make('secret') + assert.isTrue(await hash.verify(hashedValue, 'secret')) + }) + + test('check if hash needs to be rehashed using a driver', async ({ assert }) => { + const argon = new Argon({}) + const hash = new Hash(argon) + + const hashedValue = await hash.make('secret') + assert.isFalse(hash.needsReHash(hashedValue)) + }) +}) From 13d1fff355bd5f39d06f9252fff49b55a0e1bb0b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 6 Jan 2023 18:57:00 +0530 Subject: [PATCH 17/74] chore: update dependencies --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 8d66eb1..24368df 100644 --- a/package.json +++ b/package.json @@ -35,38 +35,38 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@commitlint/cli": "^17.3.0", - "@commitlint/config-conventional": "^17.3.0", + "@commitlint/cli": "^17.4.0", + "@commitlint/config-conventional": "^17.4.0", "@japa/assert": "^1.3.6", "@japa/expect-type": "^1.0.2", "@japa/run-failed-tests": "^1.1.0", "@japa/runner": "^2.2.2", "@japa/spec-reporter": "^1.3.2", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.22", + "@swc/core": "^1.3.25", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.11.12", + "@types/node": "^18.11.18", "@types/sinon": "^10.0.13", - "argon2": "^0.30.2", + "argon2": "^0.30.3", "bcrypt": "^5.0.1", "c8": "^7.12.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.29.0", - "eslint-config-prettier": "^8.5.0", + "eslint": "^8.31.0", + "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.2.0", - "husky": "^8.0.2", - "np": "^7.6.2", + "husky": "^8.0.3", + "np": "^7.6.3", "prettier": "^2.8.1", - "sinon": "^15.0.0", + "sinon": "^15.0.1", "ts-node": "^10.9.1", "typescript": "^4.9.4" }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.1.0-0" + "@poppinss/utils": "^6.3.1-0" }, "peerDependencies": { "argon2": "^0.30.2", From b119eb2805bc1d110830e3fd36d6659d930a7630 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 6 Jan 2023 19:02:05 +0530 Subject: [PATCH 18/74] docs(README): fix badge url for github workflow --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 09218d7..ccfbfcf 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ In order to ensure that the AdonisJS community is welcoming to all, please revie ## License AdonisJS hash is open-sourced software licensed under the [MIT license](LICENSE.md). -[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/hash/test?style=for-the-badge +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/hash/test.yml?style=for-the-badge [gh-workflow-url]: https://github.com/adonisjs/hash/actions/workflows/test.yml "Github action" [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript From 8e24d9ade70a92f6e0709a3d82f4c91ea7fb0444 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 12 Jan 2023 12:18:56 +0530 Subject: [PATCH 19/74] chore: update dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 24368df..412617d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@commitlint/cli": "^17.4.0", + "@commitlint/cli": "^17.4.1", "@commitlint/config-conventional": "^17.4.0", "@japa/assert": "^1.3.6", "@japa/expect-type": "^1.0.2", @@ -43,7 +43,7 @@ "@japa/runner": "^2.2.2", "@japa/spec-reporter": "^1.3.2", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.25", + "@swc/core": "^1.3.26", "@types/bcrypt": "^5.0.0", "@types/node": "^18.11.18", "@types/sinon": "^10.0.13", @@ -59,7 +59,7 @@ "github-label-sync": "^2.2.0", "husky": "^8.0.3", "np": "^7.6.3", - "prettier": "^2.8.1", + "prettier": "^2.8.2", "sinon": "^15.0.1", "ts-node": "^10.9.1", "typescript": "^4.9.4" @@ -69,7 +69,7 @@ "@poppinss/utils": "^6.3.1-0" }, "peerDependencies": { - "argon2": "^0.30.2", + "argon2": "^0.30.3", "bcrypt": "^5.0.1" }, "peerDependenciesMeta": { From a4ebfd66410f15c92977edf17571b2a20f6a3bfa Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 12 Jan 2023 12:24:32 +0530 Subject: [PATCH 20/74] fix: use hasher key to cache hash instances --- src/hash_manager.ts | 12 ++++++------ tests/hash_manager.spec.ts | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/hash_manager.ts b/src/hash_manager.ts index 2415aa7..dfefe11 100644 --- a/src/hash_manager.ts +++ b/src/hash_manager.ts @@ -59,7 +59,7 @@ export class HashManager> = {} + #hashersCache: Partial> = {} /** * Drivers implementations. Cannot be async, since the "use" @@ -94,7 +94,7 @@ export class HashManager { argon: { driver: 'argon2', }, + argon1: { + driver: 'argon2', + }, bcrypt: { driver: 'bcrypt', }, + bcrypt1: { + driver: 'bcrypt', + }, scrypt: { driver: 'scrypt', }, + scrypt1: { + driver: 'scrypt', + }, }, }) expectTypeOf(manager.use) .parameter(0) - .toEqualTypeOf<'argon' | 'scrypt' | 'bcrypt' | undefined>() + .toEqualTypeOf<'argon' | 'argon1' | 'scrypt' | 'scrypt1' | 'bcrypt' | 'bcrypt1' | undefined>() expectTypeOf(manager.use('argon')).toEqualTypeOf() expectTypeOf(manager.use('bcrypt')).toEqualTypeOf() expectTypeOf(manager.use('scrypt')).toEqualTypeOf() + expectTypeOf(manager.use('argon1')).toEqualTypeOf() + expectTypeOf(manager.use('bcrypt1')).toEqualTypeOf() + expectTypeOf(manager.use('scrypt1')).toEqualTypeOf() assert.strictEqual(manager.use('argon'), manager.use('argon')) + assert.notStrictEqual(manager.use('argon'), manager.use('argon1')) + assert.strictEqual(manager.use('bcrypt'), manager.use('bcrypt')) + assert.notStrictEqual(manager.use('bcrypt'), manager.use('bcrypt1')) + assert.strictEqual(manager.use('scrypt'), manager.use('scrypt')) + assert.notStrictEqual(manager.use('scrypt'), manager.use('scrypt1')) }) test('use default hasher', ({ assert }) => { From 19bebf369856b48098f7ff1fe62b92fadab83397 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 12 Jan 2023 12:55:50 +0530 Subject: [PATCH 21/74] feat: add hash factory for testing --- package.json | 4 ++- test_factories/hash_manager.ts | 53 ++++++++++++++++++++++++++++++ tests/hash_maanger_factory.spec.ts | 42 +++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 test_factories/hash_manager.ts create mode 100644 tests/hash_maanger_factory.spec.ts diff --git a/package.json b/package.json index 412617d..b685973 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,14 @@ "type": "module", "files": [ "build/src", + "build/test_factories", "build/index.d.ts", "build/index.js" ], "exports": { ".": "./build/index.js", - "./types": "./build/src/types.js" + "./types": "./build/src/types.js", + "./test_factories/*": "./build/test_factories/*.js" }, "scripts": { "pretest": "npm run lint", diff --git a/test_factories/hash_manager.ts b/test_factories/hash_manager.ts new file mode 100644 index 0000000..607604a --- /dev/null +++ b/test_factories/hash_manager.ts @@ -0,0 +1,53 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { HashManager } from '../index.js' +import type { HashManagerConfig, ManagerDriversConfig } from '../src/types.js' + +/** + * Hash manager factory is used to create an instance of hash manager + * for testing + */ +export class HashMangerFactory< + KnownHashers extends Record = { scrypt: { driver: 'scrypt' } } +> { + /** + * Config accepted by hash manager + */ + #config: HashManagerConfig + + constructor(config?: HashManagerConfig) { + this.#config = + config || + ({ + default: 'scrypt', + list: { + scrypt: { + driver: 'scrypt', + }, + }, + } as unknown as HashManagerConfig) + } + + /** + * Merge factory parameters + */ + merge>(options: { + config: HashManagerConfig + }): HashMangerFactory { + return new HashMangerFactory(options.config) + } + + /** + * Create hash manager instance + */ + create() { + return new HashManager(this.#config) + } +} diff --git a/tests/hash_maanger_factory.spec.ts b/tests/hash_maanger_factory.spec.ts new file mode 100644 index 0000000..1e32e00 --- /dev/null +++ b/tests/hash_maanger_factory.spec.ts @@ -0,0 +1,42 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { Bcrypt, Hash, Scrypt } from '../index.js' +import { HashMangerFactory } from '../test_factories/hash_manager.js' + +test.group('Hash manager factory', () => { + test('create instance of hash manager using factory', async ({ assert, expectTypeOf }) => { + const hash = new HashMangerFactory().create() + + assert.instanceOf(hash.use(), Hash) + expectTypeOf(hash.use()).toMatchTypeOf() + + const hashedValue = await hash.use().make('secret') + assert.isTrue(await new Scrypt({}).verify(hashedValue, 'secret')) + }) + + test('create instance of hash manager with custom config', async ({ assert }) => { + const hash = new HashMangerFactory() + .merge({ + config: { + default: 'bcrypt', + list: { + bcrypt: { + driver: 'bcrypt', + }, + }, + }, + }) + .create() + + const hashedValue = await hash.use().make('secret') + assert.isTrue(await new Bcrypt({}).verify(hashedValue, 'secret')) + }) +}) From 9c80d45893672e56b8ae0cddc770eb9eef752b94 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 12 Jan 2023 12:59:15 +0530 Subject: [PATCH 22/74] chore(release): 8.2.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b685973..922f60d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.1.0-0", + "version": "8.2.0-0", "description": "Multi driver hash module with support for PHC string formats", "main": "build/index.js", "type": "module", From d528e72601c89c209c4056b1e52b2210abd3d248 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 16 Jan 2023 17:44:03 +0530 Subject: [PATCH 23/74] chore: update dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 922f60d..e87010d 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@commitlint/cli": "^17.4.1", - "@commitlint/config-conventional": "^17.4.0", + "@commitlint/cli": "^17.4.2", + "@commitlint/config-conventional": "^17.4.2", "@japa/assert": "^1.3.6", "@japa/expect-type": "^1.0.2", "@japa/run-failed-tests": "^1.1.0", @@ -54,14 +54,14 @@ "c8": "^7.12.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.31.0", + "eslint": "^8.32.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.2.0", "husky": "^8.0.3", "np": "^7.6.3", - "prettier": "^2.8.2", + "prettier": "^2.8.3", "sinon": "^15.0.1", "ts-node": "^10.9.1", "typescript": "^4.9.4" From d8fdc2aad73d9f7b3c521e3b66f8ecf6d1fdcce7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 17 Jan 2023 10:29:02 +0530 Subject: [PATCH 24/74] refactor: remove defineConfig helper and global list of drivers Breaking change: Both `defineConfig` and global list of drivers are AdonisJS apps specific and therefore will be added by the core --- index.ts | 1 - src/define_config.ts | 46 ----- src/hash_manager.ts | 93 ++-------- src/types.ts | 42 +---- test_factories/hash_manager.ts | 31 ++-- tests/define_config.spec.ts | 59 ------ tests/hash_manager.spec.ts | 172 +++--------------- ...y.spec.ts => hash_manager_factory.spec.ts} | 10 +- 8 files changed, 62 insertions(+), 392 deletions(-) delete mode 100644 src/define_config.ts delete mode 100644 tests/define_config.spec.ts rename tests/{hash_maanger_factory.spec.ts => hash_manager_factory.spec.ts} (87%) diff --git a/index.ts b/index.ts index 6efed4c..e445a6e 100644 --- a/index.ts +++ b/index.ts @@ -12,5 +12,4 @@ export { Argon } from './src/drivers/argon.js' export { Bcrypt } from './src/drivers/bcrypt.js' export { Scrypt } from './src/drivers/scrypt.js' export { HashManager } from './src/hash_manager.js' -export { defineConfig } from './src/define_config.js' export { PhcFormatter } from './src/phc_formatter.js' diff --git a/src/define_config.ts b/src/define_config.ts deleted file mode 100644 index 4334819..0000000 --- a/src/define_config.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { RuntimeException } from '@poppinss/utils' -import type { ManagerDriversConfig, HashManagerConfig } from './types.js' - -/** - * Define configuration for the hash manager - */ -export function defineConfig>( - config: HashManagerConfig -): HashManagerConfig { - /** - * List should always be provided - */ - if (!config.list) { - throw new RuntimeException('Missing "list" property in hash config') - } - - /** - * Default property should be provided when list - * has one or more items - */ - if (Object.keys(config.list).length && !config.default) { - throw new RuntimeException( - 'Missing "default" property in hash config. Specify a default hasher' - ) - } - - /** - * The default hasher should be mentioned in the list - */ - if (config.default && !config.list[config.default]) { - throw new RuntimeException( - `Missing "list.${String(config.default)}". It is referenced by the "default" property` - ) - } - - return config -} diff --git a/src/hash_manager.ts b/src/hash_manager.ts index dfefe11..c76cb0d 100644 --- a/src/hash_manager.ts +++ b/src/hash_manager.ts @@ -7,22 +7,12 @@ * file that was distributed with this source code. */ -import { Hash } from './hash.js' -import { Argon } from './drivers/argon.js' -import { Bcrypt } from './drivers/bcrypt.js' -import { Scrypt } from './drivers/scrypt.js' - -import type { - HashDriverContract, - HashManagerConfig, - HashManagerDrivers, - ManagerDriverFactory, - ManagerDriversConfig, -} from './types.js' +import { RuntimeException } from '@poppinss/utils' import debug from './debug.js' +import { Hash } from './hash.js' import { Fake } from './drivers/fake.js' -import { RuntimeException } from '@poppinss/utils' +import type { HashDriverContract, ManagerDriverFactory } from './types.js' /** * HashManager implements the manager/builder pattern to create a use multiple @@ -32,24 +22,23 @@ import { RuntimeException } from '@poppinss/utils' * const manager = new HashManager({ * default: 'argon', * list: { - * argon: { - * driver: 'argon2', - * }, - * bcrypt: { - * driver: 'bcrypt', - * } + * argon: () => new ArgonDriver(), + * bcrypt: () => new BcryptDriver(), * } * }) * ``` */ -export class HashManager> +export class HashManager> implements HashDriverContract { /** * Hash manager config with the list of hashers in * use */ - #config: HashManagerConfig + #config: { + default?: keyof KnownHashers + list: KnownHashers + } /** * Fake hasher @@ -61,17 +50,7 @@ export class HashManager> = {} - /** - * Drivers implementations. Cannot be async, since the "use" - * method is not async - */ - #drivers: { [Driver in keyof HashManagerDrivers]?: ManagerDriverFactory } = { - bcrypt: (config) => new Bcrypt(config), - argon2: (config) => new Argon(config), - scrypt: (config) => new Scrypt(config), - } - - constructor(config: HashManagerConfig) { + constructor(config: { default?: keyof KnownHashers; list: KnownHashers }) { this.#config = config debug('creating hash manager. config: %O', this.#config) } @@ -79,24 +58,10 @@ export class HashManager( - name: Driver, - config: { driver: Driver } & HashManagerDrivers[Driver]['config'] - ): HashManagerDrivers[Driver]['implementation'] { - /** - * Ensure the driver exists - */ - const driver = this.#drivers[name] - if (!driver) { - throw new Error( - `Unknown hash driver "${name}". Make sure the driver is registered with HashManager` - ) - } - - /** - * Create an instance of the driver - */ - return driver(config) + #createDriver( + factory: DriverFactory + ): ReturnType { + return factory() as ReturnType } /** @@ -131,14 +96,14 @@ export class HashManager { - * return new Bcrypt(config) - * }) - * ``` - */ - extend( - driver: Driver, - factory: ManagerDriverFactory - ) { - /** - * Using any because of this issue - * https://github.com/microsoft/TypeScript/issues/13995 - */ - debug('adding custom driver %s', driver) - this.#drivers[driver] = factory as any - } - /** * Check if the value is a valid hash. This method just checks * for the formatting of the hash diff --git a/src/types.ts b/src/types.ts index 47c2337..f488119 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,10 +7,6 @@ * file that was distributed with this source code. */ -import type { Argon } from './drivers/argon.js' -import type { Bcrypt } from './drivers/bcrypt.js' -import type { Scrypt } from './drivers/scrypt.js' - /** * The contract Hash drivers should adhere to */ @@ -189,45 +185,9 @@ export type ScryptConfig = { keyLength?: number } -/** - * Known hash drivers. One can extend the interface to add - * custom drivers as well - */ -export interface HashManagerDrivers { - bcrypt: { - config: BcryptConfig - implementation: Bcrypt - } - argon2: { - config: ArgonConfig - implementation: Argon - } - scrypt: { - config: ScryptConfig - implementation: Scrypt - } -} - -/** - * Union of config extracted from known hash drivers - */ -export type ManagerDriversConfig = { - [K in keyof HashManagerDrivers]: { driver: K } & HashManagerDrivers[K]['config'] -}[keyof HashManagerDrivers] - /** * Factory function to return the driver implementation. The method * cannot be async, because the API that calls this method is not * async in first place. */ -export type ManagerDriverFactory = ( - config: { driver: K } & HashManagerDrivers[K]['config'] -) => HashManagerDrivers[K]['implementation'] - -/** - * Config accepted by the hash manager - */ -export type HashManagerConfig> = { - default?: keyof KnownHashers - list: KnownHashers -} +export type ManagerDriverFactory = () => HashDriverContract diff --git a/test_factories/hash_manager.ts b/test_factories/hash_manager.ts index 607604a..c324eee 100644 --- a/test_factories/hash_manager.ts +++ b/test_factories/hash_manager.ts @@ -7,41 +7,46 @@ * file that was distributed with this source code. */ -import { HashManager } from '../index.js' -import type { HashManagerConfig, ManagerDriversConfig } from '../src/types.js' +import { HashManager, Scrypt } from '../index.js' +import { ManagerDriverFactory } from '../src/types.js' + +type Config> = { + default?: keyof KnownHashers + list: KnownHashers +} /** * Hash manager factory is used to create an instance of hash manager * for testing */ export class HashMangerFactory< - KnownHashers extends Record = { scrypt: { driver: 'scrypt' } } + KnownHashers extends Record = { + scrypt: () => Scrypt + } > { /** * Config accepted by hash manager */ - #config: HashManagerConfig + #config: Config - constructor(config?: HashManagerConfig) { + constructor(config?: { default?: keyof KnownHashers; list: KnownHashers }) { this.#config = config || ({ default: 'scrypt', list: { - scrypt: { - driver: 'scrypt', - }, + scrypt: () => new Scrypt({}), }, - } as unknown as HashManagerConfig) + } as unknown as Config) } /** * Merge factory parameters */ - merge>(options: { - config: HashManagerConfig - }): HashMangerFactory { - return new HashMangerFactory(options.config) + merge>( + config: Config + ): HashMangerFactory { + return new HashMangerFactory(config) } /** diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts deleted file mode 100644 index 9bc0547..0000000 --- a/tests/define_config.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * @adonisjs/hash - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { defineConfig } from '../src/define_config.js' - -test.group('Define config', () => { - test('define config for hash manager', async ({ assert }) => { - const config = defineConfig({ - default: 'bcrypt', - list: { - bcrypt: { - driver: 'bcrypt', - rounds: 10, - }, - }, - }) - - assert.deepEqual(config, { - default: 'bcrypt', - list: { - bcrypt: { - driver: 'bcrypt', - rounds: 10, - }, - }, - }) - }) - - test('fail when list is not defined', async ({ assert }) => { - // @ts-expect-error - assert.throws(() => defineConfig({}), 'Missing "list" property in hash config') - }) - - test('allow empty list', async ({ assert }) => { - assert.deepEqual(defineConfig({ list: {} }), { list: {} }) - }) - - test('fail when default property is not mentioned in the list', async ({ assert }) => { - assert.throws( - // @ts-expect-error - () => defineConfig({ default: 'bcrypt', list: {} }), - 'Missing "list.bcrypt". It is referenced by the "default" property' - ) - }) - - test('fail when default property is not defined but list has hashers', async ({ assert }) => { - assert.throws( - () => defineConfig({ list: { bcrypt: { driver: 'bcrypt' } } }), - 'Missing "default" property in hash config. Specify a default hasher' - ) - }) -}) diff --git a/tests/hash_manager.spec.ts b/tests/hash_manager.spec.ts index 64357f4..21e11f8 100644 --- a/tests/hash_manager.spec.ts +++ b/tests/hash_manager.spec.ts @@ -7,26 +7,19 @@ * file that was distributed with this source code. */ -import sinon from 'sinon' import { test } from '@japa/runner' import { Hash } from '../src/hash.js' import { HashManager } from '../src/hash_manager.js' -import { HashDriverContract } from '../src/types.js' +import { Argon, Bcrypt, Scrypt } from '../index.js' test.group('Hash manager', () => { test('create hash instance from the manager', ({ assert, expectTypeOf }) => { const manager = new HashManager({ default: 'argon', list: { - argon: { - driver: 'argon2', - }, - bcrypt: { - driver: 'bcrypt', - }, - scrypt: { - driver: 'scrypt', - }, + argon: () => new Argon({}), + bcrypt: () => new Bcrypt({}), + scrypt: () => new Scrypt({}), }, }) @@ -47,24 +40,12 @@ test.group('Hash manager', () => { const manager = new HashManager({ default: 'argon', list: { - argon: { - driver: 'argon2', - }, - argon1: { - driver: 'argon2', - }, - bcrypt: { - driver: 'bcrypt', - }, - bcrypt1: { - driver: 'bcrypt', - }, - scrypt: { - driver: 'scrypt', - }, - scrypt1: { - driver: 'scrypt', - }, + argon: () => new Argon({}), + argon1: () => new Argon({}), + bcrypt: () => new Bcrypt({}), + bcrypt1: () => new Bcrypt({}), + scrypt: () => new Scrypt({}), + scrypt1: () => new Scrypt({}), }, }) @@ -93,15 +74,9 @@ test.group('Hash manager', () => { const manager = new HashManager({ default: 'argon', list: { - argon: { - driver: 'argon2', - }, - bcrypt: { - driver: 'bcrypt', - }, - scrypt: { - driver: 'scrypt', - }, + argon: () => new Argon({}), + bcrypt: () => new Bcrypt({}), + scrypt: () => new Scrypt({}), }, }) @@ -113,15 +88,9 @@ test.group('Hash manager', () => { test('fail when default hasher is not configured', ({ assert }) => { const manager = new HashManager({ list: { - argon: { - driver: 'argon2', - }, - bcrypt: { - driver: 'bcrypt', - }, - scrypt: { - driver: 'scrypt', - }, + argon: () => new Argon({}), + bcrypt: () => new Bcrypt({}), + scrypt: () => new Scrypt({}), }, }) @@ -131,39 +100,13 @@ test.group('Hash manager', () => { ) }) - test('fail when driver is unknown', ({ assert, expectTypeOf }) => { - const manager = new HashManager({ - default: 'pdkf', - list: { - pdkf: { - // @ts-expect-error - driver: 'pdkf', - }, - }, - }) - - assert.throws( - () => manager.use('pdkf'), - 'Unknown hash driver "pdkf". Make sure the driver is registered with HashManager' - ) - - // @ts-expect-error - expectTypeOf(manager.use).parameters.toEqualTypeOf<['pdkf']>() - }) - test('fake all hashers', async ({ assert }) => { const manager = new HashManager({ default: 'argon', list: { - argon: { - driver: 'argon2', - }, - bcrypt: { - driver: 'bcrypt', - }, - scrypt: { - driver: 'scrypt', - }, + argon: () => new Argon({}), + bcrypt: () => new Bcrypt({}), + scrypt: () => new Scrypt({}), }, }) @@ -178,78 +121,11 @@ test.group('Hash manager', () => { assert.notEqual(await manager.use('scrypt').make('hello-world'), 'hello-world') }) - test('extend to add custom drivers', async ({ assert, cleanup }) => { - const manager = new HashManager({ - default: 'passwords', - list: { - passwords: { - // @ts-expect-error - driver: 'md5', - }, - }, - }) - - class Md5 implements HashDriverContract { - async make(value: string): Promise { - return value - } - - async verify(hashedValue: string, plainValue: string): Promise { - return hashedValue === plainValue - } - - isValidHash(value: string): boolean { - return !!value - } - - needsReHash(hashedValue: string): boolean { - return !hashedValue - } - } - const md5 = new Md5() - const make = sinon.spy(md5, 'make') - const verify = sinon.spy(md5, 'verify') - const needsReHash = sinon.spy(md5, 'needsReHash') - const isValidHash = sinon.spy(md5, 'isValidHash') - - cleanup(() => { - make.restore() - verify.restore() - needsReHash.restore() - isValidHash.restore() - }) - - // @ts-expect-error - manager.extend('md5', () => md5) - - await manager.use('passwords').make('hello-world') - assert.isTrue(make.calledOnce) - assert.isTrue(make.calledWith('hello-world')) - assert.equal(await make.returnValues[0], 'hello-world') - - await manager.use('passwords').verify('hello-world', 'hello-world') - assert.isTrue(verify.calledOnce) - assert.isTrue(verify.calledWith('hello-world', 'hello-world')) - assert.equal(await verify.returnValues[0], true) - - manager.use('passwords').needsReHash('hello-world') - assert.isTrue(needsReHash.calledOnce) - assert.isTrue(needsReHash.calledWith('hello-world')) - assert.equal(needsReHash.returnValues[0], false) - - manager.use('passwords').isValidHash('hello-world') - assert.isTrue(isValidHash.calledOnce) - assert.isTrue(isValidHash.calledWith('hello-world')) - assert.equal(isValidHash.returnValues[0], true) - }) - test('hash text using the default driver', async ({ assert }) => { const manager = new HashManager({ default: 'argon', list: { - argon: { - driver: 'argon2', - }, + argon: () => new Argon({}), }, }) @@ -261,9 +137,7 @@ test.group('Hash manager', () => { const manager = new HashManager({ default: 'argon', list: { - argon: { - driver: 'argon2', - }, + argon: () => new Argon({}), }, }) @@ -275,9 +149,7 @@ test.group('Hash manager', () => { const manager = new HashManager({ default: 'argon', list: { - argon: { - driver: 'argon2', - }, + argon: () => new Argon({}), }, }) diff --git a/tests/hash_maanger_factory.spec.ts b/tests/hash_manager_factory.spec.ts similarity index 87% rename from tests/hash_maanger_factory.spec.ts rename to tests/hash_manager_factory.spec.ts index 1e32e00..ca3a56f 100644 --- a/tests/hash_maanger_factory.spec.ts +++ b/tests/hash_manager_factory.spec.ts @@ -25,13 +25,9 @@ test.group('Hash manager factory', () => { test('create instance of hash manager with custom config', async ({ assert }) => { const hash = new HashMangerFactory() .merge({ - config: { - default: 'bcrypt', - list: { - bcrypt: { - driver: 'bcrypt', - }, - }, + default: 'bcrypt', + list: { + bcrypt: () => new Bcrypt({}), }, }) .create() From 4b08606ea127bdad6c77f79ee496f5187877d64f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 17 Jan 2023 10:35:06 +0530 Subject: [PATCH 25/74] chore(release): 8.3.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e87010d..ca95ad7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.2.0-0", + "version": "8.3.0-0", "description": "Multi driver hash module with support for PHC string formats", "main": "build/index.js", "type": "module", From e999b763bbfb070bc3126dd3b922e0d670c1ca09 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 23 Jan 2023 21:25:17 +0530 Subject: [PATCH 26/74] chore: update dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca95ad7..af8397a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.3.1-0" + "@poppinss/utils": "^6.4.0-0" }, "peerDependencies": { "argon2": "^0.30.3", From 59c6da7e9daee8c18985576bd285173897ab6a0d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 27 Jan 2023 00:06:20 +0530 Subject: [PATCH 27/74] chore: update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index af8397a..e00a618 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@japa/runner": "^2.2.2", "@japa/spec-reporter": "^1.3.2", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.26", + "@swc/core": "^1.3.29", "@types/bcrypt": "^5.0.0", "@types/node": "^18.11.18", "@types/sinon": "^10.0.13", @@ -68,7 +68,7 @@ }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.4.0-0" + "@poppinss/utils": "^6.5.0-0" }, "peerDependencies": { "argon2": "^0.30.3", From 6790c0a96b37f07e1dd2556978f515b559a2b058 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 27 Jan 2023 00:07:11 +0530 Subject: [PATCH 28/74] docs(LICENSE): update license meta --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 1c19428..59a3cfa 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License -Copyright 2022 Harminder Virk, contributors +Copyright (c) 2023 AdonisJS Framework Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From 2af5152145cc0a3ec4e61a9f2124122360d95298 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 27 Jan 2023 00:08:27 +0530 Subject: [PATCH 29/74] refactor: expose all factories from the factories sub-path --- package.json | 2 +- test_factories/main.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 test_factories/main.ts diff --git a/package.json b/package.json index e00a618..6744cd3 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "exports": { ".": "./build/index.js", "./types": "./build/src/types.js", - "./test_factories/*": "./build/test_factories/*.js" + "./factories": "./build/test_factories/main.js" }, "scripts": { "pretest": "npm run lint", diff --git a/test_factories/main.ts b/test_factories/main.ts new file mode 100644 index 0000000..729a7ca --- /dev/null +++ b/test_factories/main.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/hash + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { HashMangerFactory } from './hash_manager.js' From 2d23a604e73df1b09b146b1df5fba207a93f85e1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 27 Jan 2023 00:12:02 +0530 Subject: [PATCH 30/74] chore(release): 8.3.1-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6744cd3..bf43f49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.0-0", + "version": "8.3.1-0", "description": "Multi driver hash module with support for PHC string formats", "main": "build/index.js", "type": "module", From 93804e531870e5695292f5e3debf257e6f85394b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 3 Feb 2023 11:29:37 +0530 Subject: [PATCH 31/74] chore: update dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index bf43f49..c506233 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@japa/runner": "^2.2.2", "@japa/spec-reporter": "^1.3.2", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.29", + "@swc/core": "^1.3.32", "@types/bcrypt": "^5.0.0", "@types/node": "^18.11.18", "@types/sinon": "^10.0.13", @@ -54,7 +54,7 @@ "c8": "^7.12.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.32.0", + "eslint": "^8.33.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", @@ -64,7 +64,7 @@ "prettier": "^2.8.3", "sinon": "^15.0.1", "ts-node": "^10.9.1", - "typescript": "^4.9.4" + "typescript": "^4.9.5" }, "dependencies": { "@phc/format": "^1.0.0", From 5f7b5b45bde49b17a2c7fffad94c29dfc169f27a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 13 Feb 2023 13:31:34 +0530 Subject: [PATCH 32/74] chore: update dependencies --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index c506233..0163312 100644 --- a/package.json +++ b/package.json @@ -39,29 +39,29 @@ "devDependencies": { "@commitlint/cli": "^17.4.2", "@commitlint/config-conventional": "^17.4.2", - "@japa/assert": "^1.3.6", - "@japa/expect-type": "^1.0.2", - "@japa/run-failed-tests": "^1.1.0", - "@japa/runner": "^2.2.2", - "@japa/spec-reporter": "^1.3.2", + "@japa/assert": "^1.4.1", + "@japa/expect-type": "^1.0.3", + "@japa/run-failed-tests": "^1.1.1", + "@japa/runner": "^2.3.0", + "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.32", + "@swc/core": "^1.3.35", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.11.18", + "@types/node": "^18.13.0", "@types/sinon": "^10.0.13", "argon2": "^0.30.3", "bcrypt": "^5.0.1", "c8": "^7.12.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.33.0", + "eslint": "^8.34.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.2.0", "husky": "^8.0.3", "np": "^7.6.3", - "prettier": "^2.8.3", + "prettier": "^2.8.4", "sinon": "^15.0.1", "ts-node": "^10.9.1", "typescript": "^4.9.5" From 585d76d4e6a938539622c1d46629336d3e11b9f5 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 20 Feb 2023 09:31:20 +0530 Subject: [PATCH 33/74] chore: update dependencies --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0163312..269ac67 100644 --- a/package.json +++ b/package.json @@ -37,21 +37,21 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@commitlint/cli": "^17.4.2", - "@commitlint/config-conventional": "^17.4.2", + "@commitlint/cli": "^17.4.4", + "@commitlint/config-conventional": "^17.4.4", "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", "@japa/run-failed-tests": "^1.1.1", - "@japa/runner": "^2.3.0", + "@japa/runner": "^2.5.0", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", "@swc/core": "^1.3.35", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.13.0", + "@types/node": "^18.14.0", "@types/sinon": "^10.0.13", "argon2": "^0.30.3", "bcrypt": "^5.0.1", - "c8": "^7.12.0", + "c8": "^7.13.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.34.0", From 1afa2e405e0a4737a754ab150732107e0df45fd1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 4 Mar 2023 08:34:48 +0530 Subject: [PATCH 34/74] chore: update dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 269ac67..4927833 100644 --- a/package.json +++ b/package.json @@ -42,19 +42,19 @@ "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", "@japa/run-failed-tests": "^1.1.1", - "@japa/runner": "^2.5.0", + "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.35", + "@swc/core": "^1.3.37", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.14.0", + "@types/node": "^18.14.6", "@types/sinon": "^10.0.13", "argon2": "^0.30.3", "bcrypt": "^5.0.1", "c8": "^7.13.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.34.0", + "eslint": "^8.35.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", From b35f339b4b58b08aa69498d1e87475af6926f945 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 10 Mar 2023 11:15:18 +0530 Subject: [PATCH 35/74] chore: update dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4927833..a3c22fc 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,9 @@ "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.37", + "@swc/core": "^1.3.38", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.14.6", + "@types/node": "^18.15.0", "@types/sinon": "^10.0.13", "argon2": "^0.30.3", "bcrypt": "^5.0.1", @@ -55,10 +55,10 @@ "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.35.0", - "eslint-config-prettier": "^8.6.0", + "eslint-config-prettier": "^8.7.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", - "github-label-sync": "^2.2.0", + "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^7.6.3", "prettier": "^2.8.4", From 33199afbe38c377ed78ab9d27d9dd0838a9d39d7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 10 Mar 2023 11:37:29 +0530 Subject: [PATCH 36/74] feat: add assertions methods to hash --- src/hash.ts | 33 +++++++++++++++++++++++++++++++++ src/hash_manager.ts | 14 ++++++++++++++ tests/hash.spec.ts | 18 ++++++++++++++++++ tests/hash_manager.spec.ts | 22 ++++++++++++++++++++++ 4 files changed, 87 insertions(+) diff --git a/src/hash.ts b/src/hash.ts index 79cd237..8ad832c 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import { AssertionError } from 'node:assert' import type { HashDriverContract } from './types.js' /** @@ -55,4 +56,36 @@ export class Hash implements HashDriverContract { needsReHash(hashedValue: string): boolean { return this.#driver.needsReHash(hashedValue) } + + /** + * Assert the plain value passes the hash verification + */ + async assertEquals(hashedValue: string, plainValue: string): Promise { + const isEqual = await this.#driver.verify(hashedValue, plainValue) + if (!isEqual) { + throw new AssertionError({ + message: `Expected "${plainValue}" to pass hash verification`, + expected: true, + actual: false, + operator: 'strictEqual', + stackStartFn: this.assertEquals, + }) + } + } + + /** + * Assert the plain value fails the hash verification + */ + async assertNotEquals(hashedValue: string, plainValue: string): Promise { + const isEqual = await this.#driver.verify(hashedValue, plainValue) + if (isEqual) { + throw new AssertionError({ + message: `Expected "${plainValue}" to fail hash verification`, + expected: false, + actual: true, + operator: 'strictEqual', + stackStartFn: this.assertNotEquals, + }) + } + } } diff --git a/src/hash_manager.ts b/src/hash_manager.ts index c76cb0d..c024152 100644 --- a/src/hash_manager.ts +++ b/src/hash_manager.ts @@ -155,4 +155,18 @@ export class HashManager { + return this.use().assertEquals(hashedValue, plainValue) + } + + /** + * Assert the plain value fails the hash verification + */ + async assertNotEquals(hashedValue: string, plainValue: string): Promise { + return this.use().assertNotEquals(hashedValue, plainValue) + } } diff --git a/tests/hash.spec.ts b/tests/hash.spec.ts index 8cac490..84f3afb 100644 --- a/tests/hash.spec.ts +++ b/tests/hash.spec.ts @@ -35,4 +35,22 @@ test.group('Hash', () => { const hashedValue = await hash.make('secret') assert.isFalse(hash.needsReHash(hashedValue)) }) + + test('assert hashed value against plain value', async ({ assert }) => { + const argon = new Argon({}) + const hash = new Hash(argon) + + const hashedValue = await hash.make('secret') + await assert.doesNotRejects(() => hash.assertEquals(hashedValue, 'secret')) + await assert.rejects( + () => hash.assertEquals(hashedValue, 'seret'), + 'Expected "seret" to pass hash verification' + ) + + await assert.doesNotRejects(() => hash.assertNotEquals(hashedValue, 'seret')) + await assert.rejects( + () => hash.assertNotEquals(hashedValue, 'secret'), + 'Expected "secret" to fail hash verification' + ) + }) }) diff --git a/tests/hash_manager.spec.ts b/tests/hash_manager.spec.ts index 21e11f8..36c1467 100644 --- a/tests/hash_manager.spec.ts +++ b/tests/hash_manager.spec.ts @@ -156,4 +156,26 @@ test.group('Hash manager', () => { const hashedValue = await manager.make('secret') assert.isFalse(manager.needsReHash(hashedValue)) }) + + test('assert hashed value against plain value', async ({ assert }) => { + const manager = new HashManager({ + default: 'argon', + list: { + argon: () => new Argon({}), + }, + }) + + const hashedValue = await manager.make('secret') + await assert.doesNotRejects(() => manager.assertEquals(hashedValue, 'secret')) + await assert.rejects( + () => manager.assertEquals(hashedValue, 'seret'), + 'Expected "seret" to pass hash verification' + ) + + await assert.doesNotRejects(() => manager.assertNotEquals(hashedValue, 'seret')) + await assert.rejects( + () => manager.assertNotEquals(hashedValue, 'secret'), + 'Expected "secret" to fail hash verification' + ) + }) }) From b7aab9049b6011cdd3b919442d4996488828dcbe Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 11 Mar 2023 13:30:54 +0530 Subject: [PATCH 37/74] chore: update dependencies --- package.json | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a3c22fc..d4e4cd1 100644 --- a/package.json +++ b/package.json @@ -44,17 +44,15 @@ "@japa/run-failed-tests": "^1.1.1", "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", - "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.38", + "@swc/core": "^1.3.39", "@types/bcrypt": "^5.0.0", "@types/node": "^18.15.0", - "@types/sinon": "^10.0.13", "argon2": "^0.30.3", - "bcrypt": "^5.0.1", + "bcrypt": "^5.1.0", "c8": "^7.13.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.35.0", + "eslint": "^8.36.0", "eslint-config-prettier": "^8.7.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", @@ -62,7 +60,6 @@ "husky": "^8.0.3", "np": "^7.6.3", "prettier": "^2.8.4", - "sinon": "^15.0.1", "ts-node": "^10.9.1", "typescript": "^4.9.5" }, @@ -72,7 +69,7 @@ }, "peerDependencies": { "argon2": "^0.30.3", - "bcrypt": "^5.0.1" + "bcrypt": "^5.1.0" }, "peerDependenciesMeta": { "argon2": { From 97f9da367863ba6951c9af9598e629e4d1acf633 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 11 Mar 2023 13:34:04 +0530 Subject: [PATCH 38/74] chore(release): 8.3.1-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4e4cd1..91bc303 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-0", + "version": "8.3.1-1", "description": "Multi driver hash module with support for PHC string formats", "main": "build/index.js", "type": "module", From f31e74ea18c4ddaf2f638fd657531523ac9c73ba Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 09:31:47 +0530 Subject: [PATCH 39/74] chore: update dependencies --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 91bc303..49ba4ba 100644 --- a/package.json +++ b/package.json @@ -37,35 +37,35 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@commitlint/cli": "^17.4.4", + "@commitlint/cli": "^17.5.0", "@commitlint/config-conventional": "^17.4.4", "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", "@japa/run-failed-tests": "^1.1.1", "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", - "@swc/core": "^1.3.39", + "@swc/core": "^1.3.42", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.15.0", + "@types/node": "^18.15.10", "argon2": "^0.30.3", "bcrypt": "^5.1.0", "c8": "^7.13.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.36.0", - "eslint-config-prettier": "^8.7.0", + "eslint-config-prettier": "^8.8.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^7.6.3", - "prettier": "^2.8.4", + "np": "^7.6.4", + "prettier": "^2.8.7", "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "typescript": "^5.0.2" }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.5.0-0" + "@poppinss/utils": "^6.5.0-2" }, "peerDependencies": { "argon2": "^0.30.3", From 5d64b310fb9f2d45474d70fa38fd806c5a1698a6 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 09:33:06 +0530 Subject: [PATCH 40/74] docs: update License file --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 59a3cfa..381426b 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License -Copyright (c) 2023 AdonisJS Framework +Copyright (c) 2023 Harminder Virk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From 450a631037503453479415039d7336c6316de1e8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 09:33:48 +0530 Subject: [PATCH 41/74] chore: publish source and generate delcaration map --- package.json | 4 ++++ tsconfig.json | 1 + 2 files changed, 5 insertions(+) diff --git a/package.json b/package.json index 49ba4ba..ff8abee 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,13 @@ "main": "build/index.js", "type": "module", "files": [ + "src", + "test_factories", + "index.ts", "build/src", "build/test_factories", "build/index.d.ts", + "build/index.d.ts.map", "build/index.js" ], "exports": { diff --git a/tsconfig.json b/tsconfig.json index 5c0a0dd..d6f4b0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "noImplicitAny": true, "strictBindCallApply": true, "strictFunctionTypes": true, + "declarationMap": true, "noImplicitThis": true, "skipLibCheck": true, "types": ["@types/node"] From 259af34802537cc19cf88207b7a3535a6b64ca01 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 09:35:07 +0530 Subject: [PATCH 42/74] chore: update package description --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ff8abee..a2f5897 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/hash", "version": "8.3.1-1", - "description": "Multi driver hash module with support for PHC string formats", + "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", "files": [ From a67bc7b650d3ca73ca316d0d5ec03563b6397bcc Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 09:36:43 +0530 Subject: [PATCH 43/74] docs: fix the package name in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ccfbfcf..814ebe5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# @adonisjs/events +# @adonisjs/hash
From 33dd30151da64fcd4e1794719f06b840a01af4ff Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 27 Mar 2023 09:38:58 +0530 Subject: [PATCH 44/74] chore(release): 8.3.1-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a2f5897..48981a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-1", + "version": "8.3.1-2", "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", From 232fa7dff2826df1f8a08bee3d2ece56daedbdc0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 3 Jul 2023 16:01:16 +0530 Subject: [PATCH 45/74] chore: update dependencies --- package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 48981a8..860f433 100644 --- a/package.json +++ b/package.json @@ -41,35 +41,35 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@commitlint/cli": "^17.5.0", - "@commitlint/config-conventional": "^17.4.4", + "@commitlint/cli": "^17.6.6", + "@commitlint/config-conventional": "^17.6.6", "@japa/assert": "^1.4.1", "@japa/expect-type": "^1.0.3", "@japa/run-failed-tests": "^1.1.1", "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.3.3", - "@swc/core": "^1.3.42", + "@swc/core": "^1.3.67", "@types/bcrypt": "^5.0.0", - "@types/node": "^18.15.10", + "@types/node": "^20.3.3", "argon2": "^0.30.3", "bcrypt": "^5.1.0", - "c8": "^7.13.0", + "c8": "^8.0.0", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.36.0", + "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-adonis": "^3.0.3", "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^7.6.4", - "prettier": "^2.8.7", + "np": "^8.0.4", + "prettier": "^2.8.8", "ts-node": "^10.9.1", - "typescript": "^5.0.2" + "typescript": "^5.1.6" }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.5.0-2" + "@poppinss/utils": "^6.5.0-3" }, "peerDependencies": { "argon2": "^0.30.3", From 56de0b085ff0c4b40e74177d6338223c592ce2b3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 3 Jul 2023 16:03:25 +0530 Subject: [PATCH 46/74] chore: upgrade japa to v3 --- bin/japa_types.ts | 7 ------- bin/test.ts | 15 ++++----------- package.json | 8 +++----- 3 files changed, 7 insertions(+), 23 deletions(-) delete mode 100644 bin/japa_types.ts diff --git a/bin/japa_types.ts b/bin/japa_types.ts deleted file mode 100644 index d42cac6..0000000 --- a/bin/japa_types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Assert } from '@japa/assert' - -declare module '@japa/runner' { - interface TestContext { - assert: Assert - } -} diff --git a/bin/test.ts b/bin/test.ts index ee6d2e9..c4a7b31 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,9 +1,6 @@ import { assert } from '@japa/assert' -import { pathToFileURL } from 'node:url' import { expectTypeOf } from '@japa/expect-type' -import { specReporter } from '@japa/spec-reporter' -import { runFailedTests } from '@japa/run-failed-tests' -import { processCliArgs, configure, run } from '@japa/runner' +import { processCLIArgs, configure, run } from '@japa/runner' /* |-------------------------------------------------------------------------- @@ -18,14 +15,10 @@ import { processCliArgs, configure, run } from '@japa/runner' | | Please consult japa.dev/runner-config for the config docs. */ +processCLIArgs(process.argv.slice(2)) configure({ - ...processCliArgs(process.argv.slice(2)), - ...{ - files: ['tests/**/*.spec.ts'], - plugins: [assert(), runFailedTests(), expectTypeOf()], - reporters: [specReporter()], - importer: (filePath: string) => import(pathToFileURL(filePath).href), - }, + files: ['tests/**/*.spec.ts'], + plugins: [assert(), expectTypeOf()], }) /* diff --git a/package.json b/package.json index 860f433..9e48cf7 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,9 @@ "devDependencies": { "@commitlint/cli": "^17.6.6", "@commitlint/config-conventional": "^17.6.6", - "@japa/assert": "^1.4.1", - "@japa/expect-type": "^1.0.3", - "@japa/run-failed-tests": "^1.1.1", - "@japa/runner": "^2.5.1", - "@japa/spec-reporter": "^1.3.3", + "@japa/assert": "^2.0.0-1", + "@japa/expect-type": "^2.0.0-0", + "@japa/runner": "^3.0.0-3", "@swc/core": "^1.3.67", "@types/bcrypt": "^5.0.0", "@types/node": "^20.3.3", From 3eb00d177f7dee6c5d87926fcbfae84998c5bf4e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 3 Jul 2023 16:04:43 +0530 Subject: [PATCH 47/74] chore: use @adonisjs/tooling presets for tooling config --- .github/workflows/checks.yml | 14 ++++++++++++ .github/workflows/test.yml | 7 ------ package.json | 43 ++++++++---------------------------- tsconfig.json | 31 +++----------------------- 4 files changed, 26 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/checks.yml delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..c27fb04 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,14 @@ +name: checks +on: + - push + - pull_request + +jobs: + test: + uses: adonisjs/.github/.github/workflows/test.yml@main + + lint: + uses: adonisjs/.github/.github/workflows/lint.yml@main + + typecheck: + uses: adonisjs/.github/.github/workflows/typecheck.yml@main diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 2d9bc9e..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: test -on: - - push - - pull_request -jobs: - test: - uses: adonisjs/.github/.github/workflows/test.yml@main diff --git a/package.json b/package.json index 9e48cf7..3f85dc7 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "pretest": "npm run lint", "test": "cross-env NODE_DEBUG=adonisjs:hash c8 npm run vscode:test", "clean": "del-cli build", + "typecheck": "tsc --noEmit", "compile": "npm run lint && npm run clean && tsc", "build": "npm run compile", "release": "np", @@ -41,6 +42,9 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { + "@adonisjs/eslint-config": "^1.1.7", + "@adonisjs/prettier-config": "^1.1.7", + "@adonisjs/tsconfig": "^1.1.7", "@commitlint/cli": "^17.6.6", "@commitlint/config-conventional": "^17.6.6", "@japa/assert": "^2.0.0-1", @@ -55,9 +59,6 @@ "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.44.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-adonis": "^3.0.3", - "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", @@ -89,36 +90,6 @@ "url": "https://github.com/poppinss/hash/issues" }, "homepage": "https://github.com/poppinss/hash#readme", - "eslintConfig": { - "extends": [ - "plugin:adonis/typescriptPackage", - "prettier" - ], - "plugins": [ - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ] - } - }, - "eslintIgnore": [ - "build" - ], - "prettier": { - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "useTabs": false, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 100 - }, "commitlint": { "extends": [ "@commitlint/config-conventional" @@ -143,5 +114,9 @@ "tests/**", "src/legacy/**" ] - } + }, + "eslintConfig": { + "extends": "@adonisjs/eslint-config/package" + }, + "prettier": "@adonisjs/prettier-config" } diff --git a/tsconfig.json b/tsconfig.json index d6f4b0f..2039043 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,32 +1,7 @@ { + "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { - "target": "ESNext", - "module": "NodeNext", - "lib": ["ESNext"], - "noUnusedLocals": true, - "noUnusedParameters": true, - "isolatedModules": true, - "removeComments": true, - "allowJs": true, - "declaration": true, "rootDir": "./", - "outDir": "./build", - "esModuleInterop": true, - "strictNullChecks": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "strictPropertyInitialization": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, - "declarationMap": true, - "noImplicitThis": true, - "skipLibCheck": true, - "types": ["@types/node"] - }, - "include": ["./**/*"], - "exclude": ["./node_modules", "./build"], - "ts-node": { - "swc": true + "outDir": "./build" } -} +} \ No newline at end of file From c4f4204b99bd8cfd34f35d916cdd3d90dce0e745 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 3 Jul 2023 16:05:14 +0530 Subject: [PATCH 48/74] chore: do not publish source files --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index 3f85dc7..4e77a33 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,9 @@ "main": "build/index.js", "type": "module", "files": [ - "src", - "test_factories", - "index.ts", "build/src", "build/test_factories", "build/index.d.ts", - "build/index.d.ts.map", "build/index.js" ], "exports": { From 36ac3e77fa762cc1ab59b4f303ad0eafdde1f76b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 3 Jul 2023 16:05:37 +0530 Subject: [PATCH 49/74] chore: add engines to package.json file --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 4e77a33..ebb358e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ "./types": "./build/src/types.js", "./factories": "./build/test_factories/main.js" }, + "engines": { + "node": ">=18.16.0" + }, "scripts": { "pretest": "npm run lint", "test": "cross-env NODE_DEBUG=adonisjs:hash c8 npm run vscode:test", From f1bafa56a6cef49b137223cc64930c8c6400cb12 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 3 Jul 2023 16:06:38 +0530 Subject: [PATCH 50/74] docs(README): remove snyk badge and update tests badge URL --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 814ebe5..f33cc42 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url] +[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] ## Introduction AdonisJS hash is a driver based hashing library. We ship with `argon2`, `bcrypt` and `scrypt` drivers. The generated hash from all the drivers are formatted using the [PHC string format](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md) @@ -21,8 +21,8 @@ In order to ensure that the AdonisJS community is welcoming to all, please revie ## License AdonisJS hash is open-sourced software licensed under the [MIT license](LICENSE.md). -[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/hash/test.yml?style=for-the-badge -[gh-workflow-url]: https://github.com/adonisjs/hash/actions/workflows/test.yml "Github action" +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/hash/checks.yml?style=for-the-badge +[gh-workflow-url]: https://github.com/adonisjs/hash/actions/workflows/checks.yml "Github action" [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript [typescript-url]: "typescript" @@ -32,6 +32,3 @@ AdonisJS hash is open-sourced software licensed under the [MIT license](LICENSE. [license-image]: https://img.shields.io/npm/l/@adonisjs/hash?color=blueviolet&style=for-the-badge [license-url]: LICENSE.md "license" - -[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/hash?label=Synk%20Vulnerabilities&style=for-the-badge -[synk-url]: https://snyk.io/test/github/adonisjs/hash?targetFile=package.json "synk" From ab7c0cee804aae839545210f90fc2aaf62efd767 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 3 Jul 2023 16:11:05 +0530 Subject: [PATCH 51/74] chore: allow js files in tsconfig file --- tsconfig.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 2039043..e6fe66f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { "rootDir": "./", - "outDir": "./build" + "outDir": "./build", + "allowJs": true } -} \ No newline at end of file +} From cc16ea9b553101aacfcbf94927ea68c71caa122c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 3 Jul 2023 16:11:56 +0530 Subject: [PATCH 52/74] chore(release): 8.3.1-3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ebb358e..8fed6dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-2", + "version": "8.3.1-3", "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", From ce193fce1bc0a0ef5faadec3a3d4748c00b6832b Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Wed, 5 Jul 2023 21:34:52 +0200 Subject: [PATCH 53/74] fix(factories): correct naming of the class --- {test_factories => factories}/hash_manager.ts | 8 ++++---- {test_factories => factories}/main.ts | 2 +- tests/hash_manager_factory.spec.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename {test_factories => factories}/hash_manager.ts (88%) rename {test_factories => factories}/main.ts (76%) diff --git a/test_factories/hash_manager.ts b/factories/hash_manager.ts similarity index 88% rename from test_factories/hash_manager.ts rename to factories/hash_manager.ts index c324eee..21409ed 100644 --- a/test_factories/hash_manager.ts +++ b/factories/hash_manager.ts @@ -19,7 +19,7 @@ type Config> = { * Hash manager factory is used to create an instance of hash manager * for testing */ -export class HashMangerFactory< +export class HashManagerFactory< KnownHashers extends Record = { scrypt: () => Scrypt } @@ -27,7 +27,7 @@ export class HashMangerFactory< /** * Config accepted by hash manager */ - #config: Config + readonly #config: Config constructor(config?: { default?: keyof KnownHashers; list: KnownHashers }) { this.#config = @@ -45,8 +45,8 @@ export class HashMangerFactory< */ merge>( config: Config - ): HashMangerFactory { - return new HashMangerFactory(config) + ): HashManagerFactory { + return new HashManagerFactory(config) } /** diff --git a/test_factories/main.ts b/factories/main.ts similarity index 76% rename from test_factories/main.ts rename to factories/main.ts index 729a7ca..498f6be 100644 --- a/test_factories/main.ts +++ b/factories/main.ts @@ -7,4 +7,4 @@ * file that was distributed with this source code. */ -export { HashMangerFactory } from './hash_manager.js' +export { HashManagerFactory } from './hash_manager.js' diff --git a/tests/hash_manager_factory.spec.ts b/tests/hash_manager_factory.spec.ts index ca3a56f..dbe4259 100644 --- a/tests/hash_manager_factory.spec.ts +++ b/tests/hash_manager_factory.spec.ts @@ -9,11 +9,11 @@ import { test } from '@japa/runner' import { Bcrypt, Hash, Scrypt } from '../index.js' -import { HashMangerFactory } from '../test_factories/hash_manager.js' +import { HashManagerFactory } from '../factories/hash_manager.js' test.group('Hash manager factory', () => { test('create instance of hash manager using factory', async ({ assert, expectTypeOf }) => { - const hash = new HashMangerFactory().create() + const hash = new HashManagerFactory().create() assert.instanceOf(hash.use(), Hash) expectTypeOf(hash.use()).toMatchTypeOf() @@ -23,7 +23,7 @@ test.group('Hash manager factory', () => { }) test('create instance of hash manager with custom config', async ({ assert }) => { - const hash = new HashMangerFactory() + const hash = new HashManagerFactory() .merge({ default: 'bcrypt', list: { From 17e9d2cea2dd847e167c1f798e30cad04ebd2a10 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Wed, 5 Jul 2023 21:35:22 +0200 Subject: [PATCH 54/74] fix(factories): change path --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8fed6dd..001df09 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,14 @@ "type": "module", "files": [ "build/src", - "build/test_factories", + "build/factories", "build/index.d.ts", "build/index.js" ], "exports": { ".": "./build/index.js", "./types": "./build/src/types.js", - "./factories": "./build/test_factories/main.js" + "./factories": "./build/factories/main.js" }, "engines": { "node": ">=18.16.0" From 94468218b6c17d4fd77997760c30cd7a5b0eb60a Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Wed, 5 Jul 2023 21:39:13 +0200 Subject: [PATCH 55/74] chore(release): 8.3.1-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 001df09..0523bb6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-3", + "version": "8.3.1-4", "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", From 4ec5779d61810f74673a98a782db5819798d9613 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Jul 2023 12:03:15 +0530 Subject: [PATCH 56/74] chore: update dependencies --- .github/stale.yml | 4 ++-- factories/hash_manager.ts | 2 +- package.json | 20 ++++++++++---------- src/phc_formatter.ts | 2 +- src/types.ts | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 7a6a571..f767674 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,10 +6,10 @@ daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - - "Type: Security" + - 'Type: Security' # Label to use when marking an issue as stale -staleLabel: "Status: Abandoned" +staleLabel: 'Status: Abandoned' # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > diff --git a/factories/hash_manager.ts b/factories/hash_manager.ts index 21409ed..f731d4f 100644 --- a/factories/hash_manager.ts +++ b/factories/hash_manager.ts @@ -22,7 +22,7 @@ type Config> = { export class HashManagerFactory< KnownHashers extends Record = { scrypt: () => Scrypt - } + }, > { /** * Config accepted by hash manager diff --git a/package.json b/package.json index 0523bb6..4e97f90 100644 --- a/package.json +++ b/package.json @@ -41,27 +41,27 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@adonisjs/eslint-config": "^1.1.7", - "@adonisjs/prettier-config": "^1.1.7", - "@adonisjs/tsconfig": "^1.1.7", - "@commitlint/cli": "^17.6.6", - "@commitlint/config-conventional": "^17.6.6", + "@adonisjs/eslint-config": "^1.1.8", + "@adonisjs/prettier-config": "^1.1.8", + "@adonisjs/tsconfig": "^1.1.8", + "@commitlint/cli": "^17.6.7", + "@commitlint/config-conventional": "^17.6.7", "@japa/assert": "^2.0.0-1", "@japa/expect-type": "^2.0.0-0", "@japa/runner": "^3.0.0-3", - "@swc/core": "^1.3.67", + "@swc/core": "^1.3.71", "@types/bcrypt": "^5.0.0", - "@types/node": "^20.3.3", + "@types/node": "^20.4.5", "argon2": "^0.30.3", "bcrypt": "^5.1.0", - "c8": "^8.0.0", + "c8": "^8.0.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.44.0", + "eslint": "^8.45.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "ts-node": "^10.9.1", "typescript": "^5.1.6" }, diff --git a/src/phc_formatter.ts b/src/phc_formatter.ts index 36f0222..0d913cc 100644 --- a/src/phc_formatter.ts +++ b/src/phc_formatter.ts @@ -16,7 +16,7 @@ import { PhcNode } from './types.js' * deserialize it back to a PHC object. */ export class PhcFormatter< - Params extends Record = Record + Params extends Record = Record, > { /** * Serialize salt and hash with predefined options. diff --git a/src/types.ts b/src/types.ts index f488119..2a7d5d6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,7 +37,7 @@ export interface HashDriverContract { * Shape of deserialized phc node */ export type PhcNode< - Params extends Record = Record + Params extends Record = Record, > = { id: string salt: Buffer From 52a5736a3d24e4002c998448716d86acc3b69dbc Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Jul 2023 12:06:28 +0530 Subject: [PATCH 57/74] refactor: export drivers as submodules --- index.ts | 3 --- package.json | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index e445a6e..4351f11 100644 --- a/index.ts +++ b/index.ts @@ -8,8 +8,5 @@ */ export { Hash } from './src/hash.js' -export { Argon } from './src/drivers/argon.js' -export { Bcrypt } from './src/drivers/bcrypt.js' -export { Scrypt } from './src/drivers/scrypt.js' export { HashManager } from './src/hash_manager.js' export { PhcFormatter } from './src/phc_formatter.js' diff --git a/package.json b/package.json index 4e97f90..8378157 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "exports": { ".": "./build/index.js", "./types": "./build/src/types.js", + "./drivers/argon": "./build/src/drivers/argon.js", + "./drivers/bcrypt": "./build/src/drivers/bcrypt.js", + "./drivers/scrypt": "./build/src/drivers/scrypt.js", "./factories": "./build/factories/main.js" }, "engines": { From b5f229f3a77a137ee9134a17273fb0d27da2f78f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Jul 2023 12:07:01 +0530 Subject: [PATCH 58/74] refactor: export phc_formatter from submodule --- index.ts | 1 - package.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/index.ts b/index.ts index 4351f11..1c70083 100644 --- a/index.ts +++ b/index.ts @@ -9,4 +9,3 @@ export { Hash } from './src/hash.js' export { HashManager } from './src/hash_manager.js' -export { PhcFormatter } from './src/phc_formatter.js' diff --git a/package.json b/package.json index 8378157..eeea820 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "./drivers/argon": "./build/src/drivers/argon.js", "./drivers/bcrypt": "./build/src/drivers/bcrypt.js", "./drivers/scrypt": "./build/src/drivers/scrypt.js", + "./phc_formatter": "./build/src/phc_formatter.js", "./factories": "./build/factories/main.js" }, "engines": { From 9a24973dcf1adb1c66175748c7d74df6d85ad616 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Jul 2023 12:11:16 +0530 Subject: [PATCH 59/74] test: fix failing tests --- factories/hash_manager.ts | 3 ++- tests/hash_manager.spec.ts | 4 +++- tests/hash_manager_factory.spec.ts | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/factories/hash_manager.ts b/factories/hash_manager.ts index f731d4f..fa0a3b8 100644 --- a/factories/hash_manager.ts +++ b/factories/hash_manager.ts @@ -7,7 +7,8 @@ * file that was distributed with this source code. */ -import { HashManager, Scrypt } from '../index.js' +import { HashManager } from '../index.js' +import { Scrypt } from '../src/drivers/scrypt.js' import { ManagerDriverFactory } from '../src/types.js' type Config> = { diff --git a/tests/hash_manager.spec.ts b/tests/hash_manager.spec.ts index 36c1467..7c745c0 100644 --- a/tests/hash_manager.spec.ts +++ b/tests/hash_manager.spec.ts @@ -9,8 +9,10 @@ import { test } from '@japa/runner' import { Hash } from '../src/hash.js' +import { Argon } from '../src/drivers/argon.js' +import { Bcrypt } from '../src/drivers/bcrypt.js' +import { Scrypt } from '../src/drivers/scrypt.js' import { HashManager } from '../src/hash_manager.js' -import { Argon, Bcrypt, Scrypt } from '../index.js' test.group('Hash manager', () => { test('create hash instance from the manager', ({ assert, expectTypeOf }) => { diff --git a/tests/hash_manager_factory.spec.ts b/tests/hash_manager_factory.spec.ts index dbe4259..aa6191d 100644 --- a/tests/hash_manager_factory.spec.ts +++ b/tests/hash_manager_factory.spec.ts @@ -8,7 +8,9 @@ */ import { test } from '@japa/runner' -import { Bcrypt, Hash, Scrypt } from '../index.js' +import { Hash } from '../index.js' +import { Bcrypt } from '../src/drivers/bcrypt.js' +import { Scrypt } from '../src/drivers/scrypt.js' import { HashManagerFactory } from '../factories/hash_manager.js' test.group('Hash manager factory', () => { From 88bf27cf13f39d02d3360a698ba2a89879794134 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 26 Jul 2023 12:16:23 +0530 Subject: [PATCH 60/74] chore(release): 8.3.1-5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eeea820..0a517e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-4", + "version": "8.3.1-5", "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", From 344d0c8a13d0de22fa4e6f301f496001f4d4c6f0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 12:10:28 +0530 Subject: [PATCH 61/74] chore: update dependencies --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 0a517e9..459dc44 100644 --- a/package.json +++ b/package.json @@ -48,34 +48,34 @@ "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.6.7", - "@commitlint/config-conventional": "^17.6.7", + "@commitlint/cli": "^17.7.1", + "@commitlint/config-conventional": "^17.7.0", "@japa/assert": "^2.0.0-1", "@japa/expect-type": "^2.0.0-0", "@japa/runner": "^3.0.0-3", - "@swc/core": "^1.3.71", + "@swc/core": "^1.3.78", "@types/bcrypt": "^5.0.0", - "@types/node": "^20.4.5", - "argon2": "^0.30.3", - "bcrypt": "^5.1.0", + "@types/node": "^20.5.3", + "argon2": "^0.31.0", + "bcrypt": "^5.1.1", "c8": "^8.0.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.45.0", + "eslint": "^8.47.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.0", + "prettier": "^3.0.2", "ts-node": "^10.9.1", "typescript": "^5.1.6" }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.5.0-3" + "@poppinss/utils": "^6.5.0-5" }, "peerDependencies": { - "argon2": "^0.30.3", - "bcrypt": "^5.1.0" + "argon2": "^0.31.0", + "bcrypt": "^5.1.1" }, "peerDependenciesMeta": { "argon2": { From 72ed4f26b730c9d83f9d89a274dd05f75cc687be Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 12:27:34 +0530 Subject: [PATCH 62/74] chore(release): 8.3.1-6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 459dc44..b6d86ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-5", + "version": "8.3.1-6", "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", From 084cab3adfc56900837d06747572b0b98ba2d3b2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 16 Oct 2023 16:04:51 +0530 Subject: [PATCH 63/74] chore: update dependencies --- package.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index b6d86ed..de623a9 100644 --- a/package.json +++ b/package.json @@ -48,30 +48,30 @@ "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.7.1", - "@commitlint/config-conventional": "^17.7.0", - "@japa/assert": "^2.0.0-1", - "@japa/expect-type": "^2.0.0-0", - "@japa/runner": "^3.0.0-3", - "@swc/core": "^1.3.78", + "@commitlint/cli": "^17.8.0", + "@commitlint/config-conventional": "^17.8.0", + "@japa/assert": "^2.0.0", + "@japa/expect-type": "^2.0.0", + "@japa/runner": "^3.0.2", + "@swc/core": "1.3.82", "@types/bcrypt": "^5.0.0", - "@types/node": "^20.5.3", - "argon2": "^0.31.0", + "@types/node": "^20.8.6", + "argon2": "^0.31.1", "bcrypt": "^5.1.1", "c8": "^8.0.1", "cross-env": "^7.0.3", - "del-cli": "^5.0.0", - "eslint": "^8.47.0", + "del-cli": "^5.1.0", + "eslint": "^8.51.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.2", + "prettier": "^3.0.3", "ts-node": "^10.9.1", - "typescript": "^5.1.6" + "typescript": "^5.2.2" }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.5.0-5" + "@poppinss/utils": "^6.5.0" }, "peerDependencies": { "argon2": "^0.31.0", From 8c53e45e375304ab1e0df2dc88f45e21945ef4d3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 16 Oct 2023 16:09:02 +0530 Subject: [PATCH 64/74] chore: use tsup for bundling --- package.json | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index de623a9..b4e6ae1 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,7 @@ "main": "build/index.js", "type": "module", "files": [ - "build/src", - "build/factories", - "build/index.d.ts", - "build/index.js" + "build" ], "exports": { ".": "./build/index.js", @@ -27,7 +24,7 @@ "test": "cross-env NODE_DEBUG=adonisjs:hash c8 npm run vscode:test", "clean": "del-cli build", "typecheck": "tsc --noEmit", - "compile": "npm run lint && npm run clean && tsc", + "compile": "npm run lint && npm run clean && tsup-node", "build": "npm run compile", "release": "np", "version": "npm run build", @@ -67,6 +64,7 @@ "np": "^8.0.4", "prettier": "^3.0.3", "ts-node": "^10.9.1", + "tsup": "^7.2.0", "typescript": "^5.2.2" }, "dependencies": { @@ -121,5 +119,21 @@ "eslintConfig": { "extends": "@adonisjs/eslint-config/package" }, - "prettier": "@adonisjs/prettier-config" + "prettier": "@adonisjs/prettier-config", + "tsup": { + "entry": [ + "./index.ts", + "./src/drivers/argon.ts", + "./src/drivers/bcrypt.ts", + "./src/drivers/scrypt.ts", + "./src/phc_formatter.ts", + "./src/types.ts", + "./factories/main.ts" + ], + "outDir": "./build", + "clean": true, + "format": "esm", + "dts": true, + "target": "esnext" + } } From 7bb22e78887c2504e906198fc91d443b2ac91ca3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 17 Oct 2023 08:53:25 +0530 Subject: [PATCH 65/74] chore(release): 8.3.1-7 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b4e6ae1..618374c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-6", + "version": "8.3.1-7", "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", @@ -120,7 +120,7 @@ "extends": "@adonisjs/eslint-config/package" }, "prettier": "@adonisjs/prettier-config", - "tsup": { + "tsup": { "entry": [ "./index.ts", "./src/drivers/argon.ts", From 524c291ccfaf206f71ea3c5f8ff52997d2ea956b Mon Sep 17 00:00:00 2001 From: Romain Lanz <2793951+RomainLanz@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:35:32 +0200 Subject: [PATCH 66/74] refactor(hash): needsRehash should return true when invalid identifier (#10) --- src/drivers/argon.ts | 3 ++- src/drivers/bcrypt.ts | 3 ++- src/drivers/scrypt.ts | 3 ++- tests/drivers/argon2.spec.ts | 4 ++-- tests/drivers/bcrypt.spec.ts | 4 ++-- tests/drivers/scrypt.spec.ts | 8 +++----- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/drivers/argon.ts b/src/drivers/argon.ts index 4f68774..4c40d58 100644 --- a/src/drivers/argon.ts +++ b/src/drivers/argon.ts @@ -281,6 +281,7 @@ export class Argon implements HashDriverContract { * 3. The memory value is changed * 4. The parellelism value is changed * 5. The argon variant is changed + * 6. The provided hash has not been hashed with argon * * ```ts * const isValid = await argon.verify(hash, plainText) @@ -294,7 +295,7 @@ export class Argon implements HashDriverContract { needsReHash(value: string): boolean { const phcNode = this.#phcFormatter.deserialize(value) if (!this.#ids.includes(phcNode.id)) { - throw new TypeError('Value is not a valid argon hash') + return true } /** diff --git a/src/drivers/bcrypt.ts b/src/drivers/bcrypt.ts index 92ed9c1..6f15711 100644 --- a/src/drivers/bcrypt.ts +++ b/src/drivers/bcrypt.ts @@ -227,6 +227,7 @@ export class Bcrypt implements HashDriverContract { * 1. The bcrypt version is changed * 2. Number of rounds are changed * 3. Bcrypt hash is not using MCF hash format + * 4. The provided hash has not been hashed with bcrypt * * ```ts * const isValid = await bcrypt.verify(hash, plainText) @@ -244,7 +245,7 @@ export class Bcrypt implements HashDriverContract { const phcNode = this.#phcFormatter.deserialize(value) if (phcNode.id !== 'bcrypt') { - throw new TypeError('Value is not a valid bcrypt hash') + return true } /** diff --git a/src/drivers/scrypt.ts b/src/drivers/scrypt.ts index 097755a..3a556dc 100644 --- a/src/drivers/scrypt.ts +++ b/src/drivers/scrypt.ts @@ -226,6 +226,7 @@ export class Scrypt implements HashDriverContract { * 1. The cost value is changed * 2. The blockSize value is changed * 3. The parallelization value is changed + * 4. The provided hash has not been hashed with scrypt * * ```ts * const isValid = await scrypt.verify(hash, plainText) @@ -239,7 +240,7 @@ export class Scrypt implements HashDriverContract { needsReHash(value: string): boolean { const phcNode = this.#phcFormatter.deserialize(value) if (phcNode.id !== 'scrypt') { - throw new TypeError('Value is not a valid scrypt hash') + return true } /** diff --git a/tests/drivers/argon2.spec.ts b/tests/drivers/argon2.spec.ts index a791a47..056937b 100644 --- a/tests/drivers/argon2.spec.ts +++ b/tests/drivers/argon2.spec.ts @@ -414,7 +414,7 @@ test.group('argon | needsRehash', () => { ) }) - test('throw error when not a valid argon identifier', async ({ assert }) => { + test('return true when not a valid argon identifier', async ({ assert }) => { const hash = '$bcrypt$v=19$m=4096,t=3,p=1$PcEZHj1maR/+ZQynyJHWZg$2jEN4xcww7CYp1jakZB1rxbYsZ55XH2HgjYRtdZtubI' @@ -426,7 +426,7 @@ test.group('argon | needsRehash', () => { saltSize: 16, }) - await assert.rejects(() => argon.needsReHash(hash), 'Value is not a valid argon hash') + assert.isTrue(argon.needsReHash(hash)) }) test('return true when using argon2 directly', async ({ assert }) => { diff --git a/tests/drivers/bcrypt.spec.ts b/tests/drivers/bcrypt.spec.ts index 6e5dca8..5f1f2b1 100644 --- a/tests/drivers/bcrypt.spec.ts +++ b/tests/drivers/bcrypt.spec.ts @@ -236,13 +236,13 @@ test.group('bcrypt | needsRehash', () => { ) }) - test('throw error when identifier is invalid', async ({ assert }) => { + test('return true when not a valid bcrypt identifier', async ({ assert }) => { const bcrypt = new Bcrypt({ rounds: 10, }) const hash = '$argon2id$v=98$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8' - await assert.rejects(() => bcrypt.needsReHash(hash), 'Value is not a valid bcrypt hash') + assert.isTrue(bcrypt.needsReHash(hash)) }) }) diff --git a/tests/drivers/scrypt.spec.ts b/tests/drivers/scrypt.spec.ts index 1b66eaf..22d5ef5 100644 --- a/tests/drivers/scrypt.spec.ts +++ b/tests/drivers/scrypt.spec.ts @@ -306,15 +306,13 @@ test.group('scrypt | needsRehash', () => { await assert.rejects(() => scrypt.needsReHash('foo'), 'pchstr must contain a $ as first char') }) - test('throw error when identifier is invalid', async ({ assert }) => { + test('return true when not a valid scrypt identifier', async ({ assert }) => { const scrypt = new Scrypt({ cost: 16384, }) - const hash = - '$script$n=16384,r=8,p=1$YhdCGu1G+vTC6F9oJZ16lg$IDWTbizFCq5n9YvPiy3YTPdUD12Nf1Iit8aQeGyWZdA9k9L8rKk9Ii5jQxSkV0MJyxr3/nzOHh+VTht0KFxiBA' - - await assert.rejects(() => scrypt.needsReHash(hash), 'Value is not a valid scrypt hash') + const hash = '$argon2id$v=98$r=10$Jtxi46WJ26OQ0khsYLLlnw$knXGfuRFsSjXdj88JydPOnUIglvm1S8' + assert.isTrue(scrypt.needsReHash(hash)) }) }) From b28a1232909f599a97ca9850ede046b8df75c2a2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 22 Nov 2023 14:36:00 +0530 Subject: [PATCH 67/74] chore: update dependencies --- package.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 618374c..6494dc6 100644 --- a/package.json +++ b/package.json @@ -42,37 +42,37 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@adonisjs/eslint-config": "^1.1.8", - "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/tsconfig": "^1.1.8", - "@commitlint/cli": "^17.8.0", - "@commitlint/config-conventional": "^17.8.0", - "@japa/assert": "^2.0.0", + "@adonisjs/eslint-config": "^1.1.9", + "@adonisjs/prettier-config": "^1.1.9", + "@adonisjs/tsconfig": "^1.1.9", + "@commitlint/cli": "^18.4.3", + "@commitlint/config-conventional": "^18.4.3", + "@japa/assert": "^2.0.1", "@japa/expect-type": "^2.0.0", - "@japa/runner": "^3.0.2", - "@swc/core": "1.3.82", - "@types/bcrypt": "^5.0.0", - "@types/node": "^20.8.6", - "argon2": "^0.31.1", + "@japa/runner": "^3.1.0", + "@swc/core": "^1.3.99", + "@types/bcrypt": "^5.0.2", + "@types/node": "^20.9.4", + "argon2": "^0.31.2", "bcrypt": "^5.1.1", "c8": "^8.0.1", "cross-env": "^7.0.3", "del-cli": "^5.1.0", - "eslint": "^8.51.0", + "eslint": "^8.54.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.3", + "prettier": "^3.1.0", "ts-node": "^10.9.1", - "tsup": "^7.2.0", - "typescript": "^5.2.2" + "tsup": "^8.0.1", + "typescript": "5.2.2" }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.5.0" + "@poppinss/utils": "^6.5.1" }, "peerDependencies": { - "argon2": "^0.31.0", + "argon2": "^0.31.2", "bcrypt": "^5.1.1" }, "peerDependenciesMeta": { From b0107e80042881b6779d539cda3bfa6d0258ea5f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 22 Nov 2023 14:41:49 +0530 Subject: [PATCH 68/74] chore: publish source maps and use tsc for generating types --- package.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6494dc6..4b6d3c3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,10 @@ "main": "build/index.js", "type": "module", "files": [ - "build" + "build", + "!build/bin", + "!build/coverage", + "!build/tests" ], "exports": { ".": "./build/index.js", @@ -24,7 +27,8 @@ "test": "cross-env NODE_DEBUG=adonisjs:hash c8 npm run vscode:test", "clean": "del-cli build", "typecheck": "tsc --noEmit", - "compile": "npm run lint && npm run clean && tsup-node", + "precompile": "npm run lint && npm run clean", + "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", "build": "npm run compile", "release": "np", "version": "npm run build", @@ -133,7 +137,8 @@ "outDir": "./build", "clean": true, "format": "esm", - "dts": true, + "dts": false, + "sourcemap": true, "target": "esnext" } } From 690991fe05570c2d703542cfaa5b0a56f656e7cd Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 22 Nov 2023 14:44:11 +0530 Subject: [PATCH 69/74] refactor: internals --- src/hash_manager.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/hash_manager.ts b/src/hash_manager.ts index c024152..6567405 100644 --- a/src/hash_manager.ts +++ b/src/hash_manager.ts @@ -55,15 +55,6 @@ export class HashManager( - factory: DriverFactory - ): ReturnType { - return factory() as ReturnType - } - /** * Use one of the registered hashers to hash values. * @@ -103,7 +94,7 @@ export class HashManager Date: Wed, 22 Nov 2023 14:50:42 +0530 Subject: [PATCH 70/74] chore(release): 8.3.1-8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b6d3c3..513fc82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-7", + "version": "8.3.1-8", "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", From 33a1ce8ea1d04b98333a937d90cc1cdb82b23116 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Dec 2023 09:16:48 +0530 Subject: [PATCH 71/74] chore: update dependencies --- package.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 513fc82..fea3eef 100644 --- a/package.json +++ b/package.json @@ -46,34 +46,34 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@adonisjs/eslint-config": "^1.1.9", - "@adonisjs/prettier-config": "^1.1.9", - "@adonisjs/tsconfig": "^1.1.9", + "@adonisjs/eslint-config": "^1.2.0", + "@adonisjs/prettier-config": "^1.2.0", + "@adonisjs/tsconfig": "^1.2.0", "@commitlint/cli": "^18.4.3", "@commitlint/config-conventional": "^18.4.3", - "@japa/assert": "^2.0.1", - "@japa/expect-type": "^2.0.0", - "@japa/runner": "^3.1.0", - "@swc/core": "^1.3.99", + "@japa/assert": "^2.1.0", + "@japa/expect-type": "^2.0.1", + "@japa/runner": "^3.1.1", + "@swc/core": "^1.3.101", "@types/bcrypt": "^5.0.2", - "@types/node": "^20.9.4", + "@types/node": "^20.10.5", "argon2": "^0.31.2", "bcrypt": "^5.1.1", "c8": "^8.0.1", "cross-env": "^7.0.3", "del-cli": "^5.1.0", - "eslint": "^8.54.0", + "eslint": "^8.56.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^8.0.4", - "prettier": "^3.1.0", - "ts-node": "^10.9.1", + "np": "^9.2.0", + "prettier": "^3.1.1", + "ts-node": "^10.9.2", "tsup": "^8.0.1", - "typescript": "5.2.2" + "typescript": "^5.3.3" }, "dependencies": { "@phc/format": "^1.0.0", - "@poppinss/utils": "^6.5.1" + "@poppinss/utils": "^6.7.0" }, "peerDependencies": { "argon2": "^0.31.2", From b9050cfc8690956af90409c69d508147c7b25eb9 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Dec 2023 09:21:21 +0530 Subject: [PATCH 72/74] chore(release): 8.3.1-9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fea3eef..7c39ff6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adonisjs/hash", - "version": "8.3.1-8", + "version": "8.3.1-9", "description": "Framework agnostic Password hashing package with support for PHC string format", "main": "build/index.js", "type": "module", From b8d42838aeade9bc506753203babe406cbb75ce4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 6 Jan 2024 21:04:58 +0530 Subject: [PATCH 73/74] chore: update dependencies --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 7c39ff6..7779736 100644 --- a/package.json +++ b/package.json @@ -46,20 +46,20 @@ "author": "adonisjs,virk", "license": "MIT", "devDependencies": { - "@adonisjs/eslint-config": "^1.2.0", - "@adonisjs/prettier-config": "^1.2.0", - "@adonisjs/tsconfig": "^1.2.0", - "@commitlint/cli": "^18.4.3", - "@commitlint/config-conventional": "^18.4.3", + "@adonisjs/eslint-config": "^1.2.1", + "@adonisjs/prettier-config": "^1.2.1", + "@adonisjs/tsconfig": "^1.2.1", + "@commitlint/cli": "^18.4.4", + "@commitlint/config-conventional": "^18.4.4", "@japa/assert": "^2.1.0", "@japa/expect-type": "^2.0.1", "@japa/runner": "^3.1.1", - "@swc/core": "^1.3.101", + "@swc/core": "^1.3.102", "@types/bcrypt": "^5.0.2", - "@types/node": "^20.10.5", + "@types/node": "^20.10.6", "argon2": "^0.31.2", "bcrypt": "^5.1.1", - "c8": "^8.0.1", + "c8": "^9.0.0", "cross-env": "^7.0.3", "del-cli": "^5.1.0", "eslint": "^8.56.0", From 030cd519f6b5cb50c713ef83e2d868d95e431c35 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 6 Jan 2024 21:10:03 +0530 Subject: [PATCH 74/74] chore: update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7779736..edb18e3 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "scripts": { "pretest": "npm run lint", - "test": "cross-env NODE_DEBUG=adonisjs:hash c8 npm run vscode:test", + "test": "cross-env NODE_DEBUG=adonisjs:hash c8 npm run quick:test", "clean": "del-cli build", "typecheck": "tsc --noEmit", "precompile": "npm run lint && npm run clean", @@ -36,7 +36,7 @@ "prepublishOnly": "npm run build", "lint": "eslint . --ext=.ts", "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/hash", - "vscode:test": "node --loader=ts-node/esm bin/test.ts" + "quick:test": "node --loader=ts-node/esm bin/test.ts" }, "keywords": [ "hash",