text: 'use strict'
  
  const async = require('async')
  const forge = require('node-forge')
  const util = require('./util')
  
  /**
   * Cryptographic Message Syntax (aka PKCS #7)
   *
   * CMS describes an encapsulation syntax for data protection. It
   * is used to digitally sign, digest, authenticate, or encrypt
   * arbitrary message content.
   *
   * See RFC 5652 for all the details.
   */
  class CMS {
    /**
     * Creates a new instance with a keychain
     *
     * @param {Keychain} keychain - the available keys
     */
    constructor (keychain) {
      if (!keychain) {
        throw new Error('keychain is required')
      }
  
      this.keychain = keychain
    }
  
    /**
     * Creates some protected data.
     *
     * The output Buffer contains the PKCS #7 message in DER.
     *
     * @param {string} name - The local key name.
     * @param {Buffer} plain - The data to encrypt.
     * @param {function(Error, Buffer)} callback
     * @returns {undefined}
     */
    encrypt (name, plain, callback) {
      const self = this
      const done = (err, result) => async.setImmediate(() => callback(err, result))
  
      if (!Buffer.isBuffer(plain)) {
        return done(new Error('Plain data must be a Buffer'))
      }
  
      async.series([
        (cb) => self.keychain.findKeyByName(name, cb),
        (cb) => self.keychain._getPrivateKey(name, cb)
      ], (err, results) => {
        if (err) return done(err)
  
        let key = results[0]
        let pem = results[1]
        try {
          const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._())
          util.certificateForKey(key, privateKey, (err, certificate) => {
            if (err) return callback(err)
  
            // create a p7 enveloped message
            const p7 = forge.pkcs7.createEnvelopedData()
            p7.addRecipient(certificate)
            p7.content = forge.util.createBuffer(plain)
            p7.encrypt()
  
            // convert message to DER
            const der = forge.asn1.toDer(p7.toAsn1()).getBytes()
            done(null, Buffer.from(der, 'binary'))
          })
        } catch (err) {
          done(err)
        }
      })
    }
  
    /**
     * Reads some protected data.
     *
     * The keychain must contain one of the keys used to encrypt the data.  If none of the keys
     * exists, an Error is returned with the property 'missingKeys'.  It is array of key ids.
     *
     * @param {Buffer} cmsData - The CMS encrypted data to decrypt.
     * @param {function(Error, Buffer)} callback
     * @returns {undefined}
     */
    decrypt (cmsData, callback) {
      const done = (err, result) => async.setImmediate(() => callback(err, result))
  
      if (!Buffer.isBuffer(cmsData)) {
        return done(new Error('CMS data is required'))
      }
  
      const self = this
      let cms
      try {
        const buf = forge.util.createBuffer(cmsData.toString('binary'))
        const obj = forge.asn1.fromDer(buf)
        cms = forge.pkcs7.messageFromAsn1(obj)
      } catch (err) {
        return done(new Error('Invalid CMS: ' + err.message))
      }
  
      // Find a recipient whose key we hold. We only deal with recipient certs
      // issued by ipfs (O=ipfs).
      const recipients = cms.recipients
        .filter(r => r.issuer.find(a => a.shortName === 'O' && a.value === 'ipfs'))
        .filter(r => r.issuer.find(a => a.shortName === 'CN'))
        .map(r => {
          return {
            recipient: r,
            keyId: r.issuer.find(a => a.shortName === 'CN').value
          }
        })
      async.detect(
        recipients,
        (r, cb) => self.keychain.findKeyById(r.keyId, (err, info) => cb(null, !err && info)),
        (err, r) => {
          if (err) return done(err)
          if (!r) {
            const missingKeys = recipients.map(r => r.keyId)
            err = new Error('Decryption needs one of the key(s): ' + missingKeys.join(', '))
            err.missingKeys = missingKeys
            return done(err)
          }
  
          async.waterfall([
            (cb) => self.keychain.findKeyById(r.keyId, cb),
            (key, cb) => self.keychain._getPrivateKey(key.name, cb)
          ], (err, pem) => {
            if (err) return done(err)
  
            const privateKey = forge.pki.decryptRsaPrivateKey(pem, self.keychain._())
            cms.decrypt(r.recipient, privateKey)
            done(null, Buffer.from(cms.content.getBytes(), 'binary'))
          })
        }
      )
    }
  }
  
  module.exports = CMS
  