Skip to content
Snippets Groups Projects
Crypto.js 6.43 KiB
const config = require("config");
const { pki, random, md } = require("node-forge");
const crypto = require("crypto");
const BlindSignature = require("blind-signatures");
var BigInteger = require("jsbn").BigInteger;
const NodeRSA = require("node-rsa");
const dayjs = require("dayjs");
const RedisClient = require("./RedisClient");

class Crypto {
  #dayjs;
  #dayjs_format;

  constructor() {
    this.#dayjs = require("dayjs");
    this.#dayjs_format = config.get("crypto.private_key.date_format");
  }

  /**
   * Retrieves the Private Key for the given date
   * Private Keys are generated from seeds using the Year and the month
   * of the given date
   *
   * @param {dayjs.Dayjs} date Date/Month for the private Key seed
   * @returns {Promise<NodeRSA>}
   */
  async get_private_key(date) {
    let cache_key = "private_key:" + date.format(this.#dayjs_format);
    // Check if the private key already exists in cache
    let redis_client = RedisClient.CLIENT();
    let cached_key = await redis_client.get(cache_key);

    let private_key = new NodeRSA();
    if (cached_key === null) {
      let seed =
        config.get("crypto.private_key.seed") + date.format(this.#dayjs_format);
      let sha512 = md.sha512.create();
      sha512.update(seed);
      seed = sha512.digest().toHex();
      // Feed the seed into a pseudorandom number generator
      let prng_instance = random.createInstance();
      prng_instance.seedFileSync = () => seed;

      let keypair = pki.rsa.generateKeyPair({
        bits: config.get("crypto.private_key.bit_length"),
        prng: prng_instance,
        workers: -1,
      });
      let private_key_pem = pki
        .privateKeyToPem(keypair.privateKey)
        .trim()
        .replace(/\r\n/g, "\n");

      private_key = private_key.importKey(
        Buffer.from(private_key_pem, "utf8"),
        "private"
      );

      // Store the key in cache
      await redis_client
        .pipeline()
        .set(cache_key, private_key.exportKey("pkcs8"))
        .expireat(cache_key, date.add(2, "month").unix())
        .exec();
    } else {
      let private_key_data = await redis_client.get(cache_key);
      private_key = private_key.importKey(private_key_data, "pkcs8");
    }
    return private_key;
  }

  /**
   *
   * @param {String} blinded_token
   * @param {NodeRSA} private_key
   * @returns {Promise<BigInteger>}
   */
  async sign(blinded_token, private_key) {
    let min_ms = 150;
    let start = dayjs();
    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 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(
    generation_month,
    expiration_month,
    metager_pass_codes
  ) {
    // Check if codes are expired
    if (!this.#dayjs().isBefore(expiration_month, "month")) {
      return Promise.reject("Redeem Codes are expired.");
    }

    let private_key = await this.private_key_get(
      generation_month,
      expiration_month
    );

    let uuid_regexExp =
      /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi;
    for (let i = 0; i < metager_pass_codes.length; i++) {
      // Check if code iss in correct format
      let metager_pass_code = metager_pass_codes[i];
      if (
        !metager_pass_code.hasOwnProperty("code") ||
        !metager_pass_code.code.match(uuid_regexExp)
      ) {
        return Promise.reject(metager_pass_code.code + " is not a valid UUID");
      }
      // Check if signature is in correct format
      if (
        !metager_pass_code.hasOwnProperty("signature") ||
        !metager_pass_code.signature.match(/^\d+$/gi)
      ) {
        return Promise.reject(i + 1 + ". signature has not a valid format");
      }

      let verification_result = BlindSignature.verify2({
        unblinded: metager_pass_code.signature,
        key: private_key,
        message: metager_pass_code.code,
      });
      if (!verification_result) {
        console.log("Failed" + i);
        return Promise.reject("One or more signatures could not be verified");
      }
    }
  }

  /**
   * Creates an hmac hash for purchase data so we can check it later
   */
  createIntegrityHash(
    order_id,
    expires_at,
    amount,
    unit_size,
    price_per_unit,
    public_key_n,
    public_key_e
  ) {
    let data_to_hash = JSON.stringify({
      order_id: parseInt(order_id),
      expires_at: new String(expires_at),
      amount: parseInt(amount),
      unit_size: parseInt(unit_size),
      price_per_unit: parseFloat(price_per_unit),
      public_key_n: new String(public_key_n),
      public_key_e: new String(public_key_e),
    });
    let forge = require("node-forge");
    let hmac = forge.hmac.create();
    hmac.start("sha256", config.get("crypto.hmac_integrity_seed"));
    hmac.update(data_to_hash);
    return hmac.digest().toHex();
  }

  /**
   * Validates an hmac hash for purchase data so we can check it later
   */
  validateIntegrityHash(
    user_hash,
    order_id,
    expires_at,
    amount,
    unit_size,
    price_per_unit,
    public_key_n,
    public_key_e
  ) {
    let data_to_hash = JSON.stringify({
      order_id: parseInt(order_id),
      expires_at: new String(expires_at),
      amount: parseInt(amount),
      unit_size: parseInt(unit_size),
      price_per_unit: parseFloat(price_per_unit),
      public_key_n: new String(public_key_n),
      public_key_e: new String(public_key_e),
    });
    let forge = require("node-forge");
    let hmac = forge.hmac.create();
    hmac.start("sha256", config.get("crypto.hmac_integrity_seed"));
    hmac.update(data_to_hash);
    let server_hash = hmac.digest().toHex();

    return crypto.timingSafeEqual(
      Buffer.from(server_hash, "utf8"),
      Buffer.from(user_hash, "utf8")
    );
  }
}

module.exports = Crypto;