diff --git a/docs/api.md b/docs/api.md index dca8e56983c4d8e340746ab215d5fa2409ef4521..f6b2bff6fcac1365846e4d787081746d7a7921d9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -171,6 +171,37 @@ If the key isn't charged enough for the amount of tokens or if the date supplied } ``` +## `POST /api/json/token/check` + +Checks supplied tokens for validity +You cannot submit more than 10 tokens to be used. + +### Parameters + +```json +{ + "tokens": [ + { + "token": <TOKEN>, + "signature": <SIGNATURE>, + "date": <DATE_AS_SUPPLIED_IN_TOKEN_PUBKEY>, + } + ... + ] +} +``` + +### Example Response + +If all tokens were used successfully response code will be `201`. +If any validation errors (signature, token format, etc.) occured response code will be `422` + +```json +{ + TODO +} +``` + ## `POST /api/json/token/use` Uses supplied Tokens to be consumed for MetaGer search. All supplied Tokens will be invalid after this action. diff --git a/pass/routes/api.js b/pass/routes/api.js index 2f5f1dca487d635722fde72d6a0fdf9901ccdf19..469599bfaf488654a6ce42b528558504f2b64efe 100644 --- a/pass/routes/api.js +++ b/pass/routes/api.js @@ -195,6 +195,11 @@ router.post( .custom((value) => { let checked_tokens = {}; for (let i = 0; i < value.length; i++) { + if (!(typeof value[i] == "string") || value[i].legnth > 1024) { + return Promise.reject( + "The supplied tokens can't be more than 1024 characters long" + ); + } if (value[i] in checked_tokens) { return Promise.reject("The supplied tokens need to be unique"); } else { @@ -219,27 +224,24 @@ router.post( let key = await Key.GET_KEY(req.body.key, true); if (key.get_charge() < blinded_tokens.length) { + await key.save(); res.status(422).json({ message: "Invalid Key", }); + return; } else { key.discharge_key(blinded_tokens.length); await key.save(); } - // Make signing requests always the same duration to prevent timing attacks on the private key - let duration_millis = 1000; - let start_time = dayjs(); - let crypto = new Crypto(); await crypto .get_private_key(date) - .then((private_key) => { + .then(async (private_key) => { for (let i = 0; i < blinded_tokens.length; i++) { let blinded_token = blinded_tokens[i]; - signed_tokens[blinded_token] = crypto - .sign(blinded_token, private_key) - .toString(); + let signature = await crypto.sign(blinded_token, private_key); + signed_tokens[blinded_token] = signature.toString(); } }) .catch((reason) => { @@ -248,21 +250,40 @@ router.post( status: 500, message: "Couldn't load private key", }); + return; }); - let missing_millis = Math.max( - duration_millis - dayjs().diff(start_time, "millisecond"), - 0 - ); + res.status(201).json({ + key: key.get_key, + discharged: blinded_tokens.length, + date: date.format(config.get("crypto.private_key.date_format")), + signed_tokens: signed_tokens, + }); + } +); - setTimeout(() => { - res.status(201).json({ - key: key.get_key, - discharged: blinded_tokens.length, - date: date.format(config.get("crypto.private_key.date_format")), - signed_tokens: signed_tokens, - }); - }, missing_millis); +router.post( + "/token/check", + authorizedOnly, + body("tokens") + .notEmpty() + .withMessage("Tokens need to be defined") + .bail() + .isArray({ min: 1, max: 10 }) + .withMessage("You can supply between 1 and 10 tokens") + .bail() + .custom((value) => validateTokenStructure(value)) + .bail() + .custom(async (value) => validateTokenSignature(value)), + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + res.status(422).json(errors); + return; + } + res.json({ + status: "OK", + }); } ); @@ -276,85 +297,16 @@ router.post( .isArray({ min: 1, max: 10 }) .withMessage("You can supply between 1 and 10 tokens") .bail() - .custom((value) => { - // Validate Tokens are each unique and in expected format - let checked_tokens = {}; - for (let i = 0; i < value.length; i++) { - let token = value[i]; - if ( - !("token" in token) || - !("signature" in token) || - !("date" in token) - ) { - return Promise.reject("Token structure is invalid"); - } - if ( - typeof token.token !== "string" || - typeof token.signature !== "string" || - typeof token.date !== "string" - ) { - return Promise.reject("Token structure is invalid"); - } - if ( - !token.token.match( - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/ - ) - ) { - return Promise.reject("Token format is invalid"); - } - - // Validate supplied expiration date - let date = dayjs( - token.date, - config.get("crypto.private_key.date_format") - ); - let min = dayjs() - .millisecond(0) - .second(0) - .minute(0) - .hour(0) - .date(1) - .subtract(1, "month"); - let max = min.add(2, "month"); - if (!date.isValid() || date.isBefore(min) || !date.isBefore(max)) { - return Promise.reject("Submitted Expiration format is invalid"); - } - - if (!token.signature.match(/^\d+$/)) { - return Promise.reject("Signature invalid"); - } - - if (token.token in checked_tokens) { - return Promise.reject("The supplied tokens need to be unique"); - } else { - checked_tokens[token.token] = true; - } - } - return true; - }) + .custom((value) => validateTokenStructure(value)) .bail() - .custom(async (value) => { - // Now that we checked Format of tokens: Check the signature - let crypto = new Crypto(); - for (let i = 0; i < value.length; i++) { - let token = value[i]; - let verification_result = await crypto.validateToken( - token.token, - token.date - ); - if (!verification_result) { - return Promise.reject("Invalid Signatures"); - } - } - return true; - }), + .custom(async (value) => validateTokenSignature(value)), async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { res.status(422).json(errors); return; } - res.json({ + res.status(201).json({ status: "OK", }); } @@ -387,4 +339,96 @@ function authorizedOnly(req, res, next) { } } +async function validateTokenStructure(value) { + // Validate Tokens are each unique and in expected format + let checked_tokens = {}; + let error = false; + for (let i = 0; i < value.length; i++) { + let token = value[i]; + if (!("token" in token || typeof token.token != "string")) { + error = true; + value[i]["status"] = "field_missing_token"; + continue; + } + if (!("signature" in token || typeof token.signature != "string")) { + error = true; + value[i]["status"] = "field_missing_signature"; + continue; + } + if (!("date" in token || typeof token.date != "string")) { + error = true; + value[i]["status"] = "field_missing_date"; + continue; + } + + if ( + !token.token.match( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/ + ) + ) { + error = true; + value[i]["status"] = "format_invalid_token"; + continue; + } + + // Validate supplied expiration date + let date = dayjs(token.date, config.get("crypto.private_key.date_format")); + let min = dayjs() + .millisecond(0) + .second(0) + .minute(0) + .hour(0) + .date(1) + .subtract(1, "month"); + let max = min.add(2, "month"); + if (!date.isValid() || date.isBefore(min) || !date.isBefore(max)) { + error = true; + value[i]["status"] = "format_invalid_date"; + continue; + } + + if (!token.signature.match(/^\d+$/) || token.signature.length > 1024) { + error = true; + value[i]["status"] = "format_invalid_signature"; + continue; + } + + if (token.token in checked_tokens) { + error = true; + value[i]["status"] = "token_not_unique"; + continue; + } else { + checked_tokens[token.token] = true; + } + } + if (error) { + return Promise.reject("Token structure is invalid"); + } + return true; +} + +async function validateTokenSignature(value) { + // Now that we checked Format of tokens: Check the signature + let crypto = new Crypto(); + let error = false; + for (let i = 0; i < value.length; i++) { + let token = value[i]; + let verification_result = await crypto.validateToken( + token.token, + token.signature, + token.date + ); + if (!verification_result) { + value[i]["status"] = "invalid_signature"; + error = true; + } else { + value[i]["status"] = "ok"; + } + } + if (error) { + return Promise.reject("Invalid Signatures"); + } + return true; +} + module.exports = router;