diff --git a/pass/app/Crypto.js b/pass/app/Crypto.js index bff2b96e45871ba05d7bea622f006822549575c9..66161156303d072c1ab204751b13369eb7fd8956 100644 --- a/pass/app/Crypto.js +++ b/pass/app/Crypto.js @@ -78,13 +78,13 @@ class Crypto { async sign(blinded_token, private_key) { let min_ms = 150; let start = dayjs(); - let signature = BlindSignature.sign({ + let blinded_signature = BlindSignature.sign({ blinded: new BigInteger(blinded_token), key: private_key, }); let missing_ms = Math.max(min_ms - dayjs().diff(start, "millisecond"), 0); await new Promise((resolve) => setTimeout(resolve, missing_ms)); - return signature; + return blinded_signature; } /** diff --git a/pass/routes/api.js b/pass/routes/api.js index 469599bfaf488654a6ce42b528558504f2b64efe..3f70c67611498e71241f15b42e0eaad550ce51e9 100644 --- a/pass/routes/api.js +++ b/pass/routes/api.js @@ -10,6 +10,7 @@ const Manual = require("../app/payment_processor/Manual"); const Crypto = require("../app/Crypto"); const dayjs = require("dayjs"); const NodeRSA = require("node-rsa"); +const RedisClient = require("../app/RedisClient"); router.use("/key", authorizedOnly); @@ -274,7 +275,7 @@ router.post( .bail() .custom((value) => validateTokenStructure(value)) .bail() - .custom(async (value) => validateTokenSignature(value)), + .custom(async (value) => validateTokenSignature(value, false)), async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { @@ -299,13 +300,14 @@ router.post( .bail() .custom((value) => validateTokenStructure(value)) .bail() - .custom(async (value) => validateTokenSignature(value)), + .custom(async (value) => validateTokenSignature(value, true)), async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { res.status(422).json(errors); return; } + res.status(201).json({ status: "OK", }); @@ -381,7 +383,12 @@ async function validateTokenStructure(value) { .date(1) .subtract(1, "month"); let max = min.add(2, "month"); - if (!date.isValid() || date.isBefore(min) || !date.isBefore(max)) { + if ( + !token.date.match(/^\d{4}-(0[1-9]|1[1-2])$/) || + !date.isValid() || + date.isBefore(min) || + !date.isBefore(max) + ) { error = true; value[i]["status"] = "format_invalid_date"; continue; @@ -407,12 +414,25 @@ async function validateTokenStructure(value) { return true; } -async function validateTokenSignature(value) { +async function validateTokenSignature(value, mark_used = false) { // Now that we checked Format of tokens: Check the signature let crypto = new Crypto(); let error = false; + + let redis_client = RedisClient.CLIENT(); + let used_tokens = {}; + for (let i = 0; i < value.length; i++) { let token = value[i]; + + // Check if token was already used + let redis_hash_key = "tokens:used:" + token.date; + if (await redis_client.hexists(redis_hash_key, token.token)) { + error = true; + value[i]["status"] = "token_already_used"; + continue; + } + let verification_result = await crypto.validateToken( token.token, token.signature, @@ -423,10 +443,49 @@ async function validateTokenSignature(value) { error = true; } else { value[i]["status"] = "ok"; + // Tokens can be used. Store the used tokens + if (mark_used) { + let redis_hash_expiration = dayjs() + .add(2, "month") + .date(1) + .hour(0) + .minute(0) + .second(0) + .millisecond(0); + let usage_success = await redis_client + .pipeline() + .hsetnx( + redis_hash_key, + token.token, + dayjs().format("YYYY-MM-DD HH:mm:ss") + ) + .expireat(redis_hash_key, redis_hash_expiration.unix()) + .exec(); + if (usage_success === 0) { + error = true; + value[i]["status"] = "token_already_used"; + } else { + // Store used tokens in memory so we can rollback in case of later error + if (!(redis_hash_key in used_tokens)) { + used_tokens[redis_hash_key] = []; + } + used_tokens[redis_hash_key].push(token.token); + } + } } } + if (error) { + if (mark_used) { + // Rollback any used tokens + for (let redis_hash_key in used_tokens) { + await redis_client.hdel(redis_hash_key, used_tokens[redis_hash_key]); + } + } + await redis_client.quit(); return Promise.reject("Invalid Signatures"); + } else { + await redis_client.quit(); } return true; }