crypto/rsa.js

'use strict'

/**
 * @module types
 */

const crypto = require('crypto')
const constants = crypto.constants
const Pss = require('../crypto/pss')
const pem = require('../util/pem')

/**
 * RSA-PSS using Node crypto module.
 *
 * This class combines Node's native crypto functionality with PSS padding
 * implemented in this library.
 */
class Rsa {
  constructor (opts) {
    opts = opts || {}

    this.hashAlgorithm = opts.hashAlgorithm || 'sha256'

    this.pss = new Pss({
      hashAlgorithm: this.hashAlgorithm
    })
  }

  /**
   * Get the length in bits of an RSA modulus.
   *
   * @param {Buffer} modulus RSA modulus.
   * @return {Number} Number of bits in RSA modulus.
   */
  getModulusBitLength (modulus) {
    const modulusHighByteBitLength = modulus[0].toString(2).length
    const modulusBitLength = (modulus.length - 1) * 8 + modulusHighByteBitLength

    return modulusBitLength
  }

  /**
   * Sign a message using RSA-PSS.
   *
   * @param {String} privateKey PEM-encoded RSA private key.
   * @param {Buffer} message Message to sign.
   * @return {Buffer} RSA signature.
   */
  sign (privateKey, message) {
    // Calculate modulus bit length
    const modulus = pem.modulusFromPrivateKey(privateKey)
    const modulusBitLength = this.getModulusBitLength(modulus)

    // Pad message using PSS
    const encodedMessage = this.pss.encode(message, modulusBitLength - 1)

    // OpenSSL expects the message buffer to be the same length (in bytes) as
    // the modulus.
    const paddedMessage = (encodedMessage.length < modulus.length)
      ? Buffer.concat([Rsa.ZERO_BYTE, encodedMessage])
      : encodedMessage

    // Sign
    return crypto.privateEncrypt(
      {
        key: privateKey,
        padding: constants.RSA_NO_PADDING
      },
      paddedMessage
    )
  }

  /**
   * Verify a RSA-PSS signature.
   *
   * @param {Buffer} modulus RSA public modulus.
   * @param {Buffer} message Message the signature should correspond to.
   * @param {Buffer} signature RSA signature.
   * @return {Boolean} Whether the signature is valid or not.
   */
  verify (modulus, message, signature) {
    // Verify signature
    const publicKey = pem.modulusToPem(modulus)
    const paddedMessage = crypto.publicDecrypt(
      {
        key: publicKey,
        padding: constants.RSA_NO_PADDING
      },
      signature
    )

    // OpenSSL returns a buffer that fits the bitlength of the modulus, but we
    // need this buffer to be just long enough to fit the bitlength of the
    // encodedMessage, which is one bit shorter.
    const modulusBitLength = this.getModulusBitLength(modulus)
    const encodedMessage = modulusBitLength % 8 === 1
      ? paddedMessage.slice(1)
      : paddedMessage

    // Verify message padding
    return this.pss.verify(message, encodedMessage, modulusBitLength - 1)
  }
}

// Used to add a zero for padding
Rsa.ZERO_BYTE = Buffer.from([0])

module.exports = Rsa