diff --git a/.changeset/spicy-frogs-press.md b/.changeset/spicy-frogs-press.md new file mode 100644 index 000000000..544e749ac --- /dev/null +++ b/.changeset/spicy-frogs-press.md @@ -0,0 +1,32 @@ +--- +'verdaccio-htpasswd': major +--- + +feat: allow other password hashing algorithms (#1917) + +**breaking change** + +The current implementation of the `htpasswd` module supports multiple hash formats on verify, but only `crypt` on sign in. +`crypt` is an insecure old format, so to improve the security of the new `verdaccio` release we introduce the support of multiple hash algorithms on sign in step. + +### New hashing algorithms + +The new possible hash algorithms to use are `bcrypt`, `md5`, `sha1`. `bcrypt` is chosen as a default, because of its customizable complexity and overall reliability. You can read more about them [here](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html). + +Two new properties are added to `auth` section in the configuration file: + +- `algorithm` to choose the way you want to hash passwords. +- `rounds` is used to determine `bcrypt` complexity. So one can improve security according to increasing computational power. + +Example of the new `auth` config file section: + +```yaml +auth: +htpasswd: + file: ./htpasswd + max_users: 1000 + # Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt". + algorithm: bcrypt + # Rounds number for "bcrypt", will be ignored for other algorithms. + rounds: 10 +``` diff --git a/packages/core/htpasswd/README.md b/packages/core/htpasswd/README.md index bd9d95593..7caff08f4 100644 --- a/packages/core/htpasswd/README.md +++ b/packages/core/htpasswd/README.md @@ -27,6 +27,10 @@ As simple as running: # Maximum amount of users allowed to register, defaults to "+infinity". # You can set this to -1 to disable registration. #max_users: 1000 + # Hash algorithm, possible options are: "bcrypt", "md5", "sha1", "crypt". + #algorithm: bcrypt + # Rounds number for "bcrypt", will be ignored for other algorithms. + #rounds: 10 ## Logging In diff --git a/packages/core/htpasswd/package.json b/packages/core/htpasswd/package.json index 52d392803..a53b06c1e 100644 --- a/packages/core/htpasswd/package.json +++ b/packages/core/htpasswd/package.json @@ -37,7 +37,8 @@ }, "devDependencies": { "@types/bcryptjs": "^2.4.2", - "@verdaccio/types": "workspace:10.0.0-alpha.3" + "@verdaccio/types": "workspace:10.0.0-alpha.3", + "mockdate": "^3.0.2" }, "scripts": { "clean": "rimraf ./build", diff --git a/packages/core/htpasswd/src/crypt3.ts b/packages/core/htpasswd/src/crypt3.ts index 9e34f98c1..41fcdcf04 100644 --- a/packages/core/htpasswd/src/crypt3.ts +++ b/packages/core/htpasswd/src/crypt3.ts @@ -10,28 +10,37 @@ import crypto from 'crypto'; import crypt from 'unix-crypt-td-js'; +export enum EncryptionMethod { + md5 = 'md5', + sha1 = 'sha1', + crypt = 'crypt', + blowfish = 'blowfish', + sha256 = 'sha256', + sha512 = 'sha512', +} + /** * Create salt - * @param {string} type The type of salt: md5, blowfish (only some linux + * @param {EncryptionMethod} type The type of salt: md5, blowfish (only some linux * distros), sha256 or sha512. Default is sha512. * @returns {string} Generated salt string */ -export function createSalt(type = 'crypt'): string { +export function createSalt(type: EncryptionMethod = EncryptionMethod.crypt): string { switch (type) { - case 'crypt': + case EncryptionMethod.crypt: // Legacy crypt salt with no prefix (only the first 2 bytes will be used). return crypto.randomBytes(2).toString('base64'); - case 'md5': + case EncryptionMethod.md5: return '$1$' + crypto.randomBytes(10).toString('base64'); - case 'blowfish': + case EncryptionMethod.blowfish: return '$2a$' + crypto.randomBytes(10).toString('base64'); - case 'sha256': + case EncryptionMethod.sha256: return '$5$' + crypto.randomBytes(10).toString('base64'); - case 'sha512': + case EncryptionMethod.sha512: return '$6$' + crypto.randomBytes(10).toString('base64'); default: diff --git a/packages/core/htpasswd/src/htpasswd.ts b/packages/core/htpasswd/src/htpasswd.ts index 3285e56eb..065ab49b5 100644 --- a/packages/core/htpasswd/src/htpasswd.ts +++ b/packages/core/htpasswd/src/htpasswd.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import Path from 'path'; -import { Callback, Config, IPluginAuth, PluginOptions } from '@verdaccio/types'; +import { Callback, Config, IPluginAuth, Logger, PluginOptions } from '@verdaccio/types'; import { unlockFile } from '@verdaccio/file-locking'; import { @@ -11,12 +11,18 @@ import { addUserToHTPasswd, changePasswordToHTPasswd, sanityCheck, + HtpasswdHashAlgorithm, + HtpasswdHashConfig, } from './utils'; export type HTPasswdConfig = { file: string; + algorithm?: HtpasswdHashAlgorithm; + rounds?: number; } & Config; +export const DEFAULT_BCRYPT_ROUNDS = 10; + /** * HTPasswd - Verdaccio auth class */ @@ -31,8 +37,9 @@ export default class HTPasswd implements IPluginAuth { private config: {}; private verdaccioConfig: Config; private maxUsers: number; + private hashConfig: HtpasswdHashConfig; private path: string; - private logger: {}; + private logger: Logger; private lastTime: any; // constructor public constructor(config: HTPasswdConfig, stuff: PluginOptions<{}>) { @@ -51,6 +58,28 @@ export default class HTPasswd implements IPluginAuth { // all this "verdaccio_config" stuff is for b/w compatibility only this.maxUsers = config.max_users ? config.max_users : Infinity; + let algorithm: HtpasswdHashAlgorithm; + let rounds: number | undefined; + + if (config.algorithm === undefined) { + algorithm = HtpasswdHashAlgorithm.bcrypt; + } else if (HtpasswdHashAlgorithm[config.algorithm] !== undefined) { + algorithm = HtpasswdHashAlgorithm[config.algorithm]; + } else { + throw new Error(`Invalid algorithm "${config.algorithm}"`); + } + + if (algorithm === HtpasswdHashAlgorithm.bcrypt) { + rounds = config.rounds || DEFAULT_BCRYPT_ROUNDS; + } else if (config.rounds !== undefined) { + this.logger.warn({ algo: algorithm }, 'Option "rounds" is not valid for "@{algo}" algorithm'); + } + + this.hashConfig = { + algorithm, + rounds, + }; + this.lastTime = null; const { file } = config; @@ -148,7 +177,7 @@ export default class HTPasswd implements IPluginAuth { } try { - this._writeFile(addUserToHTPasswd(body, user, password), cb); + this._writeFile(addUserToHTPasswd(body, user, password, this.hashConfig), cb); } catch (err) { return cb(err); } @@ -242,7 +271,10 @@ export default class HTPasswd implements IPluginAuth { } try { - this._writeFile(changePasswordToHTPasswd(body, user, password, newPassword), cb); + this._writeFile( + changePasswordToHTPasswd(body, user, password, newPassword, this.hashConfig), + cb + ); } catch (err) { return cb(err); } diff --git a/packages/core/htpasswd/src/utils.ts b/packages/core/htpasswd/src/utils.ts index 078c60296..c3bec075f 100644 --- a/packages/core/htpasswd/src/utils.ts +++ b/packages/core/htpasswd/src/utils.ts @@ -9,6 +9,18 @@ import { API_ERROR, HTTP_STATUS } from '@verdaccio/commons-api'; import crypt3 from './crypt3'; +export enum HtpasswdHashAlgorithm { + md5 = 'md5', + sha1 = 'sha1', + crypt = 'crypt', + bcrypt = 'bcrypt', +} + +export interface HtpasswdHashConfig { + algorithm: HtpasswdHashAlgorithm; + rounds?: number; +} + // this function neither unlocks file nor closes it // it'll have to be done manually later export function lockAndRead(name: string, cb: Callback): void { @@ -60,6 +72,41 @@ export function verifyPassword(passwd: string, hash: string): boolean { return md5(passwd, hash) === hash || crypt3(passwd, hash) === hash; } +/** + * generateHtpasswdLine - generates line for htpasswd file. + * @param {string} user + * @param {string} passwd + * @param {HtpasswdHashConfig} hashConfig + * @returns {string} + */ +export function generateHtpasswdLine( + user: string, + passwd: string, + hashConfig: HtpasswdHashConfig +): string { + let hash: string; + + switch (hashConfig.algorithm) { + case HtpasswdHashAlgorithm.bcrypt: + hash = bcrypt.hashSync(passwd, hashConfig.rounds); + break; + case HtpasswdHashAlgorithm.crypt: + hash = crypt3(passwd); + break; + case HtpasswdHashAlgorithm.md5: + hash = md5(passwd); + break; + case HtpasswdHashAlgorithm.sha1: + hash = '{SHA}' + crypto.createHash('sha1').update(passwd, 'utf8').digest('base64'); + break; + default: + throw createError('Unexpected hash algorithm'); + } + + const comment = 'autocreated ' + new Date().toJSON(); + return `${user}:${hash}:${comment}\n`; +} + /** * addUserToHTPasswd - Generate a htpasswd format for .htpasswd * @param {string} body @@ -67,7 +114,12 @@ export function verifyPassword(passwd: string, hash: string): boolean { * @param {string} passwd * @returns {string} */ -export function addUserToHTPasswd(body: string, user: string, passwd: string): string { +export function addUserToHTPasswd( + body: string, + user: string, + passwd: string, + hashConfig: HtpasswdHashConfig +): string { if (user !== encodeURIComponent(user)) { const err = createError('username should not contain non-uri-safe characters'); @@ -75,13 +127,7 @@ export function addUserToHTPasswd(body: string, user: string, passwd: string): s throw err; } - if (crypt3) { - passwd = crypt3(passwd); - } else { - passwd = '{SHA}' + crypto.createHash('sha1').update(passwd, 'utf8').digest('base64'); - } - const comment = 'autocreated ' + new Date().toJSON(); - let newline = `${user}:${passwd}:${comment}\n`; + let newline = generateHtpasswdLine(user, passwd, hashConfig); if (body.length && body[body.length - 1] !== '\n') { newline = '\n' + newline; @@ -139,10 +185,6 @@ export function sanityCheck( return null; } -export function getCryptoPassword(password: string): string { - return `{SHA}${crypto.createHash('sha1').update(password, 'utf8').digest('base64')}`; -} - /** * changePasswordToHTPasswd - change password for existing user * @param {string} body @@ -155,26 +197,16 @@ export function changePasswordToHTPasswd( body: string, user: string, passwd: string, - newPasswd: string + newPasswd: string, + hashConfig: HtpasswdHashConfig ): string { let lines = body.split('\n'); lines = lines.map((line) => { - const [username, password] = line.split(':', 3); + const [username, hash] = line.split(':', 3); if (username === user) { - let _passwd; - let _newPasswd; - if (crypt3) { - _passwd = crypt3(passwd, password); - _newPasswd = crypt3(newPasswd); - } else { - _passwd = getCryptoPassword(passwd); - _newPasswd = getCryptoPassword(newPasswd); - } - - if (password == _passwd) { - // replace old password hash with new password hash - line = line.replace(_passwd, _newPasswd); + if (verifyPassword(passwd, hash)) { + line = generateHtpasswdLine(user, newPasswd, hashConfig); } else { throw new Error('Invalid old Password'); } diff --git a/packages/core/htpasswd/tests/__snapshots__/utils.test.ts.snap b/packages/core/htpasswd/tests/__snapshots__/utils.test.ts.snap index 282edfdaf..ba3c7f818 100644 --- a/packages/core/htpasswd/tests/__snapshots__/utils.test.ts.snap +++ b/packages/core/htpasswd/tests/__snapshots__/utils.test.ts.snap @@ -1,17 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`addUserToHTPasswd - crypt3 should add new htpasswd to the end 1`] = ` +exports[`addUserToHTPasswd - bcrypt should add new htpasswd to the end 1`] = ` +"username:$2a$10$......................7zqaLmaKtn.i7IjPfuPGY2Ah/mNM6Sy:autocreated 2018-01-14T11:17:40.712Z +" +`; + +exports[`addUserToHTPasswd - bcrypt should add new htpasswd to the end in multiline input 1`] = ` +"test1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z + test2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z +username:$2a$10$......................7zqaLmaKtn.i7IjPfuPGY2Ah/mNM6Sy:autocreated 2018-01-14T11:17:40.712Z +" +`; + +exports[`addUserToHTPasswd - bcrypt should throw an error for incorrect username with space 1`] = `"username should not contain non-uri-safe characters"`; + +exports[`changePasswordToHTPasswd should change the password 1`] = ` +"root:$2a$10$......................0qqDmeqkAfPx68M2ArX8hVzcVNft5Ha:autocreated 2018-01-14T11:17:40.712Z +" +`; + +exports[`generateHtpasswdLine should correctly generate line for bcrypt 1`] = ` +"username:$2a$04$......................LAtw7/ohmmBAhnXqmkuIz83Rl5Qdjhm:autocreated 2018-01-14T11:17:40.712Z +" +`; + +exports[`generateHtpasswdLine should correctly generate line for crypt 1`] = ` "username:$66to3JK5RgZM:autocreated 2018-01-14T11:17:40.712Z " `; -exports[`addUserToHTPasswd - crypt3 should add new htpasswd to the end in multiline input 1`] = ` -"test1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z - test2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z -username:$66to3JK5RgZM:autocreated 2018-01-14T11:17:40.712Z +exports[`generateHtpasswdLine should correctly generate line for md5 1`] = ` +"username:$apr1$MMMMMMMM$2lGUwLC3NFfN74jH51z1W.:autocreated 2018-01-14T11:17:40.712Z " `; -exports[`addUserToHTPasswd - crypt3 should throw an error for incorrect username with space 1`] = `"username should not contain non-uri-safe characters"`; - -exports[`changePasswordToHTPasswd should change the password 1`] = `"root:$6JaJqI5HUf.Q:autocreated 2018-08-20T13:38:12.164Z"`; +exports[`generateHtpasswdLine should correctly generate line for sha1 1`] = ` +"username:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=:autocreated 2018-01-14T11:17:40.712Z +" +`; diff --git a/packages/core/htpasswd/tests/crypt3.test.ts b/packages/core/htpasswd/tests/crypt3.test.ts index a0239fec2..50ec4feb4 100644 --- a/packages/core/htpasswd/tests/crypt3.test.ts +++ b/packages/core/htpasswd/tests/crypt3.test.ts @@ -1,10 +1,10 @@ -import { createSalt } from '../src/crypt3'; +import { createSalt, EncryptionMethod } from '../src/crypt3'; jest.mock('crypto', () => { return { - randomBytes: (): { toString: () => string } => { + randomBytes: (len: number): { toString: () => string } => { return { - toString: (): string => '/UEGzD0RxSNDZA==', + toString: (): string => '/UEGzD0RxSNDZA=='.substring(0, len), }; }, }; @@ -12,20 +12,20 @@ jest.mock('crypto', () => { describe('createSalt', () => { test('should match with the correct salt type', () => { - expect(createSalt('crypt')).toEqual('/UEGzD0RxSNDZA=='); - expect(createSalt('md5')).toEqual('$1$/UEGzD0RxSNDZA=='); - expect(createSalt('blowfish')).toEqual('$2a$/UEGzD0RxSNDZA=='); - expect(createSalt('sha256')).toEqual('$5$/UEGzD0RxSNDZA=='); - expect(createSalt('sha512')).toEqual('$6$/UEGzD0RxSNDZA=='); + expect(createSalt(EncryptionMethod.crypt)).toEqual('/U'); + expect(createSalt(EncryptionMethod.md5)).toEqual('$1$/UEGzD0RxS'); + expect(createSalt(EncryptionMethod.blowfish)).toEqual('$2a$/UEGzD0RxS'); + expect(createSalt(EncryptionMethod.sha256)).toEqual('$5$/UEGzD0RxS'); + expect(createSalt(EncryptionMethod.sha512)).toEqual('$6$/UEGzD0RxS'); }); test('should fails on unkwon type', () => { expect(function () { - createSalt('bad'); + createSalt('bad' as any); }).toThrow(/Unknown salt type at crypt3.createSalt: bad/); }); test('should generate legacy crypt salt by default', () => { - expect(createSalt()).toEqual(createSalt('crypt')); + expect(createSalt()).toEqual(createSalt(EncryptionMethod.crypt)); }); }); diff --git a/packages/core/htpasswd/tests/htpasswd.test.ts b/packages/core/htpasswd/tests/htpasswd.test.ts index c788bb22b..4c0782dab 100644 --- a/packages/core/htpasswd/tests/htpasswd.test.ts +++ b/packages/core/htpasswd/tests/htpasswd.test.ts @@ -3,7 +3,10 @@ import crypto from 'crypto'; // @ts-ignore import fs from 'fs'; +import MockDate from 'mockdate'; + import HTPasswd, { VerdaccioConfigApp } from '../src/htpasswd'; +import { HtpasswdHashAlgorithm } from '../src/utils'; // FIXME: remove this mocks imports import Logger from './__mocks__/Logger'; @@ -19,11 +22,16 @@ const config = { max_users: 1000, }; +const getDefaultConfig = (): VerdaccioConfigApp => ({ + file: './htpasswd', + max_users: 1000, +}); + describe('HTPasswd', () => { let wrapper; beforeEach(() => { - wrapper = new HTPasswd(config, (stuff as unknown) as VerdaccioConfigApp); + wrapper = new HTPasswd(getDefaultConfig(), (stuff as unknown) as VerdaccioConfigApp); jest.resetModules(); crypto.randomBytes = jest.fn(() => { @@ -34,13 +42,21 @@ describe('HTPasswd', () => { }); describe('constructor()', () => { + const emptyPluginOptions = { config: {} } as VerdaccioConfigApp; + test('should files whether file path does not exist', () => { expect(function () { - new HTPasswd({}, ({ - config: {}, - } as unknown) as VerdaccioConfigApp); + new HTPasswd({}, emptyPluginOptions); }).toThrow(/should specify "file" in config/); }); + + test('should throw error about incorrect algorithm', () => { + expect(function () { + let config = getDefaultConfig(); + config.algorithm = 'invalid' as any; + new HTPasswd(config, emptyPluginOptions); + }).toThrow(/Invalid algorithm "invalid"/); + }); }); describe('authenticate()', () => { @@ -85,6 +101,9 @@ describe('HTPasswd', () => { dataToWrite = data; callback(); }); + + MockDate.set('2018-01-14T11:17:40.712Z'); + const callback = (a, b): void => { expect(a).toBeNull(); expect(b).toBeTruthy(); @@ -100,6 +119,7 @@ describe('HTPasswd', () => { jest.doMock('../src/utils.ts', () => { return { sanityCheck: (): Error => Error('some error'), + HtpasswdHashAlgorithm, }; }); @@ -117,6 +137,7 @@ describe('HTPasswd', () => { return { sanityCheck: (): any => null, lockAndRead: (_a, b): any => b(new Error('lock error')), + HtpasswdHashAlgorithm, }; }); @@ -136,6 +157,7 @@ describe('HTPasswd', () => { parseHTPasswd: (): void => {}, lockAndRead: (_a, b): any => b(null, ''), unlockFile: (_a, b): any => b(), + HtpasswdHashAlgorithm, }; }); @@ -153,6 +175,7 @@ describe('HTPasswd', () => { parseHTPasswd: (): void => {}, lockAndRead: (_a, b): any => b(null, ''), addUserToHTPasswd: (): void => {}, + HtpasswdHashAlgorithm, }; }); jest.doMock('fs', () => { diff --git a/packages/core/htpasswd/tests/utils.test.ts b/packages/core/htpasswd/tests/utils.test.ts index bafcb33ab..1c1015a81 100644 --- a/packages/core/htpasswd/tests/utils.test.ts +++ b/packages/core/htpasswd/tests/utils.test.ts @@ -1,5 +1,8 @@ import crypto from 'crypto'; +import MockDate from 'mockdate'; + +import { DEFAULT_BCRYPT_ROUNDS } from '../src/htpasswd'; import { verifyPassword, lockAndRead, @@ -7,12 +10,28 @@ import { addUserToHTPasswd, sanityCheck, changePasswordToHTPasswd, - getCryptoPassword, + generateHtpasswdLine, + HtpasswdHashAlgorithm, } from '../src/utils'; const mockReadFile = jest.fn(); const mockUnlockFile = jest.fn(); +const defaultHashConfig = { + algorithm: HtpasswdHashAlgorithm.bcrypt, + rounds: DEFAULT_BCRYPT_ROUNDS, +}; + +const mockTimeAndRandomBytes = () => { + MockDate.set('2018-01-14T11:17:40.712Z'); + crypto.randomBytes = jest.fn(() => { + return { + toString: (): string => '$6', + }; + }); + Math.random = jest.fn(() => 0.38849); +}; + jest.mock('@verdaccio/file-locking', () => ({ readFile: () => mockReadFile(), unlockFile: () => mockUnlockFile(), @@ -85,51 +104,53 @@ describe('verifyPassword', () => { }); }); -describe('addUserToHTPasswd - crypt3', () => { - beforeAll(() => { - // @ts-ignore - global.Date = jest.fn(() => { - return { - parse: jest.fn(), - toJSON: (): string => '2018-01-14T11:17:40.712Z', - }; - }); +describe('generateHtpasswdLine', () => { + beforeAll(mockTimeAndRandomBytes); - crypto.randomBytes = jest.fn(() => { - return { - toString: (): string => '$6', - }; - }); + const [user, passwd] = ['username', 'password']; + + it('should correctly generate line for md5', () => { + const md5Conf = { algorithm: HtpasswdHashAlgorithm.md5 }; + expect(generateHtpasswdLine(user, passwd, md5Conf)).toMatchSnapshot(); }); + it('should correctly generate line for sha1', () => { + const sha1Conf = { algorithm: HtpasswdHashAlgorithm.sha1 }; + expect(generateHtpasswdLine(user, passwd, sha1Conf)).toMatchSnapshot(); + }); + + it('should correctly generate line for crypt', () => { + const cryptConf = { algorithm: HtpasswdHashAlgorithm.crypt }; + expect(generateHtpasswdLine(user, passwd, cryptConf)).toMatchSnapshot(); + }); + + it('should correctly generate line for bcrypt', () => { + const bcryptAlgoConfig = { + algorithm: HtpasswdHashAlgorithm.bcrypt, + rounds: 2, + }; + expect(generateHtpasswdLine(user, passwd, bcryptAlgoConfig)).toMatchSnapshot(); + }); +}); + +describe('addUserToHTPasswd - bcrypt', () => { + beforeAll(mockTimeAndRandomBytes); + it('should add new htpasswd to the end', () => { const input = ['', 'username', 'password']; - expect(addUserToHTPasswd(input[0], input[1], input[2])).toMatchSnapshot(); + expect(addUserToHTPasswd(input[0], input[1], input[2], defaultHashConfig)).toMatchSnapshot(); }); it('should add new htpasswd to the end in multiline input', () => { const body = `test1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z test2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z`; const input = [body, 'username', 'password']; - expect(addUserToHTPasswd(input[0], input[1], input[2])).toMatchSnapshot(); + expect(addUserToHTPasswd(input[0], input[1], input[2], defaultHashConfig)).toMatchSnapshot(); }); it('should throw an error for incorrect username with space', () => { const [a, b, c] = ['', 'firstname lastname', 'password']; - expect(() => addUserToHTPasswd(a, b, c)).toThrowErrorMatchingSnapshot(); - }); -}); - -// ToDo: mock crypt3 with false -describe('addUserToHTPasswd - crypto', () => { - it('should create password with crypto', () => { - jest.resetModules(); - jest.doMock('../src/crypt3.ts', () => false); - const input = ['', 'username', 'password']; - const utils = require('../src/utils.ts'); - expect(utils.addUserToHTPasswd(input[0], input[1], input[2])).toEqual( - 'username:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=:autocreated 2018-01-14T11:17:40.712Z\n' - ); + expect(() => addUserToHTPasswd(a, b, c, defaultHashConfig)).toThrowErrorMatchingSnapshot(); }); }); @@ -224,7 +245,13 @@ describe('changePasswordToHTPasswd', () => { const body = 'test:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z'; try { - changePasswordToHTPasswd(body, 'test', 'somerandompassword', 'newPassword'); + changePasswordToHTPasswd( + body, + 'test', + 'somerandompassword', + 'newPassword', + defaultHashConfig + ); } catch (error) { expect(error.message).toEqual('Invalid old Password'); } @@ -233,38 +260,8 @@ describe('changePasswordToHTPasswd', () => { test('should change the password', () => { const body = 'root:$6qLTHoPfGLy2:autocreated 2018-08-20T13:38:12.164Z'; - expect(changePasswordToHTPasswd(body, 'root', 'demo123', 'newPassword')).toMatchSnapshot(); - }); - - test('should generate a different result on salt change', () => { - crypto.randomBytes = jest.fn(() => { - return { - toString: (): string => 'AB', - }; - }); - - const body = 'root:$6qLTHoPfGLy2:autocreated 2018-08-20T13:38:12.164Z'; - - expect(changePasswordToHTPasswd(body, 'root', 'demo123', 'demo123')).toEqual( - 'root:ABfaAAjDKIgfw:autocreated 2018-08-20T13:38:12.164Z' - ); - }); - - test('should change the password when crypt3 is not available', () => { - jest.resetModules(); - jest.doMock('../src/crypt3.ts', () => false); - const utils = require('../src/utils.ts'); - const body = 'username:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=:autocreated 2018-01-14T11:17:40.712Z'; - expect(utils.changePasswordToHTPasswd(body, 'username', 'password', 'newPassword')).toEqual( - 'username:{SHA}KD1HqTOO0RALX+Klr/LR98eZv9A=:autocreated 2018-01-14T11:17:40.712Z' - ); - }); -}); - -describe('getCryptoPassword', () => { - test('should return the password hash', () => { - const passwordHash = `{SHA}y9vkk2zovmMYTZ8uE/wkkjQ3G5o=`; - - expect(getCryptoPassword('demo123')).toBe(passwordHash); + expect( + changePasswordToHTPasswd(body, 'root', 'demo123', 'newPassword', defaultHashConfig) + ).toMatchSnapshot(); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d58851dd2..364e66550 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -349,6 +349,7 @@ importers: devDependencies: '@types/bcryptjs': 2.4.2 '@verdaccio/types': link:../types + mockdate: 3.0.2 specifiers: '@types/bcryptjs': ^2.4.2 '@verdaccio/commons-api': workspace:10.0.0-alpha.3 @@ -357,6 +358,7 @@ importers: apache-md5: 1.1.2 bcryptjs: 2.4.3 http-errors: 1.8.0 + mockdate: ^3.0.2 unix-crypt-td-js: 1.1.4 packages/core/local-storage: dependencies: @@ -19620,6 +19622,10 @@ packages: hasBin: true resolution: integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + /mockdate/3.0.2: + dev: true + resolution: + integrity: sha512-ldfYSUW1ocqSHGTK6rrODUiqAFPGAg0xaHqYJ5tvj1hQyFsjuHpuWgWFTZWwDVlzougN/s2/mhDr8r5nY5xDpA== /modify-values/1.0.1: dev: true engines: