mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-24 21:15:51 +01:00
parent
31d5828f46
commit
c8a040e69e
32
.changeset/spicy-frogs-press.md
Normal file
32
.changeset/spicy-frogs-press.md
Normal file
@ -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
|
||||
```
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -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<HTPasswdConfig> {
|
||||
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<HTPasswdConfig> {
|
||||
// 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<HTPasswdConfig> {
|
||||
}
|
||||
|
||||
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<HTPasswdConfig> {
|
||||
}
|
||||
|
||||
try {
|
||||
this._writeFile(changePasswordToHTPasswd(body, user, password, newPassword), cb);
|
||||
this._writeFile(
|
||||
changePasswordToHTPasswd(body, user, password, newPassword, this.hashConfig),
|
||||
cb
|
||||
);
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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
|
||||
"
|
||||
`;
|
||||
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
@ -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', () => {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user