Strengthen password in LDAP using SHA512 crypt algorithm

Uses the crypt() function to do password encryption. This function handles
several schemes such as: MD5, Blowfish, SHA1, SHA2.
SHA-512 is used in Authelia for best security.
The algorithm is fully described in
https://www.akkadia.org/drepper/SHA-crypt.txt

The 'crypt3' npm package has been added as a dependency to use the crypt()
function. The package needs to be compiled in order to call the c function,
that's why python, make and C++ compiler are installed temporarily in the
Docker image.
This commit is contained in:
Clement Michaud 2017-10-20 00:42:33 +02:00
parent 22d56b1faa
commit 7b68a543bf
19 changed files with 95 additions and 68 deletions

View File

@ -1,9 +1,13 @@
FROM node:7-alpine
FROM node:8.7.0-alpine
WORKDIR /usr/src
COPY package.json /usr/src/package.json
RUN npm install --production
RUN apk --update add --no-cache --virtual \
.build-deps make g++ python && \
npm install --production && \
apk del .build-deps
COPY dist/server /usr/src/server
COPY dist/shared /usr/src/shared

View File

@ -5,7 +5,6 @@ services:
- ./test:/usr/src/test
- ./dist/server:/usr/src/server
- ./dist/shared:/usr/src/shared
- ./node_modules:/usr/src/node_modules
- ./config.template.yml:/etc/authelia/config.yml:ro
networks:
- example-network

View File

@ -2,3 +2,6 @@ olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonymou
s auth by * none
# olcAccess: {1}to dn.base="" by * read
# olcAccess: {2}to * by * read
olcPasswordHash: {CRYPT}
olcPasswordCryptSaltFormat: $6$rounds=50000$%.16s

View File

@ -27,7 +27,7 @@ objectclass: inetOrgPerson
objectclass: top
mail: john.doe@authelia.com
sn: John Doe
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
dn: cn=harry,ou=users,dc=example,dc=com
cn: harry
@ -35,7 +35,7 @@ objectclass: inetOrgPerson
objectclass: top
mail: harry.potter@authelia.com
sn: Harry Potter
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
dn: cn=bob,ou=users,dc=example,dc=com
cn: bob
@ -43,7 +43,7 @@ objectclass: inetOrgPerson
objectclass: top
mail: bob.dylan@authelia.com
sn: Bob Dylan
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
dn: cn=james,ou=users,dc=example,dc=com
cn: james
@ -51,7 +51,7 @@ objectclass: inetOrgPerson
objectclass: top
mail: james.dean@authelia.com
sn: James Dean
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
dn: cn=blackhat,ou=users,dc=example,dc=com
cn: blackhat
@ -59,4 +59,4 @@ objectclass: inetOrgPerson
objectclass: top
mail: billy.blackhat@authelia.com
sn: Billy BlackHat
userpassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/

View File

@ -27,7 +27,7 @@
"bluebird": "3.5.0",
"body-parser": "^1.15.2",
"connect-redis": "^3.3.0",
"dovehash": "0.0.5",
"crypt3": "^1.0.0",
"ejs": "^2.5.5",
"express": "^4.14.0",
"express-request-id": "^1.4.0",

View File

@ -15,7 +15,6 @@ const yamlContent = YAML.load(configurationFilepath);
const deps: GlobalDependencies = {
u2f: require("u2f"),
dovehash: require("dovehash"),
ldapjs: require("ldapjs"),
session: require("express-session"),
winston: require("winston"),

View File

@ -7,7 +7,6 @@ import Exceptions = require("./Exceptions");
import fs = require("fs");
import ejs = require("ejs");
import { IUserDataStore } from "./storage/IUserDataStore";
import { Winston } from "../../types/Dependencies";
import express = require("express");
import ErrorReplies = require("./ErrorReplies");
import AuthenticationSessionHandler = require("./AuthenticationSession");

View File

@ -69,7 +69,8 @@ export class ServerVariablesInitializer {
const mailSenderBuilder = new MailSenderBuilder(Nodemailer);
const notifier = NotifierFactory.build(config.notifier, mailSenderBuilder);
const ldapClientFactory = new LdapClientFactory(config.ldap, deps.ldapjs);
const clientFactory = new ClientFactory(config.ldap, ldapClientFactory, deps.dovehash, deps.winston);
const clientFactory = new ClientFactory(config.ldap, ldapClientFactory,
deps.winston);
const ldapAuthenticator = new Authenticator(config.ldap, clientFactory);
const ldapPasswordUpdater = new PasswordUpdater(config.ldap, clientFactory);

View File

@ -1,9 +1,5 @@
import util = require("util");
import BluebirdPromise = require("bluebird");
import exceptions = require("../Exceptions");
import Dovehash = require("dovehash");
import { EventEmitter } from "events";
import { IClient, GroupsAndEmails } from "./IClient";
import { ILdapClient } from "./ILdapClient";
@ -11,20 +7,18 @@ import { ILdapClientFactory } from "./ILdapClientFactory";
import { LdapConfiguration } from "../configuration/Configuration";
import { Winston } from "../../../types/Dependencies";
import Util = require("util");
import { HashGenerator } from "../utils/HashGenerator";
export class Client implements IClient {
private userDN: string;
private password: string;
private ldapClient: ILdapClient;
private logger: Winston;
private dovehash: typeof Dovehash;
private options: LdapConfiguration;
constructor(userDN: string, password: string, options: LdapConfiguration,
ldapClientFactory: ILdapClientFactory, dovehash: typeof Dovehash, logger: Winston) {
ldapClientFactory: ILdapClientFactory, logger: Winston) {
this.options = options;
this.dovehash = dovehash;
this.logger = logger;
this.userDN = userDN;
this.password = password;
@ -128,18 +122,22 @@ export class Client implements IClient {
modifyPassword(username: string, newPassword: string): BluebirdPromise<void> {
const that = this;
const encodedPassword = this.dovehash.encode("SSHA", newPassword);
const change = {
operation: "replace",
modification: {
userPassword: encodedPassword
}
};
this.logger.debug("LDAP: update password of user '%s'", username);
return this.searchUserDn(username)
.then(function (userDN: string) {
that.ldapClient.modifyAsync(userDN, change);
return BluebirdPromise.join(
HashGenerator.ssha512(newPassword),
BluebirdPromise.resolve(userDN));
})
.then(function (res: string[]) {
const change = {
operation: "replace",
modification: {
userPassword: res[0]
}
};
that.logger.debug("Password new='%s'", change.modification.userPassword);
return that.ldapClient.modifyAsync(res[1], change);
})
.then(function () {
return that.ldapClient.unbindAsync();

View File

@ -4,27 +4,24 @@ import { Client } from "./Client";
import { SanitizedClient } from "./SanitizedClient";
import { ILdapClientFactory } from "./ILdapClientFactory";
import { LdapConfiguration } from "../configuration/Configuration";
import Ldapjs = require("ldapjs");
import Dovehash = require("dovehash");
import Winston = require("winston");
export class ClientFactory implements IClientFactory {
private config: LdapConfiguration;
private ldapClientFactory: ILdapClientFactory;
private dovehash: typeof Dovehash;
private logger: typeof Winston;
constructor(ldapConfiguration: LdapConfiguration, ldapClientFactory: ILdapClientFactory,
dovehash: typeof Dovehash, logger: typeof Winston) {
constructor(ldapConfiguration: LdapConfiguration,
ldapClientFactory: ILdapClientFactory,
logger: typeof Winston) {
this.config = ldapConfiguration;
this.ldapClientFactory = ldapClientFactory;
this.dovehash = dovehash;
this.logger = logger;
}
create(userDN: string, password: string): IClient {
return new SanitizedClient(new Client(userDN, password, this.config, this.ldapClientFactory,
this.dovehash, this.logger));
return new SanitizedClient(new Client(userDN, password,
this.config, this.ldapClientFactory, this.logger));
}
}

View File

@ -0,0 +1,21 @@
import BluebirdPromise = require("bluebird");
import RandomString = require("randomstring");
import Util = require("util");
const crypt = require("crypt3");
export class HashGenerator {
static ssha512(password: string, salt?: string): BluebirdPromise<string> {
const rounds = 500000;
const saltSize = 16;
// $6 means SHA512
const _salt = Util.format("$6$rounds=%d$%s", rounds,
(salt) ? salt : RandomString.generate(16));
const cryptAsync = BluebirdPromise.promisify<string, string, string>(crypt);
return cryptAsync(password, _salt)
.then(function (hash: string) {
return BluebirdPromise.resolve(Util.format("{CRYPT}%s", hash));
});
}
}

View File

@ -7,7 +7,6 @@ import winston = require("winston");
import speakeasy = require("speakeasy");
import u2f = require("u2f");
import session = require("express-session");
import { AppConfiguration, UserConfiguration } from "../src/lib/configuration/Configuration";
import { GlobalDependencies } from "../types/Dependencies";
import Server from "../src/lib/Server";
@ -34,8 +33,7 @@ describe("test server configuration", function () {
})
},
session: sessionMock as any,
ConnectRedis: Sinon.spy(),
dovehash: Sinon.spy() as any
ConnectRedis: Sinon.spy()
};
});

View File

@ -62,8 +62,7 @@ describe("test session configuration builder", function () {
session: Sinon.spy() as any,
speakeasy: Sinon.spy() as any,
u2f: Sinon.spy() as any,
winston: Sinon.spy() as any,
dovehash: Sinon.spy() as any
winston: Sinon.spy() as any
};
const options = SessionConfigurationBuilder.build(configuration, deps);
@ -144,8 +143,7 @@ describe("test session configuration builder", function () {
session: Sinon.spy() as any,
speakeasy: Sinon.spy() as any,
u2f: Sinon.spy() as any,
winston: Sinon.spy() as any,
dovehash: Sinon.spy() as any
winston: Sinon.spy() as any
};
const options = SessionConfigurationBuilder.build(configuration, deps);

View File

@ -7,7 +7,6 @@ import { LdapClientStub } from "../mocks/ldap/LdapClientStub";
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import Assert = require("assert");
import Dovehash = require("dovehash");
import Winston = require("winston");
describe("test authelia ldap client", function () {
@ -34,7 +33,7 @@ describe("test authelia ldap client", function () {
ldapClient.searchAsyncStub.returns(BluebirdPromise.resolve([{
cn: "group1"
}]));
const client = new Client(ADMIN_USER_DN, ADMIN_PASSWORD, options, factory, Dovehash, Winston);
const client = new Client(ADMIN_USER_DN, ADMIN_PASSWORD, options, factory, Winston);
return client.searchGroups("user1")
.then(function () {
@ -80,7 +79,7 @@ describe("test authelia ldap client", function () {
cn: "group1"
}]));
const client = new Client(ADMIN_USER_DN, ADMIN_PASSWORD, options, factory, Dovehash, Winston);
const client = new Client(ADMIN_USER_DN, ADMIN_PASSWORD, options, factory, Winston);
return client.searchGroups("user1")
.then(function (groups: string[]) {

View File

@ -1,13 +1,11 @@
import { PasswordUpdater } from "../../src/lib/ldap/PasswordUpdater";
import { LdapConfiguration } from "../../src/lib/configuration/Configuration";
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird");
import Assert = require("assert");
import { PasswordUpdater } from "../../src/lib/ldap/PasswordUpdater";
import { LdapConfiguration } from "../../src/lib/configuration/Configuration";
import { ClientFactoryStub } from "../mocks/ldap/ClientFactoryStub";
import { ClientStub } from "../mocks/ldap/ClientStub";
import { HashGenerator } from "../../src/lib/utils/HashGenerator";
describe("test password update", function () {
const USERNAME = "username";
@ -18,10 +16,9 @@ describe("test password update", function () {
let clientFactoryStub: ClientFactoryStub;
let adminClientStub: ClientStub;
let passwordUpdater: PasswordUpdater;
let ldapConfig: LdapConfiguration;
let dovehash: any;
let ssha512HashGenerator: Sinon.SinonStub;
beforeEach(function () {
clientFactoryStub = new ClientFactoryStub();
@ -39,19 +36,20 @@ describe("test password update", function () {
users_filter: "cn={0}"
};
dovehash = {
encode: Sinon.stub()
};
ssha512HashGenerator = Sinon.stub(HashGenerator, "ssha512");
passwordUpdater = new PasswordUpdater(ldapConfig, clientFactoryStub);
});
afterEach(function () {
ssha512HashGenerator.restore();
});
describe("success", function () {
it("should update the password successfully", function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub);
dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
ssha512HashGenerator.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD).returns(BluebirdPromise.resolve());
adminClientStub.openStub.returns(BluebirdPromise.resolve());
adminClientStub.closeStub.returns(BluebirdPromise.resolve());
@ -65,7 +63,7 @@ describe("test password update", function () {
clientFactoryStub.createStub.withArgs(ADMIN_USER_DN, ADMIN_PASSWORD)
.returns(adminClientStub);
dovehash.encode.returns("{SSHA}AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
ssha512HashGenerator.returns("{CRYPT}$6$abcdefghijklm$AQmxaKfobGY9HSQa6aDYkAWOgPGNhGYn");
adminClientStub.modifyPasswordStub.withArgs(USERNAME, NEW_PASSWORD)
.returns(BluebirdPromise.reject(new Error("Error while updating password")));
adminClientStub.openStub.returns(BluebirdPromise.resolve());

View File

@ -116,8 +116,7 @@ describe("Private pages of the server must not be accessible without session", f
session: ExpressSession,
winston: Winston,
speakeasy: speakeasy,
ConnectRedis: Sinon.spy(),
dovehash: Sinon.spy() as any
ConnectRedis: Sinon.spy()
};
server = new Server(deps);

View File

@ -116,8 +116,7 @@ describe("Public pages of the server must be accessible without session", functi
session: ExpressSession,
winston: Winston,
speakeasy: speakeasy,
ConnectRedis: Sinon.spy(),
dovehash: Sinon.spy() as any
ConnectRedis: Sinon.spy()
};
server = new Server(deps);

View File

@ -0,0 +1,18 @@
import Assert = require("assert");
import { HashGenerator } from "../../src/lib/utils/HashGenerator";
describe("test HashGenerator", function () {
it("should compute correct ssha512 (password)", function () {
return HashGenerator.ssha512("password", "jgiCMRyGXzoqpxS3")
.then(function (hash: string) {
Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/");
});
});
it("should compute correct ssha512 (test)", function () {
return HashGenerator.ssha512("test", "abcdefghijklmnop")
.then(function (hash: string) {
Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs.");
});
});
});

View File

@ -6,9 +6,7 @@ import nedb = require("nedb");
import ldapjs = require("ldapjs");
import u2f = require("u2f");
import RedisSession = require("connect-redis");
import dovehash = require("dovehash");
export type Dovehash = typeof dovehash;
export type Speakeasy = typeof speakeasy;
export type Winston = typeof winston;
export type Session = typeof session;
@ -19,7 +17,6 @@ export type ConnectRedis = typeof RedisSession;
export interface GlobalDependencies {
u2f: U2f;
dovehash: Dovehash;
ldapjs: Ldapjs;
session: Session;
ConnectRedis: ConnectRedis;