const crypto = require("crypto"); const Note = require("@models/Note"); const db = require("@config/database"); const Auth = require("@helpers/Auth"); const cs = require("@helpers/CryptoString"); const speakeasy = require("speakeasy"); let User = (module.exports = {}); const version = "4.0.0"; // 3.7.3 - diff/patch update //Login a user, if that user does not exist create them //Issues login token User.login = (username, password, authToken = null) => { return new Promise((resolve, reject) => { const lowerName = username.toLowerCase(); let statusObject = { success: false, token: null, userId: null, verificationRequired: false, message: "Incorrect Username or Password", }; db.promise() .query("SELECT * FROM user WHERE username = ? LIMIT 1", [lowerName]) .then((rows, fields) => { // // Login User // if (rows[0].length == 1) { //Pull out user data from database results const lookedUpUser = rows[0][0]; //Verify Token if set const tokenValidates = speakeasy.totp.verify({ secret: lookedUpUser["two_fa_secret"], encoding: "base32", token: authToken, window: 2, }); if (lookedUpUser.two_fa_enabled == 1 && !authToken) { statusObject["verificationRequired"] = true; statusObject["message"] = "2FA authentication required."; return resolve(statusObject); } if (lookedUpUser.two_fa_enabled == 1 && !tokenValidates) { statusObject["verificationRequired"] = true; statusObject["message"] = "Invalid Authorization Token."; return resolve(statusObject); } if ( lookedUpUser.two_fa_enabled == 0 || (lookedUpUser.two_fa_enabled == 1 && tokenValidates) ) { //hash the password and check for a match const salt = Buffer.from(lookedUpUser.salt, "binary"); crypto.pbkdf2( password, salt, lookedUpUser.iterations, 512, "sha512", function (err, delivered_key) { if (delivered_key.toString("hex") === lookedUpUser.password) { User.generateMasterKey(lookedUpUser.id, password) .then((result) => User.getMasterKey(lookedUpUser.id, password), ) .then((masterKey) => { User.generateKeypair(lookedUpUser.id, masterKey).then( ({ publicKey, privateKey }) => { //Passback a json web token Auth.createToken(lookedUpUser.id, masterKey).then( (token) => { statusObject["token"] = token; statusObject["userId"] = lookedUpUser.id; statusObject["success"] = true; return resolve(statusObject); }, ); }, ); }); } else { return resolve(statusObject); } }, ); } } else { //If user is not found, say two factor authentication is required statusObject["verificationRequired"] = true; statusObject["message"] = "2FA authentication required."; //Show fake auth token message if (authToken) { statusObject["message"] = "Invalid Authorization Token."; } return resolve(statusObject); } }) .catch(console.log); }); }; //Create user account //Issues login token User.register = (username, password) => { //For some reason, username won't get into the promise. But password will @TODO figure this out const lowerName = username.toLowerCase().trim(); return new Promise((resolve, reject) => { db.promise() .query("SELECT * FROM user WHERE username = ? LIMIT 1", [lowerName]) .then((rows, fields) => { if (rows[0].length === 0) { //No users returned, create new one. Start with hashing password //Params for hash function let shasum = crypto.createHash("sha512"); //Prepare Hash const ran = parseInt(Date.now()); //Get current time in miliseconds const semiRandomInt = Math.floor(Math.random() * 11); //Grab a random number const otherRandomInt = (ran * semiRandomInt + ran) * semiRandomInt - ran; //Mix things up a bit shasum.update("" + otherRandomInt); //Update Hasd const saltString = shasum.digest("hex"); const salt = Buffer.from(saltString, "binary"); //Generate Salt hash const iterations = 25000; crypto.pbkdf2( password, salt, iterations, 512, "sha512", function (err, delivered_key) { //Create new user object with freshly salted password var currentDate = new Date() .toISOString() .slice(0, 19) .replace("T", " "); var new_user = { username: lowerName, password: delivered_key.toString("hex"), salt: salt, iterations: iterations, last_login: currentDate, created: currentDate, }; let userId = null; let newMasterKey = null; db.promise() .query("INSERT INTO user SET ?", new_user) .then((rows, fields) => { userId = rows[0].insertId; return User.generateMasterKey(userId, password); }) .then((result) => { return User.getMasterKey(userId, password); }) .then((masterKey) => { newMasterKey = masterKey; return User.generateKeypair(userId, newMasterKey); }) .then(({ publicKey, privateKey }) => { return Auth.createToken(userId, newMasterKey); }) .then((token) => { return resolve({ token, userId }); }) .catch(console.log); }, ); } else { return reject("Username already in use."); } //END user create }) .catch(console.log); }); }; //Counts notes, pinned notes, archived notes, shared notes, unread notes, total files and types User.getCounts = (userId, extendedOptions) => { return new Promise((resolve, reject) => { let countTotals = { tags: {}, }; // const userHash = cs.hash(String(userId)).toString('base64') db.promise() .query( `SELECT SUM(archived = 1 && share_user_id IS NULL && trashed = 0) AS archivedNotes, SUM(trashed = 1) AS trashedNotes, SUM(share_user_id IS NULL && trashed = 0 AND quick_note < 2) AS totalNotes, SUM(share_user_id IS NOT null && opened IS null && trashed = 0) AS youGotMailCount, SUM(share_user_id != ? && trashed = 0) AS sharedToNotes FROM note LEFT JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) WHERE user_id = ?`, [userId, userId, userId, userId], ) .then((rows, fields) => { Object.assign(countTotals, rows[0][0]); //combine results // // @TODO - Figured out if this is useful // We want, notes shared with user and note user has shared // return db.promise().query( `SELECT count(id) AS sharedFromNotes FROM note WHERE shared = 2 AND user_id = ? AND trashed = 0`, [userId], ); }) .then((rows, fields) => { Object.assign(countTotals, rows[0][0]); //combine results return db.promise().query( `SELECT SUM(attachment_type = 1) as linkFiles, SUM(attachment_type != 1) as otherFiles, COUNT(id) as totalFiles FROM attachment WHERE visible = 1 AND user_id = ? `, [userId], ); }) .then((rows, fields) => { Object.assign(countTotals, rows[0][0]); //combine results return db .promise() .query( "SELECT id AS quickNote FROM note WHERE quick_note = 1 AND user_id = ?", [userId], ); }) .then((rows, fields) => { Object.assign(countTotals, rows[0][0]); //combine results //Count usages of user tags, sort by most popular return db.promise().query( ` SELECT tag.text, COUNT(tag_id) AS uses, tag.id FROM note_tag JOIN tag ON (tag.id = note_tag.tag_id) WHERE user_id = ? GROUP BY tag_id ORDER BY uses DESC LIMIT 16 `, [userId], ); }) .then((rows, fields) => { //Convert everything to an int or 0 Object.keys(countTotals).forEach((key) => { const count = parseInt(countTotals[key]); countTotals[key] = count ? count : 0; }); //Build out tags object let tagsObject = {}; rows[0].forEach((tagRow) => { tagsObject[tagRow["text"]] = { id: tagRow.id, uses: tagRow.uses }; }); //Assign after counts are updated countTotals["tags"] = tagsObject; countTotals["currentVersion"] = version; // Allow for extended options set on page load if (extendedOptions) { db.promise() .query( `SELECT updated FROM note JOIN note_raw_text ON note_raw_text.id = note.note_raw_text_id WHERE note.quick_note = 2 AND user_id = ?`, [userId], ) .then((rows, fields) => { if (rows[0][0] && rows[0][0].updated) { const lastOpened = rows[0][0].updated; const timeDiff = Math.round((+new Date() - lastOpened) / 1000); const hoursInSeconds = 12 * 60 * 60; //12 hours // Show metric tracking button if its been 12 hours since last entry if (lastOpened && timeDiff > hoursInSeconds) { countTotals["showTrackMetricsButton"] = true; } } resolve(countTotals); }); } else { resolve(countTotals); } }); }); }; //Log out user by deleting login token for that active session User.logout = (sessionId) => { console.log("Terminate Session -> ", sessionId); return db .promise() .query("DELETE FROM user_active_session WHERE (session_id = ?)", [ sessionId, ]); }; User.generateMasterKey = (userId, password) => { return new Promise((resolve, reject) => { if (!userId || !password) { reject("Need userId and password to generate key"); } db.promise() .query("SELECT count(id) as total FROM user_key WHERE user_id = ?", [ userId, ]) .then((rows, fields) => { //Entry already exists, you good. if (rows[0][0]["total"] > 0) { return resolve(true); // throw new Error('User Encryption key already exists') } else { // Generate user key, its big and random const masterPassword = cs.createSmallSalt(); //Generate a salt because it wants it const salt = cs.createSmallSalt(); // Encrypt master password const encryptedMasterPassword = cs.encrypt( password, salt, masterPassword, ); const created = Math.round(+new Date() / 1000); db.promise() .query( "INSERT INTO user_key (`user_id`, `salt`, `key`, `created`) VALUES (?, ?, ?, ?);", [userId, salt, encryptedMasterPassword, created], ) .then((results) => { return resolve(true); }); } }) .catch((error) => { console.log("Create Master Password Error"); console.log(error); }); }); }; User.getMasterKey = (userId, password) => { return new Promise((resolve, reject) => { if (!userId || !password) { reject("Need userId and password to fetch key"); } db.promise() .query("SELECT * FROM user_key WHERE user_id = ? LIMIT 1", [userId]) .then((rows, fields) => { const row = rows[0][0]; if (!rows[0] || rows[0].length == 0 || rows[0][0] == undefined) { return reject("Row or salt or something not set"); } const masterKey = cs.decrypt(password, row["salt"], row["key"]); if (masterKey == null) { return reject("Unable to decrypt key"); } return resolve(masterKey); }); }); }; User.generateKeypair = (userId, masterKey) => { let publicKey = null; let privateKey = null; return new Promise((resolve, reject) => { db.promise() .query("SELECT * FROM user_key WHERE user_id = ?", [userId]) .then((rows, fields) => { const row = rows[0][0]; const salt = row["salt"]; publicKey = row["public_key"]; privateKey = row["private_key_encrypted"]; if (row["public_key"] == null) { const keyPair = crypto.generateKeyPairSync("rsa", { modulusLength: 1024, publicKeyEncoding: { type: "spki", format: "pem", }, privateKeyEncoding: { type: "pkcs8", format: "pem", }, }); publicKey = keyPair.publicKey; privateKey = keyPair.privateKey; const privateKeyEncrypted = cs.encrypt(masterKey, salt, privateKey); db.promise() .query( "UPDATE user_key SET `public_key` = ?, `private_key_encrypted` = ? WHERE user_id = ?;", [publicKey, privateKeyEncrypted, userId], ) .then((rows, fields) => { return resolve({ publicKey, privateKey }); }); } else { //Decrypt private key privateKey = cs.decrypt(masterKey, salt, privateKey); return resolve({ publicKey, privateKey }); } }); }); }; User.getPublicKey = (userId) => { return new Promise((resolve, reject) => { db.promise() .query("SELECT public_key FROM user_key WHERE user_id = ?", [userId]) .then((rows, fields) => { const row = rows[0][0]; return resolve(row["public_key"]); }); }); }; User.getPrivateKey = (userId, masterKey) => { return new Promise((resolve, reject) => { db.promise() .query( "SELECT salt, private_key_encrypted FROM user_key WHERE user_id = ?", [userId], ) .then((rows, fields) => { const row = rows[0][0]; const salt = row["salt"]; privateKey = row["private_key_encrypted"]; //Decrypt private key privateKey = cs.decrypt(masterKey, salt, privateKey); return resolve(privateKey); }); }); }; User.getByUserName = (username) => { return new Promise((resolve, reject) => { db.promise() .query("SELECT * FROM user WHERE username = ? LIMIT 1", [ username.toLowerCase(), ]) .then((rows, fields) => { resolve(rows[0][0]); }); }); }; User.changePassword = (userId, oldPass, newPass) => { return new Promise((resolve, reject) => { User.getMasterKey(userId, oldPass) .then((masterKey) => { User.getPrivateKey(userId, masterKey).then((privateKey) => { //If success, user has correct password // Generate new master pass, encrypt with new password // const masterPassword = cs.createSmallSalt() const salt = cs.createSmallSalt(); const encryptedMasterPassword = cs.encrypt(newPass, salt, masterKey); const encryptedPrivateKey = cs.encrypt(masterKey, salt, privateKey); db.promise() .query( "UPDATE user_key SET salt = ?, `key` = ?, private_key_encrypted = ? WHERE user_id = ? LIMIT 1", [salt, encryptedMasterPassword, encryptedPrivateKey, userId], ) .then((r, f) => { //Create login using password let shasum = crypto.createHash("sha512"); //Prepare Hash const saltString = shasum.digest("hex"); const passwordSalt = Buffer.from(saltString, "binary"); //Generate Salt hash const iterations = 25000; crypto.pbkdf2( newPass, passwordSalt, iterations, 512, "sha512", function (err, delivered_key) { const deliveredPass = delivered_key.toString("hex"); db.promise() .query( "UPDATE user SET password = ?, salt = ? WHERE id = ? LIMIT 1", [deliveredPass, passwordSalt, userId], ) .then((r, f) => { return resolve(true); }); }, ); }); }); }) .catch((error) => { resolve(false); }); }); }; User.revokeActiveSessions = (userId, sessionId) => { return new Promise((resolve, reject) => { const userHash = cs.hash(String(userId)).toString("base64"); db.promise() .query( "DELETE FROM user_active_session WHERE user_hash = ? AND session_id != ?", [userHash, sessionId], ) .then((r, f) => { resolve(true); }); }); }; User.deleteUser = (userId, password) => { if (!userId || !password) { return new Promise((resolve, reject) => { return resolve("Missing User ID or Password. No Action Taken."); }); } //Verify user is correct by decryptig master key with password let deletePromises = []; //Delete all notes and raw text let noteDelete = db.promise().query( ` DELETE note, note_raw_text FROM note JOIN note_raw_text ON (note_raw_text.id = note.note_raw_text_id) WHERE note.user_id = ? `, [userId], ); deletePromises.push(noteDelete); //Delete user entry let userDelete = db.promise().query( ` DELETE FROM user WHERE id = ? `, [userId], ); deletePromises.push(userDelete); //Delete user_key, encrypted search index let tables = ["user_key", "user_encrypted_search_index"]; tables.forEach((tableName) => { const query = `DELETE FROM ${tableName} WHERE user_id = ?`; const deleteQuery = db.promise().query(query, [userId]); deletePromises.push(deleteQuery); }); //Remove all note attachments and files return Promise.all(deletePromises); };