Skip to content
Snippets Groups Projects
Commit e333f3d0 authored by Dominik Hebeler's avatar Dominik Hebeler
Browse files

Merge branch 'main' of gitlab.metager.de:open-source/metager-keymanager

parents 169c06aa 5c630874
No related branches found
No related tags found
No related merge requests found
Pipeline #7292 passed
...@@ -139,6 +139,7 @@ This Method does not require authentication ...@@ -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. 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. The Key will be discharged by this action.
You cannot submit more than 10 tokens to be signed. You cannot submit more than 10 tokens to be signed.
This Method does not require authentication
### Parameters ### Parameters
...@@ -169,3 +170,65 @@ If the key isn't charged enough for the amount of tokens or if the date supplied ...@@ -169,3 +170,65 @@ 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.
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
}
```
...@@ -10,7 +10,6 @@ const RedisClient = require("./RedisClient"); ...@@ -10,7 +10,6 @@ const RedisClient = require("./RedisClient");
class Crypto { class Crypto {
#dayjs; #dayjs;
#dayjs_format; #dayjs_format;
#redis;
constructor() { constructor() {
this.#dayjs = require("dayjs"); this.#dayjs = require("dayjs");
...@@ -23,7 +22,7 @@ class Crypto { ...@@ -23,7 +22,7 @@ class Crypto {
* of the given date * of the given date
* *
* @param {dayjs.Dayjs} date Date/Month for the private Key seed * @param {dayjs.Dayjs} date Date/Month for the private Key seed
* @returns {NodeRSA} * @returns {Promise<NodeRSA>}
*/ */
async get_private_key(date) { async get_private_key(date) {
let cache_key = "private_key:" + date.format(this.#dayjs_format); let cache_key = "private_key:" + date.format(this.#dayjs_format);
...@@ -71,16 +70,41 @@ class Crypto { ...@@ -71,16 +70,41 @@ class Crypto {
} }
/** /**
* *
* @param {String} blinded_token * @param {String} blinded_token
* @param {NodeRSA} private_key * @param {NodeRSA} private_key
* @returns {BigInteger} * @returns {Promise<BigInteger>}
*/ */
sign(blinded_token, private_key) { async sign(blinded_token, private_key) {
return BlindSignature.sign({ let min_ms = 150;
let start = dayjs();
let blinded_signature = BlindSignature.sign({
blinded: new BigInteger(blinded_token), blinded: new BigInteger(blinded_token),
key: private_key key: private_key,
}); });
let missing_ms = Math.max(min_ms - dayjs().diff(start, "millisecond"), 0);
await new Promise((resolve) => setTimeout(resolve, missing_ms));
return blinded_signature;
}
/**
*
* @param {String} token
* @param {String} date
*/
async validateToken(token, signature, date) {
let min_ms = 150;
let start = dayjs();
let current_date = dayjs(date, this.#dayjs_format);
let private_key = await this.get_private_key(current_date);
let verification_result = BlindSignature.verify2({
unblinded: signature,
key: private_key,
message: token,
});
let missing_ms = Math.max(min_ms - dayjs().diff(start, "millisecond"), 0);
await new Promise((resolve) => setTimeout(resolve, missing_ms));
return verification_result;
} }
async validateMetaGerPassCode( async validateMetaGerPassCode(
...@@ -129,8 +153,6 @@ class Crypto { ...@@ -129,8 +153,6 @@ class Crypto {
} }
} }
/** /**
* Creates an hmac hash for purchase data so we can check it later * Creates an hmac hash for purchase data so we can check it later
*/ */
......
var express = require("express"); var express = require("express");
var router = express.Router(); var router = express.Router();
const { const { body, validationResult } = require("express-validator");
body,
validationResult
} = require("express-validator");
const config = require("config"); const config = require("config");
const Key = require("../app/Key"); const Key = require("../app/Key");
...@@ -13,26 +10,9 @@ const Manual = require("../app/payment_processor/Manual"); ...@@ -13,26 +10,9 @@ const Manual = require("../app/payment_processor/Manual");
const Crypto = require("../app/Crypto"); const Crypto = require("../app/Crypto");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const NodeRSA = require("node-rsa"); const NodeRSA = require("node-rsa");
const RedisClient = require("../app/RedisClient");
router.use("/key", (req, res, next) => { router.use("/key", authorizedOnly);
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.post("/key/create", async (req, res) => { router.post("/key/create", async (req, res) => {
let amount = req.body.amount; let amount = req.body.amount;
...@@ -187,21 +167,40 @@ router.get("/token/pubkey", async (req, res) => { ...@@ -187,21 +167,40 @@ router.get("/token/pubkey", async (req, res) => {
}); });
}); });
router.post("/token/sign", body("key").notEmpty(), body("date").notEmpty().custom(value => { router.post(
// Make sure the date is either for the last month or this month "/token/sign",
let date = dayjs(value, config.get("crypto.private_key.date_format")); body("key").notEmpty(),
let min = dayjs().millisecond(0).second(0).minute(0).hour(0).date(1).subtract(1, "month"); body("date")
let max = min.add(2, "month"); .notEmpty()
if (!date.isValid() || date.isBefore(min) || !date.isBefore(max)) { .custom((value) => {
return Promise.reject("Submitted Date format is invalid"); // Make sure the date is either for the last month or this month
} let date = dayjs(value, config.get("crypto.private_key.date_format"));
return true; let min = dayjs()
}), .millisecond(0)
body("blinded_tokens").notEmpty().withMessage("Blinded Tokens need to be defined") .second(0)
.isArray({ min: 1, max: 10 }).withMessage("You can supply between 1 and 10 tokens to sign") .minute(0)
.custom(value => { .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 Date format is invalid");
}
return true;
}),
body("blinded_tokens")
.notEmpty()
.withMessage("Blinded Tokens need to be defined")
.isArray({ min: 1, max: 10 })
.withMessage("You can supply between 1 and 10 tokens to sign")
.custom((value) => {
let checked_tokens = {}; let checked_tokens = {};
for (let i = 0; i < value.length; i++) { 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) { if (value[i] in checked_tokens) {
return Promise.reject("The supplied tokens need to be unique"); return Promise.reject("The supplied tokens need to be unique");
} else { } else {
...@@ -209,41 +208,111 @@ router.post("/token/sign", body("key").notEmpty(), body("date").notEmpty().custo ...@@ -209,41 +208,111 @@ router.post("/token/sign", body("key").notEmpty(), body("date").notEmpty().custo
} }
} }
return true; return true;
}) }),
, async (req, res) => { async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
res.status(422).json(errors); res.status(422).json(errors);
return; return;
} }
let key = await Key.GET_KEY(req.body.key, false);
let date = dayjs(req.body.date, config.get("crypto.private_key.date_format")); let date = dayjs(
req.body.date,
config.get("crypto.private_key.date_format")
);
let blinded_tokens = req.body.blinded_tokens; let blinded_tokens = req.body.blinded_tokens;
let signed_tokens = {}; let signed_tokens = {};
let crypto = new Crypto(); let key = await Key.GET_KEY(req.body.key, true);
crypto.get_private_key(date).then(private_key => { if (key.get_charge() < blinded_tokens.length) {
for (let i = 0; i < blinded_tokens.length; i++) { await key.save();
let blinded_token = blinded_tokens[i]; res.status(422).json({
signed_tokens[blinded_token] = crypto.sign(blinded_token, private_key).toString(); message: "Invalid Key",
}
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
}); });
}).catch(reason => { return;
console.error(reason); } else {
res.status(500).json({ key.discharge_key(blinded_tokens.length);
status: 500, await key.save();
message: "Couldn't load private key" }
let crypto = new Crypto();
await crypto
.get_private_key(date)
.then(async (private_key) => {
for (let i = 0; i < blinded_tokens.length; i++) {
let blinded_token = blinded_tokens[i];
let signature = await crypto.sign(blinded_token, private_key);
signed_tokens[blinded_token] = signature.toString();
}
})
.catch((reason) => {
console.error(reason);
res.status(500).json({
status: 500,
message: "Couldn't load private key",
});
return;
}); });
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,
}); });
}
);
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, false)),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(422).json(errors);
return;
}
res.json({
status: "OK",
});
}
);
}); 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) => validateTokenStructure(value))
.bail()
.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",
});
}
);
router.use((req, res) => { router.use((req, res) => {
res.status(404).json({ res.status(404).json({
...@@ -252,4 +321,173 @@ router.use((req, res) => { ...@@ -252,4 +321,173 @@ 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();
}
}
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 (
!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;
}
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, 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,
token.date
);
if (!verification_result) {
value[i]["status"] = "invalid_signature";
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;
}
module.exports = router; module.exports = router;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment