keystroke back end

parent 9075f16a
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
"name": "keystrokedynamics", "name": "keystrokedynamics",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "server.js",
"scripts": { "scripts": {
"build": "yarn tsc", "build": "yarn tsc",
"start": "yarn build && node lib/index.js", "start": "yarn build && node lib/index.js",
...@@ -13,6 +12,7 @@ ...@@ -13,6 +12,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.1", "@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/node": "^18.15.1",
"eslint": "^7.5.0", "eslint": "^7.5.0",
"eslint-config-airbnb": "^18.2.0", "eslint-config-airbnb": "^18.2.0",
"eslint-config-airbnb-base": "^14.2.0", "eslint-config-airbnb-base": "^14.2.0",
......
...@@ -2,3 +2,14 @@ export const JWT_SECRET = "JWT_SECRET_SMART_RECRUITER"; ...@@ -2,3 +2,14 @@ export const JWT_SECRET = "JWT_SECRET_SMART_RECRUITER";
export const MONGO_URL = export const MONGO_URL =
"mongodb+srv://smart_req_admin:CvCbwaX6A4rnJuwv@smartreq.duejzyf.mongodb.net/App?retryWrites=true&w=majority"; "mongodb+srv://smart_req_admin:CvCbwaX6A4rnJuwv@smartreq.duejzyf.mongodb.net/App?retryWrites=true&w=majority";
export const API_PORT = process.env.API_PORT || 5000; export const API_PORT = process.env.API_PORT || 5000;
export const DEFAULT_CONTROLS = {
standard: {
sd: 1.5,
threshold: 65,
use: true,
},
fullStandard: {
threshold: 1,
use: true,
},
};
...@@ -44,6 +44,7 @@ export type AuthType = { ...@@ -44,6 +44,7 @@ export type AuthType = {
password: string; password: string;
keystrokeDataTimestamps: number[]; keystrokeDataTimestamps: number[];
keystrokeData: KeystrokeDataType; keystrokeData: KeystrokeDataType;
controls?: ControlsType;
}; };
export type CandidateType = { export type CandidateType = {
...@@ -101,3 +102,18 @@ export type SignUpPayload = { ...@@ -101,3 +102,18 @@ export type SignUpPayload = {
candidate?: CandidateType; candidate?: CandidateType;
organization?: OrganizationType; organization?: OrganizationType;
}; };
export type SignInPayload = {
password: string;
keydown: KeyDetails[];
keyup: KeyDetails[];
email: string;
userType: USER_TYPE;
};
export type UpdatePasswordPayload = {
passwords: string[];
keydown: KeyDetails[][];
keyup: KeyDetails[][];
oldPassword: string;
};
import express from "express"; import express from "express";
import * as mongoose from "mongoose"; import mongoose from "mongoose";
import cors from "cors";
import { API_PORT, MONGO_URL } from "./config/contants"; import { API_PORT, MONGO_URL } from "./config/contants";
const app = express(); const app = express();
app.use(cors());
// Routes // Routes
const userRoute = require("./routes/user"); const authRoute = require("./routes/auth");
// Environment constants // Environment constants
// Service Initialisation // Service Initialisation
mongoose.connect(MONGO_URL, { mongoose.connect(MONGO_URL, {
useFindAndModify: false, useFindAndModify: false,
useNewUrlParser: true, useNewUrlParser: true,
useUnifiedTopology: true, useUnifiedTopology: true,
useCreateIndex: true,
}); });
// const db = mongoose.connection;
// db.on("error", (error) => console.log(error, "connection error:"));
// db.once("open", () => {
// console.log("Connected to MongoDB Instance");
// });
// Express Initialisation // Express Initialisation
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
...@@ -23,6 +34,6 @@ app.use(express.json()); ...@@ -23,6 +34,6 @@ app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
// Routes // Routes
app.use("/user", userRoute); app.use("/auth", authRoute);
app.listen(API_PORT, () => console.log(`Listening on port ${API_PORT}`)); app.listen(API_PORT, () => console.log(`Listening on port ${API_PORT}`));
...@@ -7,14 +7,14 @@ export const authMiddleware = ( ...@@ -7,14 +7,14 @@ export const authMiddleware = (
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ) => {
const token = req.header("Authorization"); const token = req.header("auth_token");
if (!token) { if (!token) {
return res.status(401).send("Authorization denied"); return res.status(401).send("Authorization denied");
} else { } else {
const decodedToken = <jwt.JwtPayload>jwt.verify(token, JWT_SECRET); const decodedToken = <jwt.JwtPayload>jwt.verify(token, JWT_SECRET);
if (decodedToken) { if (decodedToken) {
req.query.id = decodedToken.id; req.query.userId = decodedToken.userId;
return next(); return next();
} else { } else {
return res.status(401).send("Unauthorized Access"); return res.status(401).send("Unauthorized Access");
......
...@@ -49,6 +49,17 @@ const authSchema = new Schema<AuthType>({ ...@@ -49,6 +49,17 @@ const authSchema = new Schema<AuthType>({
covMatrix: [[Number]], covMatrix: [[Number]],
}, },
}, },
controls: {
standard: {
sd: { type: Number, default: 1.5, require: false },
threshold: { type: Number, default: 65, require: false },
use: { type: Boolean, default: true, require: false },
},
fullStandard: {
threshold: { type: Number, default: 1, require: false },
use: { type: Boolean, default: true, require: false },
},
},
}); });
const Auth = model<AuthType>("Auth", authSchema); const Auth = model<AuthType>("Auth", authSchema);
......
import * as express from "express"; import { Router } from "express";
import * as _ from "lodash"; import * as _ from "lodash";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import * as bcrypt from "bcryptjs"; import * as bcrypt from "bcryptjs";
import { import {
ControlsType,
KeystrokeDataType, KeystrokeDataType,
SignInPayload,
SignUpPayload, SignUpPayload,
TypedRequest, TypedRequest,
UpdatePasswordPayload,
USER_TYPE, USER_TYPE,
} from "../config/types"; } from "../config/types";
import Auth from "../models/Auth"; import Auth from "../models/Auth";
...@@ -18,11 +21,12 @@ import { ...@@ -18,11 +21,12 @@ import {
addAttemptToKeystrokeData, addAttemptToKeystrokeData,
computeDataTendencies, computeDataTendencies,
processAttempt, processAttempt,
updatePasswordFromProcessedData,
} from "../utilities/keystroke"; } from "../utilities/keystroke";
import { JWT_SECRET } from "../config/contants"; import { DEFAULT_CONTROLS, JWT_SECRET } from "../config/contants";
import { authMiddleware } from "../middlewares/auth"; import { authMiddleware } from "../middlewares/auth";
const router = express.Router(); const router = Router();
router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => { router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => {
const { const {
...@@ -41,7 +45,7 @@ router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => { ...@@ -41,7 +45,7 @@ router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => {
if (auth) { if (auth) {
return res return res
.status(403) .status(403)
.json({ success: false, msg: "User already exists in DB" }); .json({ success: false, msg: "User already exists!" });
} }
const processedAttempts: KeystrokeDataType[] = Array(attemptCount) const processedAttempts: KeystrokeDataType[] = Array(attemptCount)
...@@ -88,8 +92,8 @@ router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => { ...@@ -88,8 +92,8 @@ router.post("/signup", async (req: TypedRequest<{}, SignUpPayload>, res) => {
} }
}); });
router.post("/login", async (req, res) => { router.post("/login", async (req: TypedRequest<{}, SignInPayload>, res) => {
const { password, keydown, keyup, email, controls, userType } = req.body; const { password, keydown, keyup, email, userType } = req.body;
const auth = await Auth.findOne({ email }); const auth = await Auth.findOne({ email });
...@@ -106,21 +110,14 @@ router.post("/login", async (req, res) => { ...@@ -106,21 +110,14 @@ router.post("/login", async (req, res) => {
const isMatch = await bcrypt.compare(password, auth.password); const isMatch = await bcrypt.compare(password, auth.password);
const credentialsValid = if (!isMatch) {
isMatch &&
_.isEqual(processedAttempt.hold.keys, auth.keystrokeData.hold.keys) &&
_.isEqual(processedAttempt.flight.keys, auth.keystrokeData.flight.keys) &&
_.isEqual(processedAttempt.dd.keys, auth.keystrokeData.dd.keys) &&
_.isEqual(processedAttempt.full.keys, auth.keystrokeData.full.keys);
if (!credentialsValid) {
return res.status(403).json({ success: false, msg: "Invalid Credentials" }); return res.status(403).json({ success: false, msg: "Invalid Credentials" });
} }
const result = processAttempt({ const result = processAttempt({
userKeystrokeData: auth.keystrokeData, userKeystrokeData: auth.keystrokeData,
attemptKeystrokeData: processedAttempt, attemptKeystrokeData: processedAttempt,
controls, controls: auth.controls || DEFAULT_CONTROLS,
}); });
if (result.accepted) { if (result.accepted) {
...@@ -130,7 +127,6 @@ router.post("/login", async (req, res) => { ...@@ -130,7 +127,6 @@ router.post("/login", async (req, res) => {
}); });
await Auth.updateOne({ email }, { $set: newUserData }); await Auth.updateOne({ email }, { $set: newUserData });
}
let user; let user;
...@@ -140,12 +136,16 @@ router.post("/login", async (req, res) => { ...@@ -140,12 +136,16 @@ router.post("/login", async (req, res) => {
user = await Organizations.findById(auth.userId); user = await Organizations.findById(auth.userId);
} }
const token = await jwt.sign(auth, JWT_SECRET, { expiresIn: "2h" }); const token = await jwt.sign({ userId: auth.userId }, JWT_SECRET, {
expiresIn: "2h",
});
return res.json({ return res.json({
success: true,
user, user,
result,
token, token,
keystrokeResult: {
result,
db: { db: {
hold: auth.keystrokeData.hold.means, hold: auth.keystrokeData.hold.means,
flight: auth.keystrokeData.flight.means, flight: auth.keystrokeData.flight.means,
...@@ -164,12 +164,12 @@ router.post("/login", async (req, res) => { ...@@ -164,12 +164,12 @@ router.post("/login", async (req, res) => {
dd: processedAttempt.dd.times, dd: processedAttempt.dd.times,
full: processedAttempt.full.times, full: processedAttempt.full.times,
}, },
controls: auth.controls || DEFAULT_CONTROLS,
},
}); });
}); } else {
return res.status(403).json({ success: false, msg: "Invalid Credentials" });
router.post("/validateKeystroke", authMiddleware, (req, res) => { }
const processedKeystrokeData = processKeystrokeData(req.body);
res.json(processedKeystrokeData);
}); });
router.get("/tendencies/:userId", authMiddleware, async (req, res) => { router.get("/tendencies/:userId", authMiddleware, async (req, res) => {
...@@ -187,4 +187,72 @@ router.get("/tendencies/:userId", authMiddleware, async (req, res) => { ...@@ -187,4 +187,72 @@ router.get("/tendencies/:userId", authMiddleware, async (req, res) => {
.catch((error) => res.status(500).send(error)); .catch((error) => res.status(500).send(error));
}); });
router.post(
"/update",
authMiddleware,
async (req: TypedRequest<{ userId: string }, ControlsType>, res) => {
try {
await Auth.updateOne(
{ userId: req.query.userId },
{ $set: { controls: req.body } }
);
return res.send("SUCCESS");
} catch (error) {
return res.status(400).send("FAILED");
}
}
);
router.post(
"/change-pwd",
authMiddleware,
async (req: TypedRequest<{ userId: string }, UpdatePasswordPayload>, res) => {
const { passwords, keydown, keyup, oldPassword } = req.body;
const attemptCount = passwords.length;
const auth = await Auth.findOne({ userId: req.query.userId });
const isMatch = await bcrypt.compare(oldPassword, auth.password);
if (!isMatch) {
return res
.status(403)
.json({ success: false, msg: "Invalid Credentials" });
}
const processedAttempts: KeystrokeDataType[] = Array(attemptCount)
.fill(0)
.map((v, i) =>
processKeystrokeData({ keydown: keydown[i], keyup: keyup[i] })
);
const passwordsEqual = processedAttempts.every((v) =>
_.isEqual(v.full.keys, processedAttempts[0].full.keys)
);
if (!passwordsEqual) {
return res
.status(403)
.json({ success: false, msg: "Passwords don't match" });
}
try {
const newPasswordData = await updatePasswordFromProcessedData(
passwords,
processedAttempts
);
Auth.updateOne({ userId: req.query.userId }, { $set: newPasswordData });
return res.json({
success: true,
});
} catch (error) {
return res
.status(500)
.json({ success: false, msg: "Error signing up", error });
}
}
);
module.exports = router; module.exports = router;
...@@ -406,3 +406,68 @@ export const processAttempt = ({ ...@@ -406,3 +406,68 @@ export const processAttempt = ({
return result; return result;
}; };
export const updatePasswordFromProcessedData = async (
passwords: string[],
processedData: KeystrokeDataType[] | any
) => {
const keystroke = {
keys: [],
times: [],
sums: [],
means: [],
sd: [],
filteredMeans: [],
filteredSd: [],
covMatrix: [],
};
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(passwords[0], salt);
let newPasswordData: Partial<AuthType> = {
password: hashedPassword,
keystrokeDataTimestamps: [],
keystrokeData: {
hold: keystroke,
flight: keystroke,
dd: keystroke,
full: keystroke,
},
};
newPasswordData = processedData.reduce((acc, v, i) => {
TYPES.map((type) => {
const key = type as keyof KeystrokeDataType;
if (i === 0) {
// Keys of the processed data is checked for equality
// So the first one is used for the dataset
acc.keystrokeData[key].keys = v[key].keys;
const length = v[key].keys.length;
// Length of the processedData array is the number of attempts
// Times is an array of arrays of each attempt
acc.keystrokeData[key].times = Array(v.length).fill([]);
acc.keystrokeData[key].means = Array(length).fill(0);
acc.keystrokeData[key].sd = Array(length).fill(0);
acc.keystrokeData[key].filteredMeans = Array(length).fill(0);
acc.keystrokeData[key].filteredSd = Array(length).fill(0);
}
acc.keystrokeData[key].times[i] = v[key].times;
return type;
});
acc.keystrokeDataTimestamps.push(Date.now());
return acc;
}, newPasswordData);
newPasswordData.keystrokeData = computeDataTendencies(
newPasswordData.keystrokeData
);
return newPasswordData;
};
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": false,
"outDir": "lib",
"sourceMap": true,
"target": "es5", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "module": "commonjs",
"lib": ["es6"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "outDir": "lib",
"esModuleInterop": true, "rootDir": "src",
"allowSyntheticDefaultImports": true,
"strict": false, "strict": false,
"forceConsistentCasingInFileNames": true, "esModuleInterop": true,
"noFallthroughCasesInSwitch": true, "resolveJsonModule": true
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
}, },
"compileOnSave": true, "compileOnSave": true,
"include": ["src"] "include": ["src"]
......
...@@ -140,6 +140,11 @@ ...@@ -140,6 +140,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-18.11.14.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-18.11.14.tgz"
integrity sha512-0KXV57tENYmmJMl+FekeW9V3O/rlcqGQQJ/hNh9r8pKIj304pskWuEd8fCyNT86g/TpO0gcOTiLzsHLEURFMIQ== integrity sha512-0KXV57tENYmmJMl+FekeW9V3O/rlcqGQQJ/hNh9r8pKIj304pskWuEd8fCyNT86g/TpO0gcOTiLzsHLEURFMIQ==
"@types/node@^18.15.1":
version "18.15.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.1.tgz#41dc2bf78e8085a250d4670d95edb7fba621dd29"
integrity sha512-U2TWca8AeHSmbpi314QBESRk7oPjSZjDsR+c+H4ECC1l+kFgpZf8Ydhv3SJpPy51VyZHHqxlb6mTTqYNNRVAIw==
"@types/qs@*": "@types/qs@*":
version "6.9.7" version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
...@@ -478,6 +483,14 @@ core-util-is@~1.0.0: ...@@ -478,6 +483,14 @@ core-util-is@~1.0.0:
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
cross-spawn@^7.0.2: cross-spawn@^7.0.2:
version "7.0.3" version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
...@@ -1528,6 +1541,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: ...@@ -1528,6 +1541,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-assign@^4:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.12.2, object-inspect@^1.9.0: object-inspect@^1.12.2, object-inspect@^1.9.0:
version "1.12.2" version "1.12.2"
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz"
...@@ -2114,7 +2132,7 @@ v8-compile-cache@^2.0.3: ...@@ -2114,7 +2132,7 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
vary@~1.1.2: vary@^1, vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment