Skip to content
This repository was archived by the owner on Jun 27, 2023. It is now read-only.

add Argon2i support #43

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"author": "Florian Kapfenberger <[email protected]>",
"license": "MIT",
"dependencies": {
"argon2": "^0.26.2",
"backpack-core": "^0.7.0",
"body-parser": "^1.18.2",
"bytes": "^3.1.0",
"dotenv": "^6.0.0",
"express": "^4.16.2",
"express-jwt": "^5.3.0",
Expand Down
4 changes: 3 additions & 1 deletion sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ MAILMAN_DB_DATABASE=vmail
MAILMAN_HOST=127.0.0.1
MAILMAN_PORT=4000
MAILMAN_BASENAME=/
[email protected]
[email protected]
MAILMAN_PW_SCHEMA=SHA512-CRYPT
MAILMAN_PW_ARGON2I_MEMORY=64MB
20 changes: 11 additions & 9 deletions src/controllers/accountController.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,16 @@ class AccountController {
if (!username || !domain || !password)
return res.status(400).json({ message: "parameters are missing" });

const id = (await Account.createAccount({
username,
domain,
password,
quota,
enabled,
sendonly
}))[0];
const id = (
await Account.createAccount({
username,
domain,
password,
quota,
enabled,
sendonly
})
)[0];
if (id) {
res.json({ account: (await Account.getAccount({ id }))[0] });
} else {
Expand Down Expand Up @@ -141,7 +143,7 @@ class AccountController {
}

// check passwords
const authenticated = Account.comparePasswords(
const authenticated = await Account.comparePasswords(
currentPassword,
account.password
);
Expand Down
5 changes: 4 additions & 1 deletion src/controllers/authenticationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ class AuthenticationController {
}

// check passwords
const authenticated = Account.comparePasswords(password, account.password);
const authenticated = await Account.comparePasswords(
password,
account.password
);

if (!authenticated) {
return res.status(401).json({ message: `credentials mismatch` });
Expand Down
108 changes: 92 additions & 16 deletions src/model/account.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,71 @@
import db from "../db";
import crypto from "crypto";
import { sha512crypt } from "sha512crypt-node";
import argon2 from "argon2";
import bytes from "bytes";

class PasswordHash {
isValidFor(schema) {
throw new Error("not implemented!");
}

async generateHash(password) {
throw new Error("not implemented!");
}

async comparePasswords(hash, password) {
throw new Error("not implemented!");
}
}

class SHA512Hash extends PasswordHash {
isValidFor(schema) {
return "SHA512-CRYPT" === schema;
}

randomSalt() {
return crypto.randomBytes(64).toString("hex");
}

getSaltFromHash(hash) {
// example:
// $6$24923bb9fc4a008d$D.aFhvUgjHL9RtXgTH8bDf9MS6MVVTBMMSLPON9OBzeMtVVUKnnrLBInjXNKCvGg5xZGDKFOX2Zhb/3mM7HYF0
const [, , salt] = hash.split("$");
return salt;
}

async generateHash(password, salt = this.randomSalt()) {
return sha512crypt(password, salt);
}

async comparePasswords(hash, password) {
const salt = this.getSaltFromHash(hashPassword);
const plainPasswordHash = await this.hashPassword(plainPassword, salt);

return plainPasswordHash === hashPassword;
}
}

class Argon2iHash extends PasswordHash {
isValidFor(schema) {
return "ARGON2I" === schema;
}

async generateHash(password) {
let memoryStr = process.env.MAILMAN_PW_ARGON2I_MEMORY || "64MB";
let memoryCost = bytes.parse(memoryStr) / 1024;

return await argon2.hash(password, {
memoryCost
});
}

async comparePasswords(hash, password) {
return await argon2.verify(hash, password);
}
}

const hashFunctions = [new SHA512Hash(), new Argon2iHash()];

class Account {
async getAccounts() {
Expand Down Expand Up @@ -31,14 +96,14 @@ class Account {

async createAccount(fields) {
if (fields.password) {
fields.password = this.hashPassword(fields.password);
fields.password = await this.hashPassword(fields.password);
}
return await db("accounts").insert(fields);
}

async updateAccount(fields, id) {
if (fields.password) {
fields.password = this.hashPassword(fields.password);
fields.password = await this.hashPassword(fields.password);
}
return await db("accounts")
.update(fields)
Expand All @@ -51,25 +116,36 @@ class Account {
.where({ id });
}

randomSalt() {
return crypto.randomBytes(64).toString("hex");
}
async hashPassword(password) {
// use old hash function for backwards compatibility
let schema = process.env.MAILMAN_PW_SCHEMA || "SHA512-CRYPT";

getSaltFromHash(hash) {
// $6$24923bb9fc4a008d$D.aFhvUgjHL9RtXgTH8bDf9MS6MVVTBMMSLPON9OBzeMtVVUKnnrLBInjXNKCvGg5xZGDKFOX2Zhb/3mM7HYF0
const [, , salt] = hash.split("$");
return salt;
}
let result = await hashFunctions.reduce(async (prev, hashFunction) => {
if (!prev && hashFunction.isValidFor(schema)) {
let hash = await hashFunction.comparePasswords(hashOnly, plaintext);
}
return prev;
}, Promise.resolve(null));

hashPassword(password, salt = this.randomSalt()) {
return `{SHA512-CRYPT}${sha512crypt(password, salt)}`;
if (result === null) {
throw new Error(
"the given password hash function is not implemented: " + schema
);
}
return result;
}

comparePasswords(plainPassword, hashPassword) {
const salt = this.getSaltFromHash(hashPassword);
const plainPasswordHash = this.hashPassword(plainPassword, salt);
async comparePasswords(plaintext, hashed) {
let [dovecotSchema, hashPart] = hashed.split("$", 2);
let schema = dovecotSchema.substring(1, dovecotSchema.length - 1);
let hashOnly = `$${hashPart}`;

return plainPasswordHash === hashPassword;
return await hashFunctions.reduce(async (prev, hashFunction) => {
if (!prev && hashFunction.isValidFor(schema)) {
return await hashFunction.comparePasswords(hashOnly, plaintext);
}
return prev;
}, Promise.resolve(false));
}
}

Expand Down