<commit_before>var crypto = require('crypto');
var async  = require('async');
var xmldom = require('xmldom');
var xpath  = require('xpath');
var utils  = require('./utils');
var pki = require('node-forge').pki;

function encryptKeyInfo(symmetricKey, options, callback) {
  if (!options)
    return callback(new Error('must provide options'));
  if (!options.rsa_pub)
    return callback(new Error('must provide options.rsa_pub with public key RSA'));
  if (!options.pem)
    return callback(new Error('must provide options.pem with certificate'));
  
  if (!options.keyEncryptionAlgorighm)
    return callback(new Error('encryption without encrypted key is not supported yet'));

  switch (options.keyEncryptionAlgorighm) {
    case 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p':
      
      //ursa:
      // var rsa_pub = ursa.createPublicKey(options.rsa_pub);
      // var encryptedKey = rsa_pub.encrypt(symmetricKey);
      // var base64EncodedEncryptedKey = encryptedKey: encryptedKey.toString('base64');

      var rsa_pub = pki.publicKeyFromPem(options.rsa_pub);
      var encrypted = rsa_pub.encrypt(symmetricKey.toString('binary'), 'RSA-OAEP'); 
      var base64EncodedEncryptedKey = new Buffer(encrypted, 'binary').toString('base64');
      
      var params = {
        encryptedKey:  base64EncodedEncryptedKey, 
        encryptionPublicCert: '<X509Data><X509Certificate>' + utils.pemToCert(options.pem.toString()) + '</X509Certificate></X509Data>', 
        keyEncryptionMethod: options.keyEncryptionAlgorighm
      };
      
      var result = utils.renderTemplate('keyinfo', params);

      return callback(null, result);        
    default:
      return callback(new Error('encryption key algorithm not supported'));
  }
}

function encrypt(content, options, callback) {
  if (!options)
    return callback(new Error('must provide options'));
  if (!content)
    return callback(new Error('must provide content to encrypt'));
  if (!options.rsa_pub)
    return callback(new Error('rsa_pub option is mandatory and you should provide a valid RSA public key'));
  if (!options.pem)
    return callback(new Error('pem option is mandatory and you should provide a valid x509 certificate encoded as PEM'));

  async.waterfall([
    function generate_symmetric_key(cb) {
      switch (options.encryptionAlgorithm) {
        case 'http://www.w3.org/2001/04/xmlenc#aes128-cbc':
          crypto.randomBytes(16, cb); // generate a symmetric random key 16 bytes length
          break;
        default:
          crypto.randomBytes(32, cb); // generate a symmetric random key 32 bytes length
      }
    },
    function encrypt_content(symmetricKey, cb) {
      switch (options.encryptionAlgorithm) {
        case 'http://www.w3.org/2001/04/xmlenc#aes128-cbc':
          encrypt_aes('aes-128-cbc', symmetricKey, content, options.input_encoding, function (err, encryptedContent) {
            if (err) return cb(err);
            cb(null, symmetricKey, encryptedContent);
          });
          break;
        case 'http://www.w3.org/2001/04/xmlenc#aes256-cbc':
          encrypt_aes('aes-256-cbc', symmetricKey, content, options.input_encoding, function (err, encryptedContent) {
            if (err) return cb(err);
            cb(null, symmetricKey, encryptedContent);
          });
          break;
        default:
          cb(new Error('encryption algorithm not supported'));
      }
    },
    function encrypt_key(symmetricKey, encryptedContent, cb) {
      encryptKeyInfo(symmetricKey, options, function(err, keyInfo) {
        if (err) return cb(err);

        var result = utils.renderTemplate('encrypted-key', {
          encryptedContent: encryptedContent.toString('base64'),
          keyInfo: keyInfo,
          contentEncryptionMethod: options.encryptionAlgorithm
        });

        cb(null, result);
      });  
    }
  ], callback);
}

function decrypt(xml, options, callback) {
  if (!options)
    return callback(new Error('must provide options'));
  if (!xml)
    return callback(new Error('must provide XML to encrypt'));
  if (!options.key)
    return callback(new Error('key option is mandatory and you should provide a valid RSA private key'));
  
  var decrypted;

  try {
    var doc = new xmldom.DOMParser().parseFromString(xml);

    var symmetricKey = decryptKeyInfo(doc, options);
    var encryptionMethod = xpath.select("/*[local-name(.)='EncryptedData']/*[local-name(.)='EncryptionMethod']", doc)[0];
    var encryptionAlgorithm = encryptionMethod.getAttribute('Algorithm');

    var decipher;
    var encryptedContent = xpath.select("/*[local-name(.)='EncryptedData']/*[local-name(.)='CipherData']/*[local-name(.)='CipherValue']", doc)[0];
    var encrypted = new Buffer(encryptedContent.textContent, 'base64');

    switch (encryptionAlgorithm) {
      case 'http://www.w3.org/2001/04/xmlenc#aes128-cbc':
        decipher = crypto.createDecipheriv('aes-128-cbc', symmetricKey, encrypted.slice(0, 16));
        if (typeof options.autopadding !== 'undefined') {
          decipher.setAutoPadding(options.autopadding);
        }
        decrypted = decipher.update(encrypted.slice(16), null, 'binary') + decipher.final();
        
        // HACK: padding is not working as expected, 
        // so this is a hack to remove characters which should not be there
        // since the decrypted content will be xml, we just remove chars after >
        if (decrypted.lastIndexOf('>') > 0) {
          decrypted = decrypted.substr(0, decrypted.lastIndexOf('>') + 1);
        }

        break;
      case 'http://www.w3.org/2001/04/xmlenc#aes256-cbc':
        decipher = crypto.createDecipheriv('aes-256-cbc', symmetricKey, encrypted.slice(0, 16)); 
        decrypted = decipher.update(encrypted.slice(16), null, 'binary') + decipher.final();

        break;
      default:
        return callback(new Error('encryption algorithm ' + encryptionAlgorithm + ' not supported'));
    }
  } catch (e) {
    return callback(e);
  }
  
  callback(null, decrypted);
}

function decryptKeyInfo(doc, options) {
  if (typeof doc === 'string') doc = new xmldom.DOMParser().parseFromString(doc);

  var keyInfo = xpath.select("//*[local-name(.)='KeyInfo' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc)[0];
  var keyEncryptionMethod = xpath.select("//*[local-name(.)='KeyInfo']/*[local-name(.)='EncryptedKey']/*[local-name(.)='EncryptionMethod']", doc)[0];
  var keyEncryptionAlgorighm = keyEncryptionMethod.getAttribute('Algorithm');

  switch (keyEncryptionAlgorighm) {
    case 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p':<commit_msg>Changed block encryption padding to follow the xmlenc spec.<commit_after>var crypto = require('crypto');
var async  = require('async');
var xmldom = require('xmldom');
var xpath  = require('xpath');
var utils  = require('./utils');
var pki = require('node-forge').pki;

function encryptKeyInfo(symmetricKey, options, callback) {
  if (!options)
    return callback(new Error('must provide options'));
  if (!options.rsa_pub)
    return callback(new Error('must provide options.rsa_pub with public key RSA'));
  if (!options.pem)
    return callback(new Error('must provide options.pem with certificate'));
  
  if (!options.keyEncryptionAlgorighm)
    return callback(new Error('encryption without encrypted key is not supported yet'));

  switch (options.keyEncryptionAlgorighm) {
    case 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p':
      
      //ursa:
      // var rsa_pub = ursa.createPublicKey(options.rsa_pub);
      // var encryptedKey = rsa_pub.encrypt(symmetricKey);
      // var base64EncodedEncryptedKey = encryptedKey: encryptedKey.toString('base64');

      var rsa_pub = pki.publicKeyFromPem(options.rsa_pub);
      var encrypted = rsa_pub.encrypt(symmetricKey.toString('binary'), 'RSA-OAEP'); 
      var base64EncodedEncryptedKey = new Buffer(encrypted, 'binary').toString('base64');
      
      var params = {
        encryptedKey:  base64EncodedEncryptedKey, 
        encryptionPublicCert: '<X509Data><X509Certificate>' + utils.pemToCert(options.pem.toString()) + '</X509Certificate></X509Data>', 
        keyEncryptionMethod: options.keyEncryptionAlgorighm
      };
      
      var result = utils.renderTemplate('keyinfo', params);

      return callback(null, result);        
    default:
      return callback(new Error('encryption key algorithm not supported'));
  }
}

function encrypt(content, options, callback) {
  if (!options)
    return callback(new Error('must provide options'));
  if (!content)
    return callback(new Error('must provide content to encrypt'));
  if (!options.rsa_pub)
    return callback(new Error('rsa_pub option is mandatory and you should provide a valid RSA public key'));
  if (!options.pem)
    return callback(new Error('pem option is mandatory and you should provide a valid x509 certificate encoded as PEM'));

  async.waterfall([
    function generate_symmetric_key(cb) {
      switch (options.encryptionAlgorithm) {
        case 'http://www.w3.org/2001/04/xmlenc#aes128-cbc':
          crypto.randomBytes(16, cb); // generate a symmetric random key 16 bytes length
          break;
        default:
          crypto.randomBytes(32, cb); // generate a symmetric random key 32 bytes length
      }
    },
    function encrypt_content(symmetricKey, cb) {
      switch (options.encryptionAlgorithm) {
        case 'http://www.w3.org/2001/04/xmlenc#aes128-cbc':
          encrypt_aes('aes-128-cbc', symmetricKey, content, options.input_encoding, function (err, encryptedContent) {
            if (err) return cb(err);
            cb(null, symmetricKey, encryptedContent);
          });
          break;
        case 'http://www.w3.org/2001/04/xmlenc#aes256-cbc':
          encrypt_aes('aes-256-cbc', symmetricKey, content, options.input_encoding, function (err, encryptedContent) {
            if (err) return cb(err);
            cb(null, symmetricKey, encryptedContent);
          });
          break;
        default:
          cb(new Error('encryption algorithm not supported'));
      }
    },
    function encrypt_key(symmetricKey, encryptedContent, cb) {
      encryptKeyInfo(symmetricKey, options, function(err, keyInfo) {
        if (err) return cb(err);

        var result = utils.renderTemplate('encrypted-key', {
          encryptedContent: encryptedContent.toString('base64'),
          keyInfo: keyInfo,
          contentEncryptionMethod: options.encryptionAlgorithm
        });

        cb(null, result);
      });  
    }
  ], callback);
}

function decrypt(xml, options, callback) {
  if (!options)
    return callback(new Error('must provide options'));
  if (!xml)
    return callback(new Error('must provide XML to encrypt'));
  if (!options.key)
    return callback(new Error('key option is mandatory and you should provide a valid RSA private key'));
  
  var decrypted;

  try {
    var doc = new xmldom.DOMParser().parseFromString(xml);

    var symmetricKey = decryptKeyInfo(doc, options);
    var encryptionMethod = xpath.select("/*[local-name(.)='EncryptedData']/*[local-name(.)='EncryptionMethod']", doc)[0];
    var encryptionAlgorithm = encryptionMethod.getAttribute('Algorithm');

    var decipher;
    var encryptedContent = xpath.select("/*[local-name(.)='EncryptedData']/*[local-name(.)='CipherData']/*[local-name(.)='CipherValue']", doc)[0];
    var encrypted = new Buffer(encryptedContent.textContent, 'base64');

    switch (encryptionAlgorithm) {
      case 'http://www.w3.org/2001/04/xmlenc#aes128-cbc':
        decipher = crypto.createDecipheriv('aes-128-cbc', symmetricKey, encrypted.slice(0, 16));

        decipher.setAutoPadding(false);
        decrypted = decipher.update(encrypted.slice(16), null, 'binary') + decipher.final();

        // Remove padding bytes equal to the value of the last byte of the returned data.
        decrypted = decrypted.substr(0, decrypted.length - decrypted.charCodeAt(decrypted.length - 1));

        break;
      case 'http://www.w3.org/2001/04/xmlenc#aes256-cbc':
        decipher = crypto.createDecipheriv('aes-256-cbc', symmetricKey, encrypted.slice(0, 16)); 

        decipher.setAutoPadding(false);
        decrypted = decipher.update(encrypted.slice(16), null, 'binary') + decipher.final();

        // Remove padding bytes equal to the value of the last byte of the returned data.
        decrypted = decrypted.substr(0, decrypted.length - decrypted.charCodeAt(decrypted.length - 1));

        break;
      default:
        return callback(new Error('encryption algorithm ' + encryptionAlgorithm + ' not supported'));
    }
  } catch (e) {
    return callback(e);
  }
  
  callback(null, decrypted);
}

function decryptKeyInfo(doc, options) {
  if (typeof doc === 'string') doc = new xmldom.DOMParser().parseFromString(doc);

  var keyInfo = xpath.select("//*[local-name(.)='KeyInfo' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']", doc)[0];
  var keyEncryptionMethod = xpath.select("//*[local-name(.)='KeyInfo']/*[local-name(.)='EncryptedKey']/*[local-name(.)='EncryptionMethod']", doc)[0];
  var keyEncryptionAlgorighm = keyEncryptionMethod.getAttribute('Algorithm');

  switch (keyEncryptionAlgorighm) {
    case 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p':
{'fraction_non_alphanumeric': 0.1360369221457214, 'fraction_numerical': 0.021109177574336082, 'mean_word_length': 3.423694779116466, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 16, 'xml': 32}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6365088', 'n_tokens_mistral': 4192, 'n_tokens_neox': 3907, 'n_words': 897}