Salt and hash passwords

This commit is contained in:
Shweelan Samson authored and Zaid Abdulla committed 2019-03-27 02:17:48 -07:00
1 parent 88ad9451e2
commit d5b194b9e8
13 files changed
+381 -54

No files matched your search

+19 -1
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { createHash } from "crypto"; import { createHash, randomBytes, pbkdf2 } from "crypto";
export function parseAuthHeader(authHeader): {} { export function parseAuthHeader(authHeader): {} {
authHeader = authHeader.trim(); authHeader = authHeader.trim();
@@ -119,3 +119,21 @@ export function digest(
return authString; return authString;
} }
export function generateSalt(length): Promise<string> {
return new Promise((resolve, reject) => {
randomBytes(length, (err, rand) => {
if (err) return void reject(err);
resolve(rand.toString("hex"));
});
});
}
export function hashPassword(pass, salt): Promise<string> {
return new Promise((resolve, reject) => {
pbkdf2(pass, salt, 10000, 128, "sha512", (err, hash) => {
if (err) return void reject(err);
resolve(hash.toString("hex"));
});
});
}
+1 -1
View File
@@ -954,7 +954,7 @@ export function getPermissions(
export function getUsers( export function getUsers(
callback: ( callback: (
err?: Error, err?: Error,
users?: { _id: string; password: string; roles: string }[] users?: { _id: string; password: string; salt: string; roles: string }[]
) => void ) => void
): void { ): void {
usersCollection.find().toArray(callback); usersCollection.find().toArray(callback);
+22
View File
@@ -74,6 +74,27 @@ function computeHash(snapshot): string {
h.update(JSON.stringify(keys)); h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.config[k])); for (const k of keys) h.update(JSON.stringify(snapshot.config[k]));
keys = Object.keys(snapshot.files).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.files[k]));
keys = Object.keys(snapshot.users).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.users[k]));
const roles = Object.keys(snapshot.permissions).sort();
h.update(JSON.stringify(roles));
for (const r of roles) {
const levels = Object.keys(snapshot.permissions[r]).sort();
h.update(JSON.stringify(levels));
for (const l of levels) {
keys = Object.keys(snapshot.permissions[r][l]).sort();
h.update(JSON.stringify(keys));
for (const k of keys)
h.update(JSON.stringify(snapshot.permissions[r][l][k]));
}
}
return h.digest("hex"); return h.digest("hex");
} }
@@ -354,6 +375,7 @@ function refresh(callback): void {
for (const user of _users) { for (const user of _users) {
users[user._id] = { users[user._id] = {
password: user.password, password: user.password,
salt: user.salt,
roles: user.roles.split(",").map(s => s.trim()) roles: user.roles.split(",").map(s => s.trim())
}; };
} }
+2 -9
View File
@@ -12,6 +12,7 @@ import Authorizer from "./common/authorizer";
import * as logger from "./logger"; import * as logger from "./logger";
import * as localCache from "./local-cache"; import * as localCache from "./local-cache";
import { PermissionSet } from "./types"; import { PermissionSet } from "./types";
import { authSimple } from "./ui/api-functions";
declare module "koa" { declare module "koa" {
interface Request { interface Request {
@@ -33,14 +34,6 @@ function getPermissionSets(ctx): PermissionSet[] {
return permissionSets; return permissionSets;
} }
function authSimple(ctx, username, password): Promise<string[]> {
const users = localCache.getUsers(ctx.state.configSnapshot);
const user = users[username];
if (user && user.password === password) return Promise.resolve(user.roles);
return Promise.resolve(null);
}
koa.on("error", async err => { koa.on("error", async err => {
throw err; throw err;
}); });
@@ -100,7 +93,7 @@ router.post("/login", async ctx => {
logger.accessWarn(log); logger.accessWarn(log);
} }
const roles = await authSimple(ctx, username, password); const roles = await authSimple(ctx.state.configSnapshot, username, password);
if (roles) return void success(roles, "simple"); if (roles) return void success(roles, "simple");
+30 -6
View File
@@ -2,6 +2,8 @@ import { exec } from "child_process";
import { ObjectID } from "mongodb"; import { ObjectID } from "mongodb";
import * as db from "./db"; import * as db from "./db";
import { del } from "../cache"; import { del } from "../cache";
import { getUsers } from "../local-cache";
import { hashPassword } from "../auth";
import { import {
insertTasks, insertTasks,
watchTask, watchTask,
@@ -186,13 +188,21 @@ export function putResource(resource, id, data): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let promise; let promise;
if (resource === "presets") promise = db.putPreset(id, data); if (resource === "presets") {
else if (resource === "provisions") promise = db.putProvision(id, data); promise = db.putPreset(id, data);
else if (resource === "virtualParameters") } else if (resource === "provisions") {
promise = db.putProvision(id, data);
} else if (resource === "virtualParameters") {
promise = db.putVirtualParameter(id, data); promise = db.putVirtualParameter(id, data);
else if (resource === "config") promise = db.putConfig(id, data); } else if (resource === "config") {
else if (resource === "permissions") promise = db.putPermission(id, data); promise = db.putConfig(id, data);
else if (resource === "users") promise = db.putUser(id, data); } else if (resource === "permissions") {
promise = db.putPermission(id, data);
} else if (resource === "users") {
delete data.password;
delete data.salt;
promise = db.putUser(id, data);
}
promise promise
.then(() => { .then(() => {
@@ -204,3 +214,17 @@ export function putResource(resource, id, data): Promise<void> {
.catch(reject); .catch(reject);
}); });
} }
export function authSimple(snapshot, username, password): Promise<string[]> {
return new Promise((resolve, reject) => {
const users = getUsers(snapshot);
const user = users[username];
if (!user || !user.password) return void resolve(null);
hashPassword(password, user.salt)
.then(hash => {
if (hash === user.password) resolve(user.roles);
else resolve(null);
})
.catch(reject);
});
}
+63
View File
@@ -7,6 +7,8 @@ import { parse } from "../common/expression-parser";
import * as logger from "../logger"; import * as logger from "../logger";
import { getConfig } from "../local-cache"; import { getConfig } from "../local-cache";
import { QueryOptions, Expression } from "../types"; import { QueryOptions, Expression } from "../types";
import { generateSalt, hashPassword } from "../auth";
import { del } from "../cache";
const router = new Router(); const router = new Router();
export default router; export default router;
@@ -497,3 +499,64 @@ router.get("/ping/:host", async ctx => {
ctx.body = `${err.name}: ${err.message}`; ctx.body = `${err.name}: ${err.message}`;
} }
}); });
router.put("/users/:id/password", async (ctx, next) => {
const username = ctx.params.id;
const log = {
message: "Change password",
context: ctx,
username: username
};
const authorizer = ctx.state.authorizer;
if (!ctx.state.user) {
// User not logged in
if (
!(await apiFunctions.authSimple(
ctx.state.configSnapshot,
username,
ctx.request.body.authPassword
))
) {
logUnauthorizedWarning(log);
ctx.status = 401;
ctx.body = "Authentication failed, check your username and password";
return;
}
} else if (!authorizer.hasAccess("users", 3)) {
logUnauthorizedWarning(log);
return void next();
}
const filter = ["=", ["PARAM", RESOURCE_IDS.users], username];
const res = await db.query("users", filter);
if (!res.length) return void next();
const newPassword = ctx.request.body.newPassword;
if (ctx.state.user) {
const validate = authorizer.getValidator("users", res[0]);
if (!validate("password", { password: newPassword })) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
}
const salt = await generateSalt(64);
const password = await hashPassword(newPassword, salt);
await db.putUser(username, { password, salt });
function delCache(): Promise<void> {
return new Promise((resolve, reject) => {
del("presets_hash", err => {
if (err) return void reject(err);
resolve();
});
});
}
await delCache();
logger.accessInfo(log);
ctx.body = "";
});
+11 -4
View File
@@ -82,12 +82,19 @@ export function query(
filter = expression.evaluate(filter, null, Date.now()); filter = expression.evaluate(filter, null, Date.now());
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
if (resource === "devices") if (resource === "devices") {
filter = mongodbFunctions.processDeviceFilter(filter); filter = mongodbFunctions.processDeviceFilter(filter);
else if (resource === "tasks") } else if (resource === "tasks") {
filter = mongodbFunctions.processTasksFilter(filter); filter = mongodbFunctions.processTasksFilter(filter);
else if (resource === "faults") } else if (resource === "faults") {
filter = mongodbFunctions.processFaultsFilter(filter); filter = mongodbFunctions.processFaultsFilter(filter);
} else if (resource === "users") {
// Protect against brute force, and dictionary attacks
const params = expression.extractParams(filter);
if (params.includes("password") || params.includes("salt"))
return Promise.reject(new Error("Invalid users filter"));
}
q = mongodbFunctions.filterToMongoQuery(filter); q = mongodbFunctions.filterToMongoQuery(filter);
} else if (filter != null && !filter) { } else if (filter != null && !filter) {
return Promise.resolve([]); return Promise.resolve([]);
@@ -111,7 +118,7 @@ export function query(
cursor.project(projection); cursor.project(projection);
} }
if (resource === "users") cursor.project({ password: 0 }); if (resource === "users") cursor.project({ password: 0, salt: 0 });
if (options.skip) cursor.skip(options.skip); if (options.skip) cursor.skip(options.skip);
if (options.limit) cursor.limit(options.limit); if (options.limit) cursor.limit(options.limit);
+16 -6
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only #!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import { get } from "../lib/config"; import { get } from "../lib/config";
import { hashPassword, generateSalt } from "../lib/auth";
const permissionsData = [ const permissionsData = [
{ role: "admin", resource: "devices", access: 3, validate: ["pass"] }, { role: "admin", resource: "devices", access: 3, validate: ["pass"] },
@@ -202,12 +203,21 @@ async function run(): Promise<void> {
.collection("permissions") .collection("permissions")
.insertMany(perms) .insertMany(perms)
); );
const users = usersData.map(p => { const saltingProms = [];
p.roles = (p.roles || []).join(","); for (const u of usersData) {
p._id = p.username; saltingProms.push(
delete p.username; new Promise(async resolve => {
return p; u.salt = await generateSalt(64);
}); u.password = await hashPassword(u.password, u.salt);
u.roles = (u.roles || []).join(",");
u._id = u.username;
delete u.username;
resolve(u);
})
);
}
const users = await Promise.all(saltingProms);
inserts.push( inserts.push(
client client
.db() .db()
+116
View File
@@ -0,0 +1,116 @@
import { VnodeDOM, ClosureComponent, Component } from "mithril";
import * as notifications from "./notifications";
import { changePassword } from "./store";
import { m } from "./components";
const component: ClosureComponent = (): Component => {
return {
view: vnode => {
const onPasswordChange = vnode.attrs["onPasswordChange"];
const enforceAuth = !vnode.attrs["noAuth"];
const username = vnode.attrs["username"];
if (username) vnode.state["username"] = username;
const form = [
m(
"p",
m("label", { for: "username" }, "Username"),
m("input", {
name: "username",
type: "text",
value: vnode.state["username"],
disabled: !!username,
oninput: m.withAttr("value", v => (vnode.state["username"] = v)),
oncreate: _vnode => {
(_vnode.dom as HTMLSelectElement).focus();
}
})
)
];
let fields = {
newPassword: "New password",
confirmPassword: "Confirm password"
};
if (enforceAuth)
fields = Object.assign({ authPassword: "Your password" }, fields);
for (const [f, l] of Object.entries(fields)) {
form.push(
m(
"p",
m("label", { for: f }, l),
m("input", {
name: f,
type: "password",
value: vnode.state[f],
oninput: m.withAttr("value", v => (vnode.state[f] = v))
})
)
);
}
const submit = m(
"button.primary",
{
type: "submit"
},
"Change password"
) as VnodeDOM;
form.push(m(".actions-bar", submit));
const children = [
m("h1", "Change password"),
m(
"form",
{
onsubmit: e => {
e.redraw = false;
e.preventDefault();
if (
!vnode.state["username"] ||
!vnode.state["newPassword"] ||
(enforceAuth && !vnode.state["authPassword"])
) {
notifications.push("error", "Please fill all fields");
} else if (
vnode.state["newPassword"] !== vnode.state["confirmPassword"]
) {
notifications.push(
"error",
"Password confirm doesn't match new password"
);
} else {
(submit.dom as HTMLFormElement).disabled = true;
changePassword(
vnode.state["username"],
vnode.state["newPassword"],
vnode.state["authPassword"]
)
.then(() => {
notifications.push(
"success",
"Password updated successfully"
);
if (onPasswordChange) onPasswordChange();
(submit.dom as HTMLFormElement).disabled = false;
})
.catch(err => {
notifications.push("error", err.message);
(submit.dom as HTMLFormElement).disabled = false;
});
}
}
},
form
)
];
return m("div.put-form", children);
}
};
};
export default component;
+21 -1
View File
@@ -1,7 +1,9 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import * as store from "./store"; import * as store from "./store";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
import * as overlay from "./overlay";
import changePasswordComponent from "./change-password-component";
export function init(args): Promise<{}> { export function init(args): Promise<{}> {
return Promise.resolve(args); return Promise.resolve(args);
@@ -63,6 +65,24 @@ export const component: ClosureComponent = (): Component => {
"Login" "Login"
) )
) )
),
m(
"a",
{
onclick: () => {
const cb = (): Children => {
const attrs = {
onPasswordChange: () => {
overlay.close(cb);
m.redraw();
}
};
return m(changePasswordComponent, attrs);
};
overlay.open(cb);
}
},
"Change password"
) )
]; ];
} }
+2 -1
View File
@@ -6,7 +6,8 @@ const singular = {
presets: "preset", presets: "preset",
provisions: "provision", provisions: "provision",
virtualParameters: "virtual parameter", virtualParameters: "virtual parameter",
files: "file" files: "file",
users: "user"
}; };
function createField(current, attr, focus): Children { function createField(current, attr, focus): Children {
+15
View File
@@ -549,6 +549,21 @@ export function evaluateExpression(exp, obj): Expression {
return memoizedEvaluate(exp, obj, fulfillTimestamp); return memoizedEvaluate(exp, obj, fulfillTimestamp);
} }
export function changePassword(
username,
newPassword,
authPassword?
): Promise<void> {
const data = { newPassword };
if (authPassword) data["authPassword"] = authPassword;
return m.request({
method: "PUT",
url: `/api/users/${username}/password`,
background: true,
data
});
}
export function logIn(username, password): Promise<void> { export function logIn(username, password): Promise<void> {
return m.request({ return m.request({
method: "POST", method: "POST",
+63 -25
View File
@@ -11,6 +11,7 @@ import * as overlay from "./overlay";
import * as smartQuery from "./smart-query"; import * as smartQuery from "./smart-query";
import { map, parse, stringify } from "../lib/common/expression-parser"; import { map, parse, stringify } from "../lib/common/expression-parser";
import filterComponent from "./filter-component"; import filterComponent from "./filter-component";
import changePasswordComponent from "./change-password-component";
import { Children, ClosureComponent, Component } from "mithril"; import { Children, ClosureComponent, Component } from "mithril";
const PAGE_SIZE = config.ui.pageSize || 10; const PAGE_SIZE = config.ui.pageSize || 10;
@@ -20,7 +21,6 @@ const memoizedJsonParse = memoize(JSON.parse);
const attributes = [ const attributes = [
{ id: "_id", label: "Username" }, { id: "_id", label: "Username" },
{ id: "password", label: "Password", type: "password", unsortable: true },
{ {
id: "roles", id: "roles",
label: "Roles", label: "Roles",
@@ -47,19 +47,27 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
const object = Object.assign({}, _object); const object = Object.assign({}, _object);
if (action === "save") { if (action === "save") {
const id = object["_id"]; const id = object["_id"];
const password = object["password"];
const confirm = object["confirm"];
delete object["_id"]; delete object["_id"];
delete object["password"];
delete object["confirm"];
if (!id) return void resolve({ _id: "ID can not be empty" }); if (!id) return void resolve({ _id: "ID can not be empty" });
if (isNew) {
if (!password) {
return void resolve({ password: "Password can not be empty" });
} else if (password !== confirm) {
return void resolve({
confirm: "Confirm password doesn't match password"
});
}
}
if (!Array.isArray(object.roles) || !object.roles.length) if (!Array.isArray(object.roles) || !object.roles.length)
return void resolve({ roles: "Role(s) must be selected" }); return void resolve({ roles: "Role(s) must be selected" });
if (typeof object.password !== "string" || !object.password.length) {
if (isNew)
return void resolve({ password: "Password can not be empty" });
else delete object.password;
}
object.roles = object.roles.join(","); object.roles = object.roles.join(",");
store store
@@ -78,14 +86,22 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
store store
.putResource("users", id, object) .putResource("users", id, object)
.then(() => { .then(() => {
notifications.push( if (isNew) {
"success", store
`User ${exists ? "updated" : "created"}` .changePassword(id, password)
); .then(() => {
notifications.push("success", "User created");
store.fulfill(0, Date.now()); store.fulfill(0, Date.now());
resolve(); resolve();
}) })
.catch(reject); .catch(reject);
} else {
notifications.push("success", "User updated");
store.fulfill(0, Date.now());
resolve();
}
})
.catch(reject);
}) })
.catch(reject); .catch(reject);
} else if (action === "delete") { } else if (action === "delete") {
@@ -106,15 +122,9 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
}); });
} }
const formData = {
resource: "users",
attributes: attributes
};
const getDownloadUrl = memoize(filter => { const getDownloadUrl = memoize(filter => {
const cols = {}; const cols = {};
for (const attr of attributes) for (const attr of attributes) cols[attr.label] = attr.id;
if (attr.id !== "password") cols[attr.label] = attr.id;
return `/api/users.csv?${m.buildQueryString({ return `/api/users.csv?${m.buildQueryString({
filter: stringify(filter), filter: stringify(filter),
@@ -158,8 +168,6 @@ function renderTable(
const labels = [m("th", selectAll)]; const labels = [m("th", selectAll)];
for (const attr of attributes) { for (const attr of attributes) {
if (attr.id === "password") continue;
const label = attr.label; const label = attr.label;
if (attr.unsortable) { if (attr.unsortable) {
@@ -187,6 +195,8 @@ function renderTable(
labels.push(m("th", [label, sortable])); labels.push(m("th", [label, sortable]));
} }
const canChangePasswords = window.authorizer.hasAccess("users", 3);
const rows = []; const rows = [];
for (const user of users) { for (const user of users) {
const checkbox = m("input", { const checkbox = m("input", {
@@ -203,8 +213,7 @@ function renderTable(
}); });
const tds = [m("td", checkbox)]; const tds = [m("td", checkbox)];
for (const attr of attributes) for (const attr of attributes) tds.push(m("td", user[attr.id]));
if (attr.id !== "password") tds.push(m("td", user[attr.id]));
tds.push( tds.push(
m( m(
@@ -214,7 +223,8 @@ function renderTable(
{ {
onclick: () => { onclick: () => {
const cb = (): Children => { const cb = (): Children => {
return m( const children = [
m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
{ {
@@ -244,9 +254,28 @@ function renderTable(
}); });
} }
}, },
formData {
resource: "users",
attributes: attributes
}
) )
); )
];
if (canChangePasswords) {
children.push(m("hr"));
const attrs = {
noAuth: true,
username: user._id,
onPasswordChange: () => {
overlay.close(cb);
m.redraw();
}
};
children.push(m(changePasswordComponent, attrs));
}
return children;
}; };
overlay.open(cb); overlay.open(cb);
} }
@@ -331,6 +360,15 @@ function renderTable(
]; ];
if (window.authorizer.hasAccess("users", 3)) { if (window.authorizer.hasAccess("users", 3)) {
const formData = {
resource: "users",
attributes: [
attributes[0],
{ id: "password", label: "Password", type: "password" },
{ id: "confirm", label: "Confirm password", type: "password" },
attributes[1]
]
};
buttons.push( buttons.push(
m( m(
"button.primary", "button.primary",