Keystroke auth

parents
Pipeline #6253 failed with stages
data/attempts
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, unique: true },
password: String,
keystrokeDataTimestamps: [Date],
keystrokeData: {
hold: {
keys: [String],
times: [[Number]],
sums: [Number],
means: [Number],
sd: [Number],
filteredMeans: [Number],
filteredSd: [Number],
covMatrix: [[Number]],
},
flight: {
keys: [String],
times: [[Number]],
sums: [Number],
means: [Number],
sd: [Number],
filteredMeans: [Number],
filteredSd: [Number],
covMatrix: [[Number]],
},
dd: {
keys: [String],
times: [[Number]],
sums: [Number],
means: [Number],
sd: [Number],
filteredMeans: [Number],
filteredSd: [Number],
covMatrix: [[Number]],
},
full: {
keys: [String],
times: [[Number]],
sums: [Number],
means: [Number],
sd: [Number],
filteredMeans: [Number],
filteredSd: [Number],
covMatrix: [[Number]],
},
},
});
module.exports = mongoose.model('User', userSchema);
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "keystrokedynamics",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js",
"burnthemall": "rm -rf node_modules package-lock.json && npm i"
},
"author": "Namit Nathwani",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"express-winston": "^4.0.3",
"lodash": "^4.17.19",
"mathjs": "^7.2.0",
"mongoose": "^5.9.25",
"simple-statistics": "^7.1.0",
"winston": "^3.3.3"
},
"devDependencies": {
"eslint": "^7.5.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-plugin-import": "^2.22.0",
"nodemon": "^2.0.4"
}
}
const express = require("express");
const _ = require("lodash");
const { logger } = require("../utilities/loggers");
const {
processKeystrokeData,
findUser,
signUpNewUser,
updateUser,
createSignupDataFromProcessedData,
addAttemptToKeystrokeData,
computeDataTendencies,
processAttempt,
} = require("../utilities/userUtility");
const router = express.Router();
router.use(express.json({ extended: false }));
router.use(express.urlencoded({ extended: false }));
router.post("/validateKeystroke", (req, res) => {
const processedKeystrokeData = processKeystrokeData(req.body);
res.json(processedKeystrokeData);
});
router.get("/find/:username", async (req, res) => {
res.json(await findUser(req.params.username));
});
router.get("/tendencies/:username", async (req, res) => {
const user = await findUser(req.params.username);
return res.json({
db: (await findUser(req.params.username)).keystrokeData,
calc: computeDataTendencies(user.keystrokeData),
});
});
router.post("/signup", async (req, res) => {
const { passwords, keydown, keyup, username } = req.body;
const attemptCount = passwords.length;
const processedAttempts = Array(attemptCount)
.fill()
.map((v, i) =>
processKeystrokeData({ keydown: keydown[i], keyup: keyup[i] })
);
const passwordsEqual = processedAttempts.every((v) =>
_.isEqual(v.full.keys, processedAttempts[0].full.keys)
);
const userInDb = await findUser(username);
if (!passwordsEqual) {
// If the entered passwords don't match
return res
.status(403)
.json({ success: false, msg: "Passwords don't match" });
}
if (userInDb) {
// If the user already exists in the Database
return res
.status(403)
.json({ success: false, msg: "User already exists in DB" });
}
const signupData = createSignupDataFromProcessedData(
username,
passwords,
processedAttempts
);
try {
const newUserData = await signUpNewUser(signupData);
logger.info(`Signed up ${username}`);
return res.json({
success: true,
username: newUserData.username,
});
} catch (error) {
logger.error(error);
logger.debug(req.body);
return res
.status(500)
.json({ success: false, msg: "Error signing up", error });
}
});
router.post("/login", async (req, res) => {
const { password, keydown, keyup, username, controls } = req.body;
const processedAttempt = processKeystrokeData({ keydown, keyup });
const userInDb = await findUser(username);
if (!userInDb) {
// If the user does not exist in the Database
return res
.status(403)
.json({ success: false, msg: "User does not exist in DB" });
}
const credentialsValid =
password === userInDb.password &&
_.isEqual(processedAttempt.hold.keys, userInDb.keystrokeData.hold.keys) &&
_.isEqual(
processedAttempt.flight.keys,
userInDb.keystrokeData.flight.keys
) &&
_.isEqual(processedAttempt.dd.keys, userInDb.keystrokeData.dd.keys) &&
_.isEqual(processedAttempt.full.keys, userInDb.keystrokeData.full.keys);
if (!credentialsValid) {
return res.status(403).json({ success: false, msg: "Invalid Credentials" });
}
const result = processAttempt({
userKeystrokeData: userInDb.keystrokeData,
attemptKeystrokeData: processedAttempt,
controls,
});
if (result.accepted) {
const newUserData = addAttemptToKeystrokeData({
userData: userInDb,
attemptKeystrokeData: processedAttempt,
});
newUserData.__v += 1;
await updateUser({
username,
updateData: newUserData,
});
}
return res.json({
result,
db: {
hold: userInDb.keystrokeData.hold.means,
flight: userInDb.keystrokeData.flight.means,
dd: userInDb.keystrokeData.dd.means,
full: userInDb.keystrokeData.full.means,
},
filteredDb: {
hold: userInDb.keystrokeData.hold.filteredMeans,
flight: userInDb.keystrokeData.flight.filteredMeans,
dd: userInDb.keystrokeData.dd.filteredMeans,
full: userInDb.keystrokeData.full.filteredMeans,
},
attempt: {
hold: processedAttempt.hold.times,
flight: processedAttempt.flight.times,
dd: processedAttempt.dd.times,
full: processedAttempt.full.times,
},
});
});
module.exports = router;
const express = require("express");
const mongoose = require("mongoose");
// Utilities
const { logger, expressWinstonLogger } = require("./utilities/loggers.js");
// Routes
const userRoute = require("./routes/user");
// Environment constants
const API_PORT = process.env.API_PORT || 3001;
const MONGO_URL = "";
// Service Initialisation
mongoose.connect(MONGO_URL, {
useFindAndModify: false,
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on("error", logger.error.bind(logger, "connection error:"));
db.once("open", () => {
logger.info("Connected to MongoDB Instance");
});
// Express Initialisation
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(expressWinstonLogger);
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
if (req.method === "OPTIONS") {
return res.status(200).end();
}
return next();
});
// Routes
app.use("/user", userRoute);
app.get("/", (req, res) => {
res.json({ msg: "Default Route" });
});
app.listen(API_PORT, () => logger.info(`Listening on port ${API_PORT}`));
const winston = require('winston');
const expressWinston = require('express-winston');
// const winstonLevels = {
// error: 0,
// warn: 1,
// info: 2,
// verbose: 3,
// debug: 4,
// silly: 5,
// };
module.exports.logger = winston.createLogger({
transports: [
new winston.transports.Console(),
],
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
});
module.exports.expressWinstonLogger = expressWinston.logger({
transports: [
new winston.transports.Console({ level: 'verbose' }),
],
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
msg() {
return '{{res.statusCode}} {{req.method}} {{req.url}} {{res.responseTime}}ms';
},
colorize: true,
meta: false,
statusLevels: false, // default value
level(req, res) {
let level = '';
if (res.statusCode >= 100) { level = 'verbose'; }
if (res.statusCode >= 400) { level = 'warn'; }
if (res.statusCode >= 500) { level = 'error'; }
// Ops is worried about hacking attempts so make Unauthorized and Forbidden critical
// if (res.statusCode == 401 || res.statusCode == 403) { level = "critical"; }
return level;
},
});
const ss = require("simple-statistics");
const _ = require("lodash");
const { logger } = require("./loggers");
const User = require("../models/User");
const SD_MULTIPLIER = 2.5;
const THRESHOLD_PERCENT = 65;
const FULL_STANDARD_THRESHOLD = 1;
const STANDARD_NORM_MULTIPLIER = 1000;
const TYPES = ["hold", "flight", "dd", "full"];
const norm = (a) => Math.sqrt(ss.sum(a.map((v) => v ** 2)));
const euclidean = (d1, d2, w = 1) => Math.sqrt(w * Math.abs(d2 - d1) ** 2);
const cityblock = (d1, d2) => Math.abs(d1 - d2);
const cityblockArray = (a1, a2) =>
ss.sum(a1.map((v, i) => Math.abs(a1[i] - a2[i])));
const covariance = (x, y, xMean, yMean, n) =>
(1 / (n - 1)) *
ss.sum(
Array(n)
.fill(0)
.map((v, i) => (x[i] - xMean) * (y[i] - yMean))
);
const processKeystrokeData = ({ keydown, keyup }) => {
const data = {
hold: {
keys: [],
times: [],
},
flight: {
keys: [],
times: [],
},
dd: {
keys: [],
times: [],
},
full: {
keys: [],
times: [],
},
};
for (let i = 0; i < keydown.length; i += 1) {
const { code: downCode, key: downKey, time: downTime } = keydown[i];
const { code: upCode, key: upKey, time: upTime } = keyup[i];
const holdTime = upTime - downTime;
if (downKey !== upKey || downCode !== upCode) {
logger.error(`Found a mismatch ${downKey} & ${upKey}`);
logger.error(`Keydown: ${keydown}\nKeyup: ${keyup}`);
}
data.full.keys.push(`H:${downKey}`);
data.full.times.push(holdTime);
data.hold.keys.push(downKey);
data.hold.times.push(holdTime);
if (i < keydown.length - 1) {
const { key: nextDownKey, time: nextDownTime } = keydown[i + 1];
const keyString = `${downKey}:${nextDownKey}`;
const flightTime = nextDownTime - upTime;
const ddTime = nextDownTime - downTime;
data.full.keys.push(`F:${keyString}`);
data.full.times.push(flightTime);
data.full.keys.push(`DD:${keyString}`);
data.full.times.push(ddTime);
data.flight.keys.push(keyString);
data.flight.times.push(flightTime);
data.dd.keys.push(keyString);
data.dd.times.push(ddTime);
}
}
return data;
};
const computeStandardTendencies = (data) => {
const transposedData = _.unzip(data.times);
const means = data.means.map((v, i) => ss.mean(transposedData[i]));
const sd = data.sd.map((v, i) => ss.standardDeviation(transposedData[i]));
return { means, sd };
};
const computeFilteredTendencies = (data, SDFilterMultiplier = 2) => {
const transposedData = _.unzip(data.times).map((row) =>
row.filter(
// Each Row is now the columns of original matrix
// Each element (attempt for a given keystroke) is checked and filtered
(v, i) => euclidean(v, data.means[i]) < SDFilterMultiplier * data.sd[i]
)
);
const filteredMeans = data.filteredMeans.map((v, i) =>
ss.mean(transposedData[i].length === 0 ? [0] : transposedData[i])
);
const filteredSd = data.filteredSd.map((v, i) =>
ss.standardDeviation(
transposedData[i].length === 0 ? [0] : transposedData[i]
)
);
return { filteredMeans, filteredSd };
};
const computeMahalanobis = (data, means) => {
const transTime = _.unzip(data.times);
const obsCount = data.times.length;
const featureCount = data.times[0].length;
const covMatrix = new Array(featureCount)
.fill(0)
.map(() => new Array(featureCount).fill(0));
Array(featureCount)
.fill(0)
.map((v, i) => {
Array(featureCount)
.fill(0)
.map((w, j) => {
covMatrix[i][j] = covariance(
transTime[i],
transTime[j],
means[i],
means[j],
obsCount
);
return j;
});
return i;
});
return { covMatrix };
};
const computeDataTendencies = (keystrokeData) => {
TYPES.map((type) => {
const { means, sd } = computeStandardTendencies(keystrokeData[type]);
keystrokeData[type].means = means;
keystrokeData[type].sd = sd;
const { filteredMeans, filteredSd } = computeFilteredTendencies(
keystrokeData[type]
);
keystrokeData[type].filteredMeans = filteredMeans;
keystrokeData[type].filteredSd = filteredSd;
const { covMatrix } = computeMahalanobis(keystrokeData[type], means);
keystrokeData[type].covMatrix = covMatrix;
return type;
});
return keystrokeData;
};
const createSignupDataFromProcessedData = (
username,
passwords,
processedData
) => {
let signupData = {
username,
password: passwords[0],
keystrokeDataTimestamps: [],
keystrokeData: {
hold: {},
flight: {},
dd: {},
full: {},
},
};
signupData = processedData.reduce((acc, v, i) => {
TYPES.map((type) => {
if (i === 0) {
// Keys of the processed data is checked for equality
// So the first one is used for the dataset
acc.keystrokeData[type].keys = v[type].keys;
// Length of the processedData array is the number of attempts
// Times is an array of arrays of each attempt
acc.keystrokeData[type].times = Array(v.length).fill([]);
acc.keystrokeData[type].means = Array(v[type].keys.length).fill(0);
acc.keystrokeData[type].sd = Array(v[type].keys.length).fill(0);
acc.keystrokeData[type].filteredMeans = Array(v[type].keys.length).fill(
0
);
acc.keystrokeData[type].filteredSd = Array(v[type].keys.length).fill(0);
}
acc.keystrokeData[type].times[i] = v[type].times;
return type;
});
acc.keystrokeDataTimestamps.push(Date.now());
return acc;
}, signupData);
signupData.keystrokeData = computeDataTendencies(signupData.keystrokeData);
return signupData;
};
const findUser = (username) => User.findOne({ username }).exec();
const signUpNewUser = ({
username,
password,
keystrokeData,
keystrokeDataTimestamps,
}) =>
User.create({
username,
password,
keystrokeData,
keystrokeDataTimestamps,
});
const updateUser = ({ username, updateData }) =>
User.updateOne({ username }, updateData).exec();
const addAttemptToKeystrokeData = ({ userData, attemptKeystrokeData }) => {
TYPES.map((type) => {
userData.keystrokeData[type].times.push(attemptKeystrokeData[type].times);
return type;
});
userData.keystrokeDataTimestamps.push(Date.now());
userData.keystrokeData = computeDataTendencies(userData.keystrokeData);
// console.log(userData);
return userData;
};
const processStandard = (userKeystrokeData, attemptKeystrokeData, controls) => {
const { threshold = THRESHOLD_PERCENT, sd: sdMult = SD_MULTIPLIER } =
controls;
const scores = {
distance: {},
inRange: {},
inRangePercent: {},
};
TYPES.map((type) => {
scores.distance[type] = Array(userKeystrokeData[type].means.length).fill(0);
const isInRange = Array(userKeystrokeData[type].means.length).fill(false);
let inRangeCount = 0;
let totalCount = 0;
scores.inRangePercent[type] = 0;
scores.inRange[type] = false;
userKeystrokeData[type].means.map((v, i) => {
const mean = userKeystrokeData[type].means[i];
const sd = userKeystrokeData[type].sd[i];
const attemptTime = attemptKeystrokeData[type].times[i];
const low = mean - sd * sdMult;
const high = mean + sd * sdMult;
const distance = cityblock(mean, attemptTime);
scores.distance[type][i] = distance;
isInRange[i] = attemptTime >= low && attemptTime <= high;
inRangeCount += !!isInRange[i];
totalCount += 1;
return v;
});
scores.inRangePercent[type] = (inRangeCount / totalCount) * 100;
if (scores.inRangePercent[type] >= threshold) {
scores.inRange[type] = true;
}
return type;
});
return scores;
};
const processFullStandard = (
userKeystrokeData,
attemptKeystrokeData,
controls
) => {
const { threshold = FULL_STANDARD_THRESHOLD } = controls;
const scores = {
normedDistance: {},