util/pem.js

'use strict'

/**
 * @module util
 */

const asn1 = require('asn1.js')

// Crypto-conditions always use the same RSA exponent, namely 65537
const RSA_EXPONENT = 65537

/**
 * ASN.1 schema for RSA public key.
 *
 * From RFC 3447, section A.1.1.
 *
 *    RSAPublicKey ::= SEQUENCE {
 *      modulus           INTEGER,  -- n
 *      publicExponent    INTEGER   -- e
 *    }
 *
 * @type {asn1.Entity}
 */
const RsaPublicKey = asn1.define('RsaPublicKey', function () {
  this.seq().obj(
    this.key('modulus').int(),
    this.key('publicExponent').int()
  )
})

/**
 * ASN.1 schema for RSA private key.
 *
 * From RFC 3447, section A.1.2.
 *
 *    RSAPrivateKey ::= SEQUENCE {
 *      version           Version,
 *      modulus           INTEGER,  -- n
 *      publicExponent    INTEGER,  -- e
 *      privateExponent   INTEGER,  -- d
 *      prime1            INTEGER,  -- p
 *      prime2            INTEGER,  -- q
 *      exponent1         INTEGER,  -- d mod (p-1)
 *      exponent2         INTEGER,  -- d mod (q-1)
 *      coefficient       INTEGER,  -- (inverse of q) mod p
 *      otherPrimeInfos   OtherPrimeInfos OPTIONAL
 *    }
 *
 *    Version ::= INTEGER { two-prime(0), multi(1) }
 *      (CONSTRAINED BY {
 *        -- version must be multi if otherPrimeInfos present --
 *      })
 *
 *    OtherPrimeInfos ::= SEQUENCE SIZE(1..MAX) OF OtherPrimeInfo
 *
 *    OtherPrimeInfo ::= SEQUENCE {
 *      prime             INTEGER,  -- ri
 *      exponent          INTEGER,  -- di
 *      coefficient       INTEGER   -- ti
 *    }
 *
 * @type {asn1.Entity}
 */
const RsaPrivateKey = asn1.define('RsaPrivateKey', function () {
  this.seq().obj(
    this.key('version').int(),
    this.key('modulus').int(),
    this.key('publicExponent').int(),
    this.key('privateExponent').int(),
    this.key('prime1').int(),
    this.key('prime2').int(),
    this.key('exponent1').int(),
    this.key('exponent2').int(),
    this.key('coefficient').int(),
    this.optional().key('otherPrimeInfos').seqof(this.obj(
      this.key('prime').int(),
      this.key('exponent').int(),
      this.key('coefficient').int()
    ))
  )
})

/**
 * Utilities for RSA-related DER/PEM encoding.
 */
class Pem {
  /**
   * Convert an RSA modulus to a PEM-encoded RSAPublicKey.
   *
   * Encodes the public using the RSAPublicKey format given in
   * RFC 3447, appendix C.
   *
   * This function assumes that the exponent is 65537.
   *
   * @param {Buffer} modulus RSA public modulus.
   * @return {String} PEM-encoded RSA public key.
   */
  static modulusToPem (modulus) {
    // We expect the modulus with no leading zeros
    if (modulus[0] === 0) {
      throw new Error('Modulus may not start with zero')
    }

    // If the high bit is set, we need to prefix a zero
    if (modulus[0] & 0x80) {
      modulus = Buffer.concat([Buffer.from([0]), modulus])
    }

    const derPublicKey = RsaPublicKey.encode({
      modulus,
      publicExponent: RSA_EXPONENT
    })

    return (
      '-----BEGIN RSA PUBLIC KEY-----\n' +
      derPublicKey.toString('base64').match(/.{1,64}/g).join('\n') + '\n' +
      '-----END RSA PUBLIC KEY-----\n'
    )
  }

  /**
   * Retrieve a modulus from a PEM-encoded private key.
   *
   * @param {String} privateKey PEM-encoded RSA private key.
   * @return {Buffer} modulus RSA public modulus.
   */
  static modulusFromPrivateKey (privateKey) {
    const pem = privateKey
      .replace('-----BEGIN RSA PRIVATE KEY-----', '')
      .replace('-----END RSA PRIVATE KEY-----', '')
      .replace(/\s+|\n\r|\n|\r$/gm, '')
    const buffer = Buffer.from(pem, 'base64')

    const decodedPrivateKey = RsaPrivateKey.decode(buffer)
    const modulus = decodedPrivateKey.modulus.toArrayLike(Buffer)
    return modulus
  }
}

module.exports = Pem