0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-03-04 02:02:39 -05:00
verdaccio/packages/plugins/htpasswd/src/utils.ts

221 lines
5.9 KiB
TypeScript

import md5 from 'apache-md5';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import createError, { HttpError } from 'http-errors';
import { API_ERROR, HTTP_STATUS } from '@verdaccio/core';
import { readFile } from '@verdaccio/file-locking';
import { Callback } from '@verdaccio/types';
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 {
readFile(name, { lock: true }, (err, res) => {
if (err) {
return cb(err);
}
return cb(null, res);
});
}
/**
* parseHTPasswd - convert htpasswd lines to object.
* @param {string} input
* @returns {object}
*/
export function parseHTPasswd(input: string): Record<string, any> {
// The input is split on line ending styles that are both windows and unix compatible
return input.split(/[\r]?[\n]/).reduce((result, line) => {
const args = line.split(':', 3).map((str) => str.trim());
if (args.length > 1) {
result[args[0]] = args[1];
}
return result;
}, {});
}
/**
* verifyPassword - matches password and it's hash.
* @param {string} passwd
* @param {string} hash
* @returns {Promise<boolean>}
*/
export async function verifyPassword(passwd: string, hash: string): Promise<boolean> {
if (hash.match(/^\$2([aby])\$/)) {
return await bcrypt.compare(passwd, hash);
} else if (hash.indexOf('{PLAIN}') === 0) {
return passwd === hash.slice(7);
} else if (hash.indexOf('{SHA}') === 0) {
return (
crypto
.createHash('sha1')
// https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding
.update(passwd, 'utf8')
.digest('base64') === hash.slice(5)
);
}
// for backwards compatibility, first check md5 then check crypt3
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
* @param {string} user
* @param {string} passwd
* @param {HtpasswdHashConfig} hashConfig
* @returns {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');
err.status = HTTP_STATUS.CONFLICT;
throw err;
}
let newline = generateHtpasswdLine(user, passwd, hashConfig);
if (body.length && body[body.length - 1] !== '\n') {
newline = '\n' + newline;
}
return body + newline;
}
/**
* Sanity check for a user
* @param {string} user
* @param {object} users
* @param {string} password
* @param {Callback} verifyFn
* @param {number} maxUsers
* @returns {object}
*/
export async function sanityCheck(
user: string,
password: string,
verifyFn: Callback,
users: {},
maxUsers: number
): Promise<HttpError | null> {
let err;
// check for user or password
if (!user || !password) {
err = Error(API_ERROR.USERNAME_PASSWORD_REQUIRED);
err.status = HTTP_STATUS.BAD_REQUEST;
return err;
}
const hash = users[user];
if (maxUsers < 0) {
err = Error(API_ERROR.REGISTRATION_DISABLED);
err.status = HTTP_STATUS.CONFLICT;
return err;
}
if (hash) {
const auth = await verifyFn(password, users[user]);
if (auth) {
err = Error(API_ERROR.USERNAME_ALREADY_REGISTERED);
err.status = HTTP_STATUS.CONFLICT;
return err;
}
err = Error(API_ERROR.UNAUTHORIZED_ACCESS);
err.status = HTTP_STATUS.UNAUTHORIZED;
return err;
} else if (Object.keys(users).length >= maxUsers) {
err = Error(API_ERROR.MAX_USERS_REACHED);
err.status = HTTP_STATUS.FORBIDDEN;
return err;
}
return null;
}
/**
* changePasswordToHTPasswd - change password for existing user
* @param {string} body
* @param {string} user
* @param {string} passwd
* @param {string} newPasswd
* @param {HtpasswdHashConfig} hashConfig
* @returns {string}
*/
export async function changePasswordToHTPasswd(
body: string,
user: string,
passwd: string,
newPasswd: string,
hashConfig: HtpasswdHashConfig
): Promise<string> {
let lines = body.split('\n');
const userLineIndex = lines.findIndex((line) => line.split(':', 1).shift() === user);
if (userLineIndex === -1) {
throw new Error(`Unable to change password for user '${user}': user does not currently exist`);
}
const [username, hash] = lines[userLineIndex].split(':', 2);
const passwordValid = await verifyPassword(passwd, hash);
if (!passwordValid) {
throw new Error(`Unable to change password for user '${user}': invalid old password`);
}
const updatedUserLine = generateHtpasswdLine(username, newPasswd, hashConfig);
lines.splice(userLineIndex, 1, updatedUserLine);
return lines.join('\n');
}