diff --git a/docs/api.md b/docs/api.md index 4376832f0f9f11080957f8b381f93f7d24540b05..dca8e56983c4d8e340746ab215d5fa2409ef4521 100644 --- a/docs/api.md +++ b/docs/api.md @@ -139,6 +139,7 @@ This Method does not require authentication Signs submitted blinded tokens with the servers private key. The submitted key needs to hold a charge for each token or the request will fail. The Key will be discharged by this action. You cannot submit more than 10 tokens to be signed. +This Method does not require authentication ### Parameters @@ -169,3 +170,34 @@ If the key isn't charged enough for the amount of tokens or if the date supplied } } ``` + +## `POST /api/json/token/use` + +Uses supplied Tokens to be consumed for MetaGer search. All supplied Tokens will be invalid after this action. +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 +} +``` diff --git a/pass/app/Crypto.js b/pass/app/Crypto.js index cfb719a96c2acc1d68baa200c700a347c3516561..5985c775908b9d43c9e09d506cba5fc3c68c3077 100644 --- a/pass/app/Crypto.js +++ b/pass/app/Crypto.js @@ -10,7 +10,6 @@ const RedisClient = require("./RedisClient"); class Crypto { #dayjs; #dayjs_format; - #redis; constructor() { this.#dayjs = require("dayjs"); @@ -23,7 +22,7 @@ class Crypto { * of the given date * * @param {dayjs.Dayjs} date Date/Month for the private Key seed - * @returns {NodeRSA} + * @returns {Promise<NodeRSA>} */ async get_private_key(date) { let cache_key = "private_key:" + date.format(this.#dayjs_format); @@ -71,15 +70,30 @@ class Crypto { } /** - * - * @param {String} blinded_token - * @param {NodeRSA} private_key + * + * @param {String} blinded_token + * @param {NodeRSA} private_key * @returns {BigInteger} */ sign(blinded_token, private_key) { return BlindSignature.sign({ blinded: new BigInteger(blinded_token), - key: private_key + key: private_key, + }); + } + + /** + * + * @param {String} token + * @param {String} expiration + */ + async validateToken(token, signature, expiration) { + let date = dayjs(expiration, this.#dayjs_format); + let private_key = await this.get_private_key(date); + return BlindSignature.verify2({ + unblinded: signature, + key: private_key, + message: token, }); } @@ -129,8 +143,6 @@ class Crypto { } } - - /** * Creates an hmac hash for purchase data so we can check it later */ diff --git a/pass/routes/api.js b/pass/routes/api.js index 96c781e01a257fc402694fc49614c4c87bb96797..2f5f1dca487d635722fde72d6a0fdf9901ccdf19 100644 --- a/pass/routes/api.js +++ b/pass/routes/api.js @@ -11,25 +11,7 @@ const Crypto = require("../app/Crypto"); const dayjs = require("dayjs"); const NodeRSA = require("node-rsa"); -router.use("/key", (req, res, next) => { - let auth_token = req.get("Authorization"); - let authorized = false; - if (auth_token) { - auth_token = auth_token.replace(/^Bearer /, ""); - let required_token = config.get("app.api_token"); - if (required_token === auth_token) { - authorized = true; - } - } - if (!authorized) { - res.status(401).json({ - code: 401, - error: "You are not authorized for API usage", - }); - } else { - next(); - } -}); +router.use("/key", authorizedOnly); router.post("/key/create", async (req, res) => { let amount = req.body.amount; @@ -284,6 +266,100 @@ router.post( } ); +router.post( + "/token/use", + 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) => { + // 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; + }) + .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; + }), + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + res.status(422).json(errors); + return; + } + res.json({ + status: "OK", + }); + } +); + router.use((req, res) => { res.status(404).json({ code: 404, @@ -291,4 +367,24 @@ router.use((req, res) => { }); }); +function authorizedOnly(req, res, next) { + let auth_token = req.get("Authorization"); + let authorized = false; + if (auth_token) { + auth_token = auth_token.replace(/^Bearer /, ""); + let required_token = config.get("app.api_token"); + if (required_token === auth_token) { + authorized = true; + } + } + if (!authorized) { + res.status(401).json({ + code: 401, + error: "You are not authorized for API usage", + }); + } else { + next(); + } +} + module.exports = router;