diff options
Diffstat (limited to 'src/routers/vault/utils')
-rw-r--r-- | src/routers/vault/utils/claim.js | 83 | ||||
-rw-r--r-- | src/routers/vault/utils/ephemeral.js | 87 | ||||
-rw-r--r-- | src/routers/vault/utils/flatfile.js | 31 | ||||
-rw-r--r-- | src/routers/vault/utils/md5saltcrypt.js | 38 |
4 files changed, 239 insertions, 0 deletions
diff --git a/src/routers/vault/utils/claim.js b/src/routers/vault/utils/claim.js new file mode 100644 index 0000000..472e05a --- /dev/null +++ b/src/routers/vault/utils/claim.js @@ -0,0 +1,83 @@ +const { Op } = require("sequelize"); + +// claim by email // TODO: DRY this +const claim_accounts = async (req, email, vault_id, session = null) => { + const locals = req.app.locals; + + if (email === null || email.length < 5 || email === "a@a.com") + return Promise.resolve(false); + + if (session === null) { + for (const [key, sess] of locals.session) { + // try to find the session + if (sess.authenticated === true && sess.vault === vault_id) { + session = sess; + } + } + } + + // TODO: make these operations less expensive (foreign keys could help) + let already_claimed = await locals.vault.claimed_legacy_accounts.findAll({ + where: {vaultId: vault_id}, + }); + + already_claimed = already_claimed.map(acc => { + return {accountId: { + [Op.not]: acc.accountId, // NOTE: if query is larger than 65535 this will throw + }}; + }); + + const to_claim = await locals.legacy.login.findAll({ + where: { + email: email, + [Op.and]: already_claimed, + }, + }); + + for (const acc of to_claim) { + await locals.vault.claimed_legacy_accounts.create({ + accountId: acc.accountId, + vaultId: vault_id, + }); + + req.app.locals.vault.account_log.create({ + vaultId: vault_id, + accountType: "LEGACY", + actionType: "LINK", + accountId: acc.accountId, + ip: req.app.locals.sequelize.vault.fn("INET6_ATON", req.ip), + }); + + if (session !== null) { + const chars = []; + const chars_ = await locals.legacy.char.findAll({ + where: {accountId: acc.accountId}, + }); + + for (const char of chars_) { + chars.push({ + name: char.name, + charId: char.charId, + revoltId: char.revoltId, + level: char.baseLevel, + sex: char.sex, + }); + } + // add to session cache + session.legacyAccounts.push({ + name: acc.userid, + accountId: acc.accountId, + revoltId: acc.revoltId, + chars, + }); + } + + locals.logger.info(`Vault: linked Legacy account ${acc.accountId} to Vault account {${vault_id}} [${req.ip}]`); + } + + // TODO: split TMWA claiming into its own function, add forums and wiki claiming +}; + +module.exports = { + claim_accounts, +}; diff --git a/src/routers/vault/utils/ephemeral.js b/src/routers/vault/utils/ephemeral.js new file mode 100644 index 0000000..211e84b --- /dev/null +++ b/src/routers/vault/utils/ephemeral.js @@ -0,0 +1,87 @@ +// TODO: use Redis for in-memory caching of sessions + +// this was originally a Proxy<Map<String, Session>> but was replaced because of a nodejs bug +// XXX: maybe we should use an already-existing Express session manager // NIH syndrome +const timeout_symbol = Symbol("timeout"); +const hydrate_symbol = Symbol("hydrate"); +const container_symbol = Symbol("container"); +const session_handler = { + [container_symbol]: new Map(), + [hydrate_symbol] (key, obj) { + if (obj === null || obj === undefined) + return obj; + + if (Reflect.has(obj, timeout_symbol)) + clearTimeout(obj[timeout_symbol]); + + let expires = new Date(); + expires.setUTCHours(expires.getUTCHours() + 6); + obj.expires = expires // this could also be a symbol + obj[timeout_symbol] = setTimeout(() => session_handler.delete(key), 6 * 3600000); // 6 hours + + return obj; + }, + has (key) { + return session_handler[container_symbol].has(key); + }, + get (key) { + return session_handler[hydrate_symbol](key, session_handler[container_symbol].get(key)); + }, + set (key, obj) { + return session_handler[container_symbol].set(key, session_handler[hydrate_symbol](key, obj)); + }, + delete (key) { + if (session_handler[container_symbol].get(key) && session_handler[container_symbol].get(key)[timeout_symbol]) + clearTimeout(session_handler[container_symbol].get(key)[timeout_symbol]); + return session_handler[container_symbol].delete(key); + }, + [Symbol.iterator]: function* () { + for (const [key, obj] of session_handler[container_symbol]) { + yield [key, obj]; + } + }, +}; + +// TODO: DRY this shit +const identity_handler = { + [container_symbol]: new Map(), + [hydrate_symbol] (key, obj) { + if (obj === null || obj === undefined) + return obj; + + if (Reflect.has(obj, timeout_symbol)) + clearTimeout(obj[timeout_symbol]); + + let expires = new Date(); + expires.setUTCMinutes(expires.getUTCMinutes() + 30); + obj.expires = expires // this could also be a symbol + obj[timeout_symbol] = setTimeout(() => identity_handler.delete(key), 30 * 60000); // 30 minutes + + return obj; + }, + has (key) { + return identity_handler[container_symbol].has(key); + }, + get (key) { + return identity_handler[container_symbol].get(key); + }, + set (key, obj) { + return identity_handler[container_symbol].set(key, identity_handler[hydrate_symbol](key, obj)); + }, + delete (key) { + if (identity_handler[container_symbol].get(key) && identity_handler[container_symbol].get(key)[timeout_symbol]) + clearTimeout(identity_handler[container_symbol].get(key)[timeout_symbol]); + return identity_handler[container_symbol].delete(key); + }, + [Symbol.iterator]: function* () { + for (const [key, obj] of identity_handler[container_symbol]) { + yield [key, obj]; + } + }, +}; + + +module.exports = { + session_handler, + identity_handler, +} diff --git a/src/routers/vault/utils/flatfile.js b/src/routers/vault/utils/flatfile.js new file mode 100644 index 0000000..e9d6fee --- /dev/null +++ b/src/routers/vault/utils/flatfile.js @@ -0,0 +1,31 @@ +const execFile = require("child_process").execFile; +const ripgrep = require("ripgrep-bin"); + +const execAsync = (cmd, par) => + new Promise((resolve, reject) => + execFile(cmd, par, (error, stdout, stderr) => + resolve(error ? "" : (stdout ? stdout : stderr)))); + +const tmwa_account_regex = new RegExp("^(?<id>[0-9]+)\t(?<name>[^\t]+)\t(?<password>[^\t]+)\t"); + +const parseAccountLine = (line) => { + const { groups: account } = tmwa_account_regex.exec(line); + return { + id: +account.id, + name: account.name, + password: account.password, + }; +} + +const findAccount = async (account_id, name) => { + const regex = `^${account_id}\t${name}\t`; + const stdout = await execAsync(ripgrep, ["--case-sensitive", `--max-count=1`, regex, "account.txt"]); + let account = null; + if (stdout.length) + account = parseAccountLine(stdout.slice(0, -1).split("\n")[0]); + return account; +}; + +module.exports = { + findAccount, +}; diff --git a/src/routers/vault/utils/md5saltcrypt.js b/src/routers/vault/utils/md5saltcrypt.js new file mode 100644 index 0000000..55933f5 --- /dev/null +++ b/src/routers/vault/utils/md5saltcrypt.js @@ -0,0 +1,38 @@ +// password hashing for the Legacy server +// https://gitlab.com/evol/evol-hercules/blob/master/src/elogin/md5calc.c +// https://github.com/themanaworld/tmwa/blob/c82c9741bc1a0b110bccce1bcc76903a6e747a00/src/high/md5more.cpp + +const crypto = require("crypto"); // native + +// generate md5 from string +const md5 = (str) => crypto.createHash("md5").update(str).digest("hex"); + +// weak md5 password hashing and salting (eAthena) +const md5saltcrypt = (salt, plain) => md5(md5(plain) + md5(salt)).slice(0, -8); + +// check plain password against its salted hash +const verify = (salt, hashed, plain) => md5saltcrypt(salt, plain) === hashed; + +// takes apart a password string (!salt$hash) and verifies it +const verify_ea = (raw, plain) => verify(raw.slice(1, 6), raw.slice(-24), plain); + +// generate a new salt +const new_salt = () => { + let salt = ""; + do { + salt += String.fromCharCode(Math.floor(78 * Math.random() + 48)); + } while (salt.length < 5); + return salt; +}; + +// generate a password string with the given salt +const hash = (salt, plain) => `!${salt}$${md5saltcrypt(salt, plain)}`; + +// generate a password string with a new salt +const hash_new = (plain) => hash(new_salt(), plain); + + +module.exports = { + verify: verify_ea, + hash: hash_new, +}; |