All files / platform/packages/url-signer/src MessageVerifier.js

94.87% Statements 37/39
96.15% Branches 25/26
100% Functions 5/5
94.87% Lines 37/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120  1x 1x 1x 1x                         3x         3x 3x                         27x 1x   26x 26x 26x                   29x 4x         25x 25x 3x         22x 22x     22x 22x             31x         31x 3x         28x 1x         27x 4x         23x 5x   18x       23x 13x   10x 10x 10x             1x  
'use strict';
const crypto = require('crypto');
const base64 = require('./Base64');
const helper = require('./Helper');
const Hmac = require('./Hmac');
 
/**
 * Message verifier is similar to the encryption. However, the actual payload
 * is not encrypted and just base64 encoded. This is helpful when you are
 * not concerned about the confidentiality of the data, but just want to
 * make sure that is not tampered after encoding.
 */
class MessageVerifier {
  /**
   * @param {string} secret
   */
  constructor(secret) {
    this.secret = secret;
    /**
     * The key for signing and encrypting values. It is derived
     * from the user provided secret.
     */
    this.cryptoKey = crypto.createHash('sha256').update(this.secret).digest();
    this.separator = '.';
  }
 
  /**
   * Signs a value with the secret key. The signed value is not encrypted, but just
   * signed for avoiding tampering to the original message.
   *
   * @param {*} value Any `JSON.stringify` valid value
   * @param {Date|null|undefined} expiresAt
   * @param {string} purpose
   * @returns
   */
  sign(value, expiresAt, purpose) {
    if (value === null || value === undefined) {
      throw new Error('"MessageVerifier.sign" cannot sign null or undefined values');
    }
    const encoded = base64.urlEncode(helper.safeStringify({ message: value, expiresAt, purpose }));
    const hash = new Hmac(this.cryptoKey).generate(encoded);
    return `${encoded}${this.separator}${hash}`;
  }
 
  /**
   * Unsign a previously signed value with an optional purpose
   * @param {string} value
   * @param {string} purpose
   * @returns
   */
  unsign(value, purpose) {
    if (typeof value !== 'string') {
      throw new Error('MessageVerifier.unsign expects a string value');
    }
    /**
     * Ensure value is in correct format
     */
    const [encoded, hash] = value.split(this.separator);
    if (!encoded || !hash) {
      return null;
    }
    /**
     * Ensure value can be decoded
     */
    const decoded = base64.urlDecode(encoded);
    Iif (!decoded) {
      return null;
    }
    const isValid = new Hmac(this.cryptoKey).compare(encoded, hash);
    return isValid ? this.verify(decoded, purpose) : null;
  }
 
  /**
   * Verifies the message for expiry and purpose
   */
  verify(message, purpose) {
    const parsed = helper.safeJsonParse(message);
    /**
     * Safe parse returns the value as it is when unable to JSON.parse it. However, in
     * our case if value was correctly parsed, it should never match the input
     */
    if (parsed === message) {
      return null;
    }
    /**
     * Missing ".message" property
     */
    if (!parsed.message) {
      return null;
    }
    /**
     * Ensure purposes are same.
     */
    if (parsed.purpose !== purpose) {
      return null;
    }
    /**
     * Ensure isn't expired
     */
    if (this.isExpired(parsed)) {
      return null;
    }
    return parsed.message;
  }
 
  isExpired(message) {
    if (!message.expiresAt) {
      return false;
    }
    try {
      const expiresAt = new Date(message.expiresAt);
      return isNaN(expiresAt.getTime()) || expiresAt < new Date();
    } catch (error) {
      return true;
    }
  }
}
 
module.exports = MessageVerifier;