From ed49e29cfe6cbaa6a6c9af8125faf926121a7f69 Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@hebeler.club>
Date: Sat, 11 Mar 2023 22:13:15 +0100
Subject: [PATCH] added base for token use api method

---
 docs/api.md        |  32 +++++++++++
 pass/app/Crypto.js |  28 +++++++---
 pass/routes/api.js | 134 ++++++++++++++++++++++++++++++++++++++-------
 3 files changed, 167 insertions(+), 27 deletions(-)

diff --git a/docs/api.md b/docs/api.md
index 4376832..dca8e56 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 cfb719a..5985c77 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 96c781e..2f5f1dc 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;
-- 
GitLab