Salt and hash passwords
This commit is contained in:
1 parent
88ad9451e2
commit
d5b194b9e8
13 files changed
+410
-83
No files matched your search
+19
-1
@@ -17,7 +17,7 @@
|
||||
* 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): {} {
|
||||
authHeader = authHeader.trim();
|
||||
@@ -119,3 +119,21 @@ export function digest(
|
||||
|
||||
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"));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -954,7 +954,7 @@ export function getPermissions(
|
||||
export function getUsers(
|
||||
callback: (
|
||||
err?: Error,
|
||||
users?: { _id: string; password: string; roles: string }[]
|
||||
users?: { _id: string; password: string; salt: string; roles: string }[]
|
||||
) => void
|
||||
): void {
|
||||
usersCollection.find().toArray(callback);
|
||||
|
||||
@@ -74,6 +74,27 @@ function computeHash(snapshot): string {
|
||||
h.update(JSON.stringify(keys));
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -354,6 +375,7 @@ function refresh(callback): void {
|
||||
for (const user of _users) {
|
||||
users[user._id] = {
|
||||
password: user.password,
|
||||
salt: user.salt,
|
||||
roles: user.roles.split(",").map(s => s.trim())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import Authorizer from "./common/authorizer";
|
||||
import * as logger from "./logger";
|
||||
import * as localCache from "./local-cache";
|
||||
import { PermissionSet } from "./types";
|
||||
import { authSimple } from "./ui/api-functions";
|
||||
|
||||
declare module "koa" {
|
||||
interface Request {
|
||||
@@ -33,14 +34,6 @@ function getPermissionSets(ctx): PermissionSet[] {
|
||||
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 => {
|
||||
throw err;
|
||||
});
|
||||
@@ -100,7 +93,7 @@ router.post("/login", async ctx => {
|
||||
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");
|
||||
|
||||
|
||||
+30
-6
@@ -2,6 +2,8 @@ import { exec } from "child_process";
|
||||
import { ObjectID } from "mongodb";
|
||||
import * as db from "./db";
|
||||
import { del } from "../cache";
|
||||
import { getUsers } from "../local-cache";
|
||||
import { hashPassword } from "../auth";
|
||||
import {
|
||||
insertTasks,
|
||||
watchTask,
|
||||
@@ -186,13 +188,21 @@ export function putResource(resource, id, data): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let promise;
|
||||
|
||||
if (resource === "presets") promise = db.putPreset(id, data);
|
||||
else if (resource === "provisions") promise = db.putProvision(id, data);
|
||||
else if (resource === "virtualParameters")
|
||||
if (resource === "presets") {
|
||||
promise = db.putPreset(id, data);
|
||||
} else if (resource === "provisions") {
|
||||
promise = db.putProvision(id, data);
|
||||
} else if (resource === "virtualParameters") {
|
||||
promise = db.putVirtualParameter(id, data);
|
||||
else if (resource === "config") promise = db.putConfig(id, data);
|
||||
else if (resource === "permissions") promise = db.putPermission(id, data);
|
||||
else if (resource === "users") promise = db.putUser(id, data);
|
||||
} else if (resource === "config") {
|
||||
promise = db.putConfig(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
|
||||
.then(() => {
|
||||
@@ -204,3 +214,17 @@ export function putResource(resource, id, data): Promise<void> {
|
||||
.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);
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { parse } from "../common/expression-parser";
|
||||
import * as logger from "../logger";
|
||||
import { getConfig } from "../local-cache";
|
||||
import { QueryOptions, Expression } from "../types";
|
||||
import { generateSalt, hashPassword } from "../auth";
|
||||
import { del } from "../cache";
|
||||
|
||||
const router = new Router();
|
||||
export default router;
|
||||
@@ -497,3 +499,64 @@ router.get("/ping/:host", async ctx => {
|
||||
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
@@ -82,12 +82,19 @@ export function query(
|
||||
filter = expression.evaluate(filter, null, Date.now());
|
||||
|
||||
if (Array.isArray(filter)) {
|
||||
if (resource === "devices")
|
||||
if (resource === "devices") {
|
||||
filter = mongodbFunctions.processDeviceFilter(filter);
|
||||
else if (resource === "tasks")
|
||||
} else if (resource === "tasks") {
|
||||
filter = mongodbFunctions.processTasksFilter(filter);
|
||||
else if (resource === "faults")
|
||||
} else if (resource === "faults") {
|
||||
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);
|
||||
} else if (filter != null && !filter) {
|
||||
return Promise.resolve([]);
|
||||
@@ -111,7 +118,7 @@ export function query(
|
||||
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.limit) cursor.limit(options.limit);
|
||||
|
||||
Reference in new issue
Block a user