/** * Javascript implementation of PKCS#7 v1.5. Currently only certain parts of * PKCS#7 are implemented, especially the enveloped-data content type. * * @author Stefan Siegl * * Copyright (c) 2012 Stefan Siegl * * Currently this implementation only supports ContentType of either * EnvelopedData or EncryptedData on root level. The top level elements may * contain only a ContentInfo of ContentType Data, i.e. plain data. Further * nesting is not (yet) supported. * * The Forge validators for PKCS #7's ASN.1 structures are available from * a seperate file pkcs7asn1.js, since those are referenced from other * PKCS standards like PKCS #12. */ (function() { /* ########## Begin module implementation ########## */ function initModule(forge) { // shortcut for ASN.1 API var asn1 = forge.asn1; // shortcut for PKCS#7 API var p7 = forge.pkcs7 = forge.pkcs7 || {}; /** * Converts a PKCS#7 message from PEM format. * * @param pem the PEM-formatted PKCS#7 message. * * @return the PKCS#7 message. */ p7.messageFromPem = function(pem) { var msg = forge.pem.decode(pem)[0]; if(msg.type !== 'PKCS7') { throw { message: 'Could not convert PKCS#7 message from PEM; PEM header type ' + 'is not "PKCS#7".', headerType: msg.type }; } if(msg.procType && msg.procType.type === 'ENCRYPTED') { throw { message: 'Could not convert PKCS#7 message from PEM; PEM is encrypted.' }; } // convert DER to ASN.1 object var obj = asn1.fromDer(msg.body); return p7.messageFromAsn1(obj); }; /** * Converts a PKCS#7 message to PEM format. * * @param msg The PKCS#7 message object * @param maxline The maximum characters per line, defaults to 64. * * @return The PEM-formatted PKCS#7 message. */ p7.messageToPem = function(msg, maxline) { // convert to ASN.1, then DER, then PEM-encode var pemObj = { type: 'PKCS7', body: asn1.toDer(msg.toAsn1()).getBytes() }; return forge.pem.encode(pemObj, {maxline: maxline}); }; /** * Converts a PKCS#7 message from an ASN.1 object. * * @param obj the ASN.1 representation of a ContentInfo. * * @return the PKCS#7 message. */ p7.messageFromAsn1 = function(obj) { // validate root level ContentInfo and capture data var capture = {}; var errors = []; if(!asn1.validate(obj, p7.asn1.contentInfoValidator, capture, errors)) { throw { message: 'Cannot read PKCS#7 message. ' + 'ASN.1 object is not an PKCS#7 ContentInfo.', errors: errors }; } var contentType = asn1.derToOid(capture.contentType); var msg; switch(contentType) { case forge.pki.oids.envelopedData: msg = p7.createEnvelopedData(); break; case forge.pki.oids.encryptedData: msg = p7.createEncryptedData(); break; case forge.pki.oids.signedData: msg = p7.createSignedData(); break; default: throw { message: 'Cannot read PKCS#7 message. ContentType with OID ' + contentType + ' is not (yet) supported.' }; } msg.fromAsn1(capture.content.value[0]); return msg; }; /** * Converts a single RecipientInfo from an ASN.1 object. * * @param obj The ASN.1 representation of a RecipientInfo. * * @return The recipientInfo object. */ var _recipientInfoFromAsn1 = function(obj) { // Validate EnvelopedData content block and capture data. var capture = {}; var errors = []; if(!asn1.validate(obj, p7.asn1.recipientInfoValidator, capture, errors)) { throw { message: 'Cannot read PKCS#7 message. ' + 'ASN.1 object is not an PKCS#7 EnvelopedData.', errors: errors }; } return { version: capture.version.charCodeAt(0), issuer: forge.pki.RDNAttributesAsArray(capture.issuer), serialNumber: forge.util.createBuffer(capture.serial).toHex(), encContent: { algorithm: asn1.derToOid(capture.encAlgorithm), parameter: capture.encParameter.value, content: capture.encKey } }; }; /** * Converts a single recipientInfo object to an ASN.1 object. * * @param obj The recipientInfo object. * * @return The ASN.1 representation of a RecipientInfo. */ var _recipientInfoToAsn1 = function(obj) { return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // Version asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, String.fromCharCode(obj.version)), // IssuerAndSerialNumber asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // Name forge.pki.distinguishedNameToAsn1({ attributes: obj.issuer }), // Serial asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, forge.util.hexToBytes(obj.serialNumber)) ]), // KeyEncryptionAlgorithmIdentifier asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // Algorithm asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(obj.encContent.algorithm).getBytes()), // Parameter, force NULL, only RSA supported for now. asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') ]), // EncryptedKey asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.encContent.content) ]); }; /** * Map a set of RecipientInfo ASN.1 objects to recipientInfo objects. * * @param objArr Array of ASN.1 representations RecipientInfo (i.e. SET OF). * * @return array of recipientInfo objects. */ var _recipientInfosFromAsn1 = function(objArr) { var ret = []; for(var i = 0; i < objArr.length; i ++) { ret.push(_recipientInfoFromAsn1(objArr[i])); } return ret; }; /** * Map an array of recipientInfo objects to ASN.1 objects. * * @param recipientsArr Array of recipientInfo objects. * * @return Array of ASN.1 representations RecipientInfo. */ var _recipientInfosToAsn1 = function(recipientsArr) { var ret = []; for(var i = 0; i < recipientsArr.length; i ++) { ret.push(_recipientInfoToAsn1(recipientsArr[i])); } return ret; }; /** * Map messages encrypted content to ASN.1 objects. * * @param ec The encContent object of the message. * * @return ASN.1 representation of the encContent object (SEQUENCE). */ var _encContentToAsn1 = function(ec) { return [ // ContentType, always Data for the moment asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(forge.pki.oids.data).getBytes()), // ContentEncryptionAlgorithmIdentifier asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // Algorithm asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(ec.algorithm).getBytes()), // Parameters (IV) asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, ec.parameter.getBytes()) ]), // [0] EncryptedContent asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, ec.content.getBytes()) ]) ]; }; /** * Reads the "common part" of an PKCS#7 content block (in ASN.1 format) * * This function reads the "common part" of the PKCS#7 content blocks * EncryptedData and EnvelopedData, i.e. version number and symmetrically * encrypted content block. * * The result of the ASN.1 validate and capture process is returned * to allow the caller to extract further data, e.g. the list of recipients * in case of a EnvelopedData object. * * @param msg The PKCS#7 object to read the data to * @param obj The ASN.1 representation of the content block * @param validator The ASN.1 structure validator object to use * @return Map with values captured by validator object */ var _fromAsn1 = function(msg, obj, validator) { var capture = {}; var errors = []; if(!asn1.validate(obj, validator, capture, errors)) { throw { message: 'Cannot read PKCS#7 message. ' + 'ASN.1 object is not an PKCS#7 EnvelopedData.', errors: errors }; } // Check contentType, so far we only support (raw) Data. var contentType = asn1.derToOid(capture.contentType); if(contentType !== forge.pki.oids.data) { throw { message: 'Unsupported PKCS#7 message. ' + 'Only contentType Data supported within EnvelopedData.' }; } if(capture.encContent) { var content = ''; if(forge.util.isArray(capture.encContent)) { for(var i = 0; i < capture.encContent.length; ++i) { if(capture.encContent[i].type !== asn1.Type.OCTETSTRING) { throw { message: 'Malformed PKCS#7 message, expecting encrypted ' + 'content constructed of only OCTET STRING objects.' }; } content += capture.encContent[i].value; } } else { content = capture.encContent; } msg.encContent = { algorithm: asn1.derToOid(capture.encAlgorithm), parameter: forge.util.createBuffer(capture.encParameter.value), content: forge.util.createBuffer(content) }; } if(capture.content) { var content = ''; if(forge.util.isArray(capture.content)) { for(var i = 0; i < capture.content.length; ++i) { if(capture.content[i].type !== asn1.Type.OCTETSTRING) { throw { message: 'Malformed PKCS#7 message, expecting ' + 'content constructed of only OCTET STRING objects.' }; } content += capture.content[i].value; } } else { content = capture.content; } msg.content = forge.util.createBuffer(content); } msg.version = capture.version.charCodeAt(0); msg.rawCapture = capture; return capture; }; /** * Decrypt the symmetrically encrypted content block of the PKCS#7 message. * * Decryption is skipped in case the PKCS#7 message object already has a * (decrypted) content attribute. The algorithm, key and cipher parameters * (probably the iv) are taken from the encContent attribute of the message * object. * * @param The PKCS#7 message object. */ var _decryptContent = function (msg) { if(msg.encContent.key === undefined) { throw { message: 'Symmetric key not available.' }; } if(msg.content === undefined) { var ciph; switch(msg.encContent.algorithm) { case forge.pki.oids['aes128-CBC']: case forge.pki.oids['aes192-CBC']: case forge.pki.oids['aes256-CBC']: ciph = forge.aes.createDecryptionCipher(msg.encContent.key); break; case forge.pki.oids['des-EDE3-CBC']: ciph = forge.des.createDecryptionCipher(msg.encContent.key); break; default: throw { message: 'Unsupported symmetric cipher, ' + 'OID ' + msg.encContent.algorithm }; } ciph.start(msg.encContent.parameter); ciph.update(msg.encContent.content); if(!ciph.finish()) { throw { message: 'Symmetric decryption failed.' }; } msg.content = ciph.output; } }; p7.createSignedData = function() { var msg = null; msg = { type: forge.pki.oids.signedData, version: 1, fromAsn1: function(obj) { // validate SignedData content block and capture data. _fromAsn1(msg, obj, p7.asn1.signedDataValidator); } }; return msg; }; /** * Creates an empty PKCS#7 message of type EncryptedData. * * @return the message. */ p7.createEncryptedData = function() { var msg = null; msg = { type: forge.pki.oids.encryptedData, version: 0, encContent: { algorithm: forge.pki.oids['aes256-CBC'] }, /** * Reads an EncryptedData content block (in ASN.1 format) * * @param obj The ASN.1 representation of the EncryptedData content block */ fromAsn1: function(obj) { // Validate EncryptedData content block and capture data. _fromAsn1(msg, obj, p7.asn1.encryptedDataValidator); }, /** * Decrypt encrypted content * * @param key The (symmetric) key as a byte buffer */ decrypt: function(key) { if(key !== undefined) { msg.encContent.key = key; } _decryptContent(msg); } }; return msg; }; /** * Creates an empty PKCS#7 message of type EnvelopedData. * * @return the message. */ p7.createEnvelopedData = function() { var msg = null; msg = { type: forge.pki.oids.envelopedData, version: 0, recipients: [], encContent: { algorithm: forge.pki.oids['aes256-CBC'] }, /** * Reads an EnvelopedData content block (in ASN.1 format) * * @param obj The ASN.1 representation of the EnvelopedData content block */ fromAsn1: function(obj) { // Validate EnvelopedData content block and capture data. var capture = _fromAsn1(msg, obj, p7.asn1.envelopedDataValidator); msg.recipients = _recipientInfosFromAsn1(capture.recipientInfos.value); }, toAsn1: function() { // ContentInfo return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // ContentType asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, asn1.oidToDer(msg.type).getBytes()), // [0] EnvelopedData asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ // Version asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, String.fromCharCode(msg.version)), // RecipientInfos asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, _recipientInfosToAsn1(msg.recipients)), // EncryptedContentInfo asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, _encContentToAsn1(msg.encContent)) ]) ]) ]); }, /** * Find recipient by X.509 certificate's issuer. * * @param cert the certificate with the issuer to look for. * * @return the recipient object. */ findRecipient: function(cert) { var sAttr = cert.issuer.attributes; for(var i = 0; i < msg.recipients.length; ++i) { var r = msg.recipients[i]; var rAttr = r.issuer; if(r.serialNumber !== cert.serialNumber) { continue; } if(rAttr.length !== sAttr.length) { continue; } var match = true; for(var j = 0; j < sAttr.length; ++j) { if(rAttr[j].type !== sAttr[j].type || rAttr[j].value !== sAttr[j].value) { match = false; break; } } if(match) { return r; } } return null; }, /** * Decrypt enveloped content * * @param recipient The recipient object related to the private key * @param privKey The (RSA) private key object */ decrypt: function(recipient, privKey) { if(msg.encContent.key === undefined && recipient !== undefined && privKey !== undefined) { switch(recipient.encContent.algorithm) { case forge.pki.oids.rsaEncryption: var key = privKey.decrypt(recipient.encContent.content); msg.encContent.key = forge.util.createBuffer(key); break; default: throw { message: 'Unsupported asymmetric cipher, ' + 'OID ' + recipient.encContent.algorithm }; } } _decryptContent(msg); }, /** * Add (another) entity to list of recipients. * * @param cert The certificate of the entity to add. */ addRecipient: function(cert) { msg.recipients.push({ version: 0, issuer: cert.subject.attributes, serialNumber: cert.serialNumber, encContent: { // We simply assume rsaEncryption here, since forge.pki only // supports RSA so far. If the PKI module supports other // ciphers one day, we need to modify this one as well. algorithm: forge.pki.oids.rsaEncryption, key: cert.publicKey } }); }, /** * Encrypt enveloped content. * * This function supports two optional arguments, cipher and key, which * can be used to influence symmetric encryption. Unless cipher is * provided, the cipher specified in encContent.algorithm is used * (defaults to AES-256-CBC). If no key is provided, encContent.key * is (re-)used. If that one's not set, a random key will be generated * automatically. * * @param [key] The key to be used for symmetric encryption. * @param [cipher] The OID of the symmetric cipher to use. */ encrypt: function(key, cipher) { // Part 1: Symmetric encryption if(msg.encContent.content === undefined) { cipher = cipher || msg.encContent.algorithm; key = key || msg.encContent.key; var keyLen, ivLen, ciphFn; switch(cipher) { case forge.pki.oids['aes128-CBC']: keyLen = 16; ivLen = 16; ciphFn = forge.aes.createEncryptionCipher; break; case forge.pki.oids['aes192-CBC']: keyLen = 24; ivLen = 16; ciphFn = forge.aes.createEncryptionCipher; break; case forge.pki.oids['aes256-CBC']: keyLen = 32; ivLen = 16; ciphFn = forge.aes.createEncryptionCipher; break; case forge.pki.oids['des-EDE3-CBC']: keyLen = 24; ivLen = 8; ciphFn = forge.des.createEncryptionCipher; break; default: throw { message: 'Unsupported symmetric cipher, OID ' + cipher }; } if(key === undefined) { key = forge.util.createBuffer(forge.random.getBytes(keyLen)); } else if(key.length() != keyLen) { throw { message: 'Symmetric key has wrong length, ' + 'got ' + key.length() + ' bytes, expected ' + keyLen }; } // Keep a copy of the key & IV in the object, so the caller can // use it for whatever reason. msg.encContent.algorithm = cipher; msg.encContent.key = key; msg.encContent.parameter = forge.util.createBuffer(forge.random.getBytes(ivLen)); var ciph = ciphFn(key); ciph.start(msg.encContent.parameter.copy()); ciph.update(msg.content); // The finish function does PKCS#7 padding by default, therefore // no action required by us. if(!ciph.finish()) { throw { message: 'Symmetric encryption failed.' }; } msg.encContent.content = ciph.output; } // Part 2: asymmetric encryption for each recipient for(var i = 0; i < msg.recipients.length; i ++) { var recipient = msg.recipients[i]; if(recipient.encContent.content !== undefined) { continue; // Nothing to do, encryption already done. } switch(recipient.encContent.algorithm) { case forge.pki.oids.rsaEncryption: recipient.encContent.content = recipient.encContent.key.encrypt(msg.encContent.key.data); break; default: throw { message: 'Unsupported asymmetric cipher, OID ' + recipient.encContent.algorithm }; } } } }; return msg; }; } // end module implementation /* ########## Begin module wrapper ########## */ var name = 'pkcs7'; if(typeof define !== 'function') { // NodeJS -> AMD if(typeof module === 'object' && module.exports) { var nodeJS = true; define = function(ids, factory) { factory(require, module); }; } //