Spaces:
Configuration error
Configuration error
/** | |
* Javascript implementation of PKCS#7 v1.5. | |
* | |
* @author Stefan Siegl | |
* @author Dave Longley | |
* | |
* Copyright (c) 2012 Stefan Siegl <stesie@brokenpipe.de> | |
* Copyright (c) 2012-2015 Digital Bazaar, Inc. | |
* | |
* Currently this implementation only supports ContentType of EnvelopedData, | |
* EncryptedData, or SignedData at the 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 separate file pkcs7asn1.js, since those are referenced from other | |
* PKCS standards like PKCS #12. | |
*/ | |
var forge = require('./forge'); | |
require('./aes'); | |
require('./asn1'); | |
require('./des'); | |
require('./oids'); | |
require('./pem'); | |
require('./pkcs7asn1'); | |
require('./random'); | |
require('./util'); | |
require('./x509'); | |
// shortcut for ASN.1 API | |
var asn1 = forge.asn1; | |
// shortcut for PKCS#7 API | |
var p7 = module.exports = 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') { | |
var error = new Error('Could not convert PKCS#7 message from PEM; PEM ' + | |
'header type is not "PKCS#7".'); | |
error.headerType = msg.type; | |
throw error; | |
} | |
if(msg.procType && msg.procType.type === 'ENCRYPTED') { | |
throw new Error('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)) { | |
var error = new Error('Cannot read PKCS#7 message. ' + | |
'ASN.1 object is not an PKCS#7 ContentInfo.'); | |
error.errors = errors; | |
throw error; | |
} | |
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 new Error('Cannot read PKCS#7 message. ContentType with OID ' + | |
contentType + ' is not (yet) supported.'); | |
} | |
msg.fromAsn1(capture.content.value[0]); | |
return msg; | |
}; | |
p7.createSignedData = function() { | |
var msg = null; | |
msg = { | |
type: forge.pki.oids.signedData, | |
version: 1, | |
certificates: [], | |
crls: [], | |
// TODO: add json-formatted signer stuff here? | |
signers: [], | |
// populated during sign() | |
digestAlgorithmIdentifiers: [], | |
contentInfo: null, | |
signerInfos: [], | |
fromAsn1: function(obj) { | |
// validate SignedData content block and capture data. | |
_fromAsn1(msg, obj, p7.asn1.signedDataValidator); | |
msg.certificates = []; | |
msg.crls = []; | |
msg.digestAlgorithmIdentifiers = []; | |
msg.contentInfo = null; | |
msg.signerInfos = []; | |
if(msg.rawCapture.certificates) { | |
var certs = msg.rawCapture.certificates.value; | |
for(var i = 0; i < certs.length; ++i) { | |
msg.certificates.push(forge.pki.certificateFromAsn1(certs[i])); | |
} | |
} | |
// TODO: parse crls | |
}, | |
toAsn1: function() { | |
// degenerate case with no content | |
if(!msg.contentInfo) { | |
msg.sign(); | |
} | |
var certs = []; | |
for(var i = 0; i < msg.certificates.length; ++i) { | |
certs.push(forge.pki.certificateToAsn1(msg.certificates[i])); | |
} | |
var crls = []; | |
// TODO: implement CRLs | |
// [0] SignedData | |
var signedData = 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, | |
asn1.integerToDer(msg.version).getBytes()), | |
// DigestAlgorithmIdentifiers | |
asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.SET, true, | |
msg.digestAlgorithmIdentifiers), | |
// ContentInfo | |
msg.contentInfo | |
]) | |
]); | |
if(certs.length > 0) { | |
// [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL | |
signedData.value[0].value.push( | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, certs)); | |
} | |
if(crls.length > 0) { | |
// [1] IMPLICIT CertificateRevocationLists OPTIONAL | |
signedData.value[0].value.push( | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, crls)); | |
} | |
// SignerInfos | |
signedData.value[0].value.push( | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, | |
msg.signerInfos)); | |
// 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] SignedData | |
signedData | |
]); | |
}, | |
/** | |
* Add (another) entity to list of signers. | |
* | |
* Note: If authenticatedAttributes are provided, then, per RFC 2315, | |
* they must include at least two attributes: content type and | |
* message digest. The message digest attribute value will be | |
* auto-calculated during signing and will be ignored if provided. | |
* | |
* Here's an example of providing these two attributes: | |
* | |
* forge.pkcs7.createSignedData(); | |
* p7.addSigner({ | |
* issuer: cert.issuer.attributes, | |
* serialNumber: cert.serialNumber, | |
* key: privateKey, | |
* digestAlgorithm: forge.pki.oids.sha1, | |
* authenticatedAttributes: [{ | |
* type: forge.pki.oids.contentType, | |
* value: forge.pki.oids.data | |
* }, { | |
* type: forge.pki.oids.messageDigest | |
* }] | |
* }); | |
* | |
* TODO: Support [subjectKeyIdentifier] as signer's ID. | |
* | |
* @param signer the signer information: | |
* key the signer's private key. | |
* [certificate] a certificate containing the public key | |
* associated with the signer's private key; use this option as | |
* an alternative to specifying signer.issuer and | |
* signer.serialNumber. | |
* [issuer] the issuer attributes (eg: cert.issuer.attributes). | |
* [serialNumber] the signer's certificate's serial number in | |
* hexadecimal (eg: cert.serialNumber). | |
* [digestAlgorithm] the message digest OID, as a string, to use | |
* (eg: forge.pki.oids.sha1). | |
* [authenticatedAttributes] an optional array of attributes | |
* to also sign along with the content. | |
*/ | |
addSigner: function(signer) { | |
var issuer = signer.issuer; | |
var serialNumber = signer.serialNumber; | |
if(signer.certificate) { | |
var cert = signer.certificate; | |
if(typeof cert === 'string') { | |
cert = forge.pki.certificateFromPem(cert); | |
} | |
issuer = cert.issuer.attributes; | |
serialNumber = cert.serialNumber; | |
} | |
var key = signer.key; | |
if(!key) { | |
throw new Error( | |
'Could not add PKCS#7 signer; no private key specified.'); | |
} | |
if(typeof key === 'string') { | |
key = forge.pki.privateKeyFromPem(key); | |
} | |
// ensure OID known for digest algorithm | |
var digestAlgorithm = signer.digestAlgorithm || forge.pki.oids.sha1; | |
switch(digestAlgorithm) { | |
case forge.pki.oids.sha1: | |
case forge.pki.oids.sha256: | |
case forge.pki.oids.sha384: | |
case forge.pki.oids.sha512: | |
case forge.pki.oids.md5: | |
break; | |
default: | |
throw new Error( | |
'Could not add PKCS#7 signer; unknown message digest algorithm: ' + | |
digestAlgorithm); | |
} | |
// if authenticatedAttributes is present, then the attributes | |
// must contain at least PKCS #9 content-type and message-digest | |
var authenticatedAttributes = signer.authenticatedAttributes || []; | |
if(authenticatedAttributes.length > 0) { | |
var contentType = false; | |
var messageDigest = false; | |
for(var i = 0; i < authenticatedAttributes.length; ++i) { | |
var attr = authenticatedAttributes[i]; | |
if(!contentType && attr.type === forge.pki.oids.contentType) { | |
contentType = true; | |
if(messageDigest) { | |
break; | |
} | |
continue; | |
} | |
if(!messageDigest && attr.type === forge.pki.oids.messageDigest) { | |
messageDigest = true; | |
if(contentType) { | |
break; | |
} | |
continue; | |
} | |
} | |
if(!contentType || !messageDigest) { | |
throw new Error('Invalid signer.authenticatedAttributes. If ' + | |
'signer.authenticatedAttributes is specified, then it must ' + | |
'contain at least two attributes, PKCS #9 content-type and ' + | |
'PKCS #9 message-digest.'); | |
} | |
} | |
msg.signers.push({ | |
key: key, | |
version: 1, | |
issuer: issuer, | |
serialNumber: serialNumber, | |
digestAlgorithm: digestAlgorithm, | |
signatureAlgorithm: forge.pki.oids.rsaEncryption, | |
signature: null, | |
authenticatedAttributes: authenticatedAttributes, | |
unauthenticatedAttributes: [] | |
}); | |
}, | |
/** | |
* Signs the content. | |
* @param options Options to apply when signing: | |
* [detached] boolean. If signing should be done in detached mode. Defaults to false. | |
*/ | |
sign: function(options) { | |
options = options || {}; | |
// auto-generate content info | |
if(typeof msg.content !== 'object' || msg.contentInfo === null) { | |
// use Data ContentInfo | |
msg.contentInfo = asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// ContentType | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(forge.pki.oids.data).getBytes()) | |
]); | |
// add actual content, if present | |
if('content' in msg) { | |
var content; | |
if(msg.content instanceof forge.util.ByteBuffer) { | |
content = msg.content.bytes(); | |
} else if(typeof msg.content === 'string') { | |
content = forge.util.encodeUtf8(msg.content); | |
} | |
if (options.detached) { | |
msg.detachedContent = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, content); | |
} else { | |
msg.contentInfo.value.push( | |
// [0] EXPLICIT content | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, | |
content) | |
])); | |
} | |
} | |
} | |
// no signers, return early (degenerate case for certificate container) | |
if(msg.signers.length === 0) { | |
return; | |
} | |
// generate digest algorithm identifiers | |
var mds = addDigestAlgorithmIds(); | |
// generate signerInfos | |
addSignerInfos(mds); | |
}, | |
verify: function() { | |
throw new Error('PKCS#7 signature verification not yet implemented.'); | |
}, | |
/** | |
* Add a certificate. | |
* | |
* @param cert the certificate to add. | |
*/ | |
addCertificate: function(cert) { | |
// convert from PEM | |
if(typeof cert === 'string') { | |
cert = forge.pki.certificateFromPem(cert); | |
} | |
msg.certificates.push(cert); | |
}, | |
/** | |
* Add a certificate revokation list. | |
* | |
* @param crl the certificate revokation list to add. | |
*/ | |
addCertificateRevokationList: function(crl) { | |
throw new Error('PKCS#7 CRL support not yet implemented.'); | |
} | |
}; | |
return msg; | |
function addDigestAlgorithmIds() { | |
var mds = {}; | |
for(var i = 0; i < msg.signers.length; ++i) { | |
var signer = msg.signers[i]; | |
var oid = signer.digestAlgorithm; | |
if(!(oid in mds)) { | |
// content digest | |
mds[oid] = forge.md[forge.pki.oids[oid]].create(); | |
} | |
if(signer.authenticatedAttributes.length === 0) { | |
// no custom attributes to digest; use content message digest | |
signer.md = mds[oid]; | |
} else { | |
// custom attributes to be digested; use own message digest | |
// TODO: optimize to just copy message digest state if that | |
// feature is ever supported with message digests | |
signer.md = forge.md[forge.pki.oids[oid]].create(); | |
} | |
} | |
// add unique digest algorithm identifiers | |
msg.digestAlgorithmIdentifiers = []; | |
for(var oid in mds) { | |
msg.digestAlgorithmIdentifiers.push( | |
// AlgorithmIdentifier | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// algorithm | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(oid).getBytes()), | |
// parameters (null) | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') | |
])); | |
} | |
return mds; | |
} | |
function addSignerInfos(mds) { | |
var content; | |
if (msg.detachedContent) { | |
// Signature has been made in detached mode. | |
content = msg.detachedContent; | |
} else { | |
// Note: ContentInfo is a SEQUENCE with 2 values, second value is | |
// the content field and is optional for a ContentInfo but required here | |
// since signers are present | |
// get ContentInfo content | |
content = msg.contentInfo.value[1]; | |
// skip [0] EXPLICIT content wrapper | |
content = content.value[0]; | |
} | |
if(!content) { | |
throw new Error( | |
'Could not sign PKCS#7 message; there is no content to sign.'); | |
} | |
// get ContentInfo content type | |
var contentType = asn1.derToOid(msg.contentInfo.value[0].value); | |
// serialize content | |
var bytes = asn1.toDer(content); | |
// skip identifier and length per RFC 2315 9.3 | |
// skip identifier (1 byte) | |
bytes.getByte(); | |
// read and discard length bytes | |
asn1.getBerValueLength(bytes); | |
bytes = bytes.getBytes(); | |
// digest content DER value bytes | |
for(var oid in mds) { | |
mds[oid].start().update(bytes); | |
} | |
// sign content | |
var signingTime = new Date(); | |
for(var i = 0; i < msg.signers.length; ++i) { | |
var signer = msg.signers[i]; | |
if(signer.authenticatedAttributes.length === 0) { | |
// if ContentInfo content type is not "Data", then | |
// authenticatedAttributes must be present per RFC 2315 | |
if(contentType !== forge.pki.oids.data) { | |
throw new Error( | |
'Invalid signer; authenticatedAttributes must be present ' + | |
'when the ContentInfo content type is not PKCS#7 Data.'); | |
} | |
} else { | |
// process authenticated attributes | |
// [0] IMPLICIT | |
signer.authenticatedAttributesAsn1 = asn1.create( | |
asn1.Class.CONTEXT_SPECIFIC, 0, true, []); | |
// per RFC 2315, attributes are to be digested using a SET container | |
// not the above [0] IMPLICIT container | |
var attrsAsn1 = asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.SET, true, []); | |
for(var ai = 0; ai < signer.authenticatedAttributes.length; ++ai) { | |
var attr = signer.authenticatedAttributes[ai]; | |
if(attr.type === forge.pki.oids.messageDigest) { | |
// use content message digest as value | |
attr.value = mds[signer.digestAlgorithm].digest(); | |
} else if(attr.type === forge.pki.oids.signingTime) { | |
// auto-populate signing time if not already set | |
if(!attr.value) { | |
attr.value = signingTime; | |
} | |
} | |
// convert to ASN.1 and push onto Attributes SET (for signing) and | |
// onto authenticatedAttributesAsn1 to complete SignedData ASN.1 | |
// TODO: optimize away duplication | |
attrsAsn1.value.push(_attributeToAsn1(attr)); | |
signer.authenticatedAttributesAsn1.value.push(_attributeToAsn1(attr)); | |
} | |
// DER-serialize and digest SET OF attributes only | |
bytes = asn1.toDer(attrsAsn1).getBytes(); | |
signer.md.start().update(bytes); | |
} | |
// sign digest | |
signer.signature = signer.key.sign(signer.md, 'RSASSA-PKCS1-V1_5'); | |
} | |
// add signer info | |
msg.signerInfos = _signersToAsn1(msg.signers); | |
} | |
}; | |
/** | |
* 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, | |
encryptedContent: { | |
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.encryptedContent.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: [], | |
encryptedContent: { | |
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 = _recipientsFromAsn1(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, | |
asn1.integerToDer(msg.version).getBytes()), | |
// RecipientInfos | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, | |
_recipientsToAsn1(msg.recipients)), | |
// EncryptedContentInfo | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, | |
_encryptedContentToAsn1(msg.encryptedContent)) | |
]) | |
]) | |
]); | |
}, | |
/** | |
* 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.encryptedContent.key === undefined && recipient !== undefined && | |
privKey !== undefined) { | |
switch(recipient.encryptedContent.algorithm) { | |
case forge.pki.oids.rsaEncryption: | |
case forge.pki.oids.desCBC: | |
var key = privKey.decrypt(recipient.encryptedContent.content); | |
msg.encryptedContent.key = forge.util.createBuffer(key); | |
break; | |
default: | |
throw new Error('Unsupported asymmetric cipher, ' + | |
'OID ' + recipient.encryptedContent.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.issuer.attributes, | |
serialNumber: cert.serialNumber, | |
encryptedContent: { | |
// 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 encryptedContent.algorithm is used | |
* (defaults to AES-256-CBC). If no key is provided, encryptedContent.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.encryptedContent.content === undefined) { | |
cipher = cipher || msg.encryptedContent.algorithm; | |
key = key || msg.encryptedContent.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 new Error('Unsupported symmetric cipher, OID ' + cipher); | |
} | |
if(key === undefined) { | |
key = forge.util.createBuffer(forge.random.getBytes(keyLen)); | |
} else if(key.length() != keyLen) { | |
throw new Error('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.encryptedContent.algorithm = cipher; | |
msg.encryptedContent.key = key; | |
msg.encryptedContent.parameter = forge.util.createBuffer( | |
forge.random.getBytes(ivLen)); | |
var ciph = ciphFn(key); | |
ciph.start(msg.encryptedContent.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 new Error('Symmetric encryption failed.'); | |
} | |
msg.encryptedContent.content = ciph.output; | |
} | |
// Part 2: asymmetric encryption for each recipient | |
for(var i = 0; i < msg.recipients.length; ++i) { | |
var recipient = msg.recipients[i]; | |
// Nothing to do, encryption already done. | |
if(recipient.encryptedContent.content !== undefined) { | |
continue; | |
} | |
switch(recipient.encryptedContent.algorithm) { | |
case forge.pki.oids.rsaEncryption: | |
recipient.encryptedContent.content = | |
recipient.encryptedContent.key.encrypt( | |
msg.encryptedContent.key.data); | |
break; | |
default: | |
throw new Error('Unsupported asymmetric cipher, OID ' + | |
recipient.encryptedContent.algorithm); | |
} | |
} | |
} | |
}; | |
return msg; | |
}; | |
/** | |
* Converts a single recipient from an ASN.1 object. | |
* | |
* @param obj the ASN.1 RecipientInfo. | |
* | |
* @return the recipient object. | |
*/ | |
function _recipientFromAsn1(obj) { | |
// validate EnvelopedData content block and capture data | |
var capture = {}; | |
var errors = []; | |
if(!asn1.validate(obj, p7.asn1.recipientInfoValidator, capture, errors)) { | |
var error = new Error('Cannot read PKCS#7 RecipientInfo. ' + | |
'ASN.1 object is not an PKCS#7 RecipientInfo.'); | |
error.errors = errors; | |
throw error; | |
} | |
return { | |
version: capture.version.charCodeAt(0), | |
issuer: forge.pki.RDNAttributesAsArray(capture.issuer), | |
serialNumber: forge.util.createBuffer(capture.serial).toHex(), | |
encryptedContent: { | |
algorithm: asn1.derToOid(capture.encAlgorithm), | |
parameter: capture.encParameter ? capture.encParameter.value : undefined, | |
content: capture.encKey | |
} | |
}; | |
} | |
/** | |
* Converts a single recipient object to an ASN.1 object. | |
* | |
* @param obj the recipient object. | |
* | |
* @return the ASN.1 RecipientInfo. | |
*/ | |
function _recipientToAsn1(obj) { | |
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// Version | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, | |
asn1.integerToDer(obj.version).getBytes()), | |
// 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.encryptedContent.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.encryptedContent.content) | |
]); | |
} | |
/** | |
* Map a set of RecipientInfo ASN.1 objects to recipient objects. | |
* | |
* @param infos an array of ASN.1 representations RecipientInfo (i.e. SET OF). | |
* | |
* @return an array of recipient objects. | |
*/ | |
function _recipientsFromAsn1(infos) { | |
var ret = []; | |
for(var i = 0; i < infos.length; ++i) { | |
ret.push(_recipientFromAsn1(infos[i])); | |
} | |
return ret; | |
} | |
/** | |
* Map an array of recipient objects to ASN.1 RecipientInfo objects. | |
* | |
* @param recipients an array of recipientInfo objects. | |
* | |
* @return an array of ASN.1 RecipientInfos. | |
*/ | |
function _recipientsToAsn1(recipients) { | |
var ret = []; | |
for(var i = 0; i < recipients.length; ++i) { | |
ret.push(_recipientToAsn1(recipients[i])); | |
} | |
return ret; | |
} | |
/** | |
* Converts a single signer from an ASN.1 object. | |
* | |
* @param obj the ASN.1 representation of a SignerInfo. | |
* | |
* @return the signer object. | |
*/ | |
function _signerFromAsn1(obj) { | |
// validate EnvelopedData content block and capture data | |
var capture = {}; | |
var errors = []; | |
if(!asn1.validate(obj, p7.asn1.signerInfoValidator, capture, errors)) { | |
var error = new Error('Cannot read PKCS#7 SignerInfo. ' + | |
'ASN.1 object is not an PKCS#7 SignerInfo.'); | |
error.errors = errors; | |
throw error; | |
} | |
var rval = { | |
version: capture.version.charCodeAt(0), | |
issuer: forge.pki.RDNAttributesAsArray(capture.issuer), | |
serialNumber: forge.util.createBuffer(capture.serial).toHex(), | |
digestAlgorithm: asn1.derToOid(capture.digestAlgorithm), | |
signatureAlgorithm: asn1.derToOid(capture.signatureAlgorithm), | |
signature: capture.signature, | |
authenticatedAttributes: [], | |
unauthenticatedAttributes: [] | |
}; | |
// TODO: convert attributes | |
var authenticatedAttributes = capture.authenticatedAttributes || []; | |
var unauthenticatedAttributes = capture.unauthenticatedAttributes || []; | |
return rval; | |
} | |
/** | |
* Converts a single signerInfo object to an ASN.1 object. | |
* | |
* @param obj the signerInfo object. | |
* | |
* @return the ASN.1 representation of a SignerInfo. | |
*/ | |
function _signerToAsn1(obj) { | |
// SignerInfo | |
var rval = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// version | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, | |
asn1.integerToDer(obj.version).getBytes()), | |
// 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)) | |
]), | |
// digestAlgorithm | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// algorithm | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(obj.digestAlgorithm).getBytes()), | |
// parameters (null) | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') | |
]) | |
]); | |
// authenticatedAttributes (OPTIONAL) | |
if(obj.authenticatedAttributesAsn1) { | |
// add ASN.1 previously generated during signing | |
rval.value.push(obj.authenticatedAttributesAsn1); | |
} | |
// digestEncryptionAlgorithm | |
rval.value.push(asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// algorithm | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(obj.signatureAlgorithm).getBytes()), | |
// parameters (null) | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') | |
])); | |
// encryptedDigest | |
rval.value.push(asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.signature)); | |
// unauthenticatedAttributes (OPTIONAL) | |
if(obj.unauthenticatedAttributes.length > 0) { | |
// [1] IMPLICIT | |
var attrsAsn1 = asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, []); | |
for(var i = 0; i < obj.unauthenticatedAttributes.length; ++i) { | |
var attr = obj.unauthenticatedAttributes[i]; | |
attrsAsn1.values.push(_attributeToAsn1(attr)); | |
} | |
rval.value.push(attrsAsn1); | |
} | |
return rval; | |
} | |
/** | |
* Map a set of SignerInfo ASN.1 objects to an array of signer objects. | |
* | |
* @param signerInfoAsn1s an array of ASN.1 SignerInfos (i.e. SET OF). | |
* | |
* @return an array of signers objects. | |
*/ | |
function _signersFromAsn1(signerInfoAsn1s) { | |
var ret = []; | |
for(var i = 0; i < signerInfoAsn1s.length; ++i) { | |
ret.push(_signerFromAsn1(signerInfoAsn1s[i])); | |
} | |
return ret; | |
} | |
/** | |
* Map an array of signer objects to ASN.1 objects. | |
* | |
* @param signers an array of signer objects. | |
* | |
* @return an array of ASN.1 SignerInfos. | |
*/ | |
function _signersToAsn1(signers) { | |
var ret = []; | |
for(var i = 0; i < signers.length; ++i) { | |
ret.push(_signerToAsn1(signers[i])); | |
} | |
return ret; | |
} | |
/** | |
* Convert an attribute object to an ASN.1 Attribute. | |
* | |
* @param attr the attribute object. | |
* | |
* @return the ASN.1 Attribute. | |
*/ | |
function _attributeToAsn1(attr) { | |
var value; | |
// TODO: generalize to support more attributes | |
if(attr.type === forge.pki.oids.contentType) { | |
value = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(attr.value).getBytes()); | |
} else if(attr.type === forge.pki.oids.messageDigest) { | |
value = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, | |
attr.value.bytes()); | |
} else if(attr.type === forge.pki.oids.signingTime) { | |
/* Note per RFC 2985: Dates between 1 January 1950 and 31 December 2049 | |
(inclusive) MUST be encoded as UTCTime. Any dates with year values | |
before 1950 or after 2049 MUST be encoded as GeneralizedTime. [Further,] | |
UTCTime values MUST be expressed in Greenwich Mean Time (Zulu) and MUST | |
include seconds (i.e., times are YYMMDDHHMMSSZ), even where the | |
number of seconds is zero. Midnight (GMT) must be represented as | |
"YYMMDD000000Z". */ | |
// TODO: make these module-level constants | |
var jan_1_1950 = new Date('1950-01-01T00:00:00Z'); | |
var jan_1_2050 = new Date('2050-01-01T00:00:00Z'); | |
var date = attr.value; | |
if(typeof date === 'string') { | |
// try to parse date | |
var timestamp = Date.parse(date); | |
if(!isNaN(timestamp)) { | |
date = new Date(timestamp); | |
} else if(date.length === 13) { | |
// YYMMDDHHMMSSZ (13 chars for UTCTime) | |
date = asn1.utcTimeToDate(date); | |
} else { | |
// assume generalized time | |
date = asn1.generalizedTimeToDate(date); | |
} | |
} | |
if(date >= jan_1_1950 && date < jan_1_2050) { | |
value = asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.UTCTIME, false, | |
asn1.dateToUtcTime(date)); | |
} else { | |
value = asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.GENERALIZEDTIME, false, | |
asn1.dateToGeneralizedTime(date)); | |
} | |
} | |
// TODO: expose as common API call | |
// create a RelativeDistinguishedName set | |
// each value in the set is an AttributeTypeAndValue first | |
// containing the type (an OID) and second the value | |
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// AttributeType | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(attr.type).getBytes()), | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [ | |
// AttributeValue | |
value | |
]) | |
]); | |
} | |
/** | |
* Map messages encrypted content to ASN.1 objects. | |
* | |
* @param ec The encryptedContent object of the message. | |
* | |
* @return ASN.1 representation of the encryptedContent object (SEQUENCE). | |
*/ | |
function _encryptedContentToAsn1(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) | |
!ec.parameter ? | |
undefined : | |
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 the value map captured by validator object. | |
*/ | |
function _fromAsn1(msg, obj, validator) { | |
var capture = {}; | |
var errors = []; | |
if(!asn1.validate(obj, validator, capture, errors)) { | |
var error = new Error('Cannot read PKCS#7 message. ' + | |
'ASN.1 object is not a supported PKCS#7 message.'); | |
error.errors = error; | |
throw error; | |
} | |
// Check contentType, so far we only support (raw) Data. | |
var contentType = asn1.derToOid(capture.contentType); | |
if(contentType !== forge.pki.oids.data) { | |
throw new Error('Unsupported PKCS#7 message. ' + | |
'Only wrapped ContentType Data supported.'); | |
} | |
if(capture.encryptedContent) { | |
var content = ''; | |
if(forge.util.isArray(capture.encryptedContent)) { | |
for(var i = 0; i < capture.encryptedContent.length; ++i) { | |
if(capture.encryptedContent[i].type !== asn1.Type.OCTETSTRING) { | |
throw new Error('Malformed PKCS#7 message, expecting encrypted ' + | |
'content constructed of only OCTET STRING objects.'); | |
} | |
content += capture.encryptedContent[i].value; | |
} | |
} else { | |
content = capture.encryptedContent; | |
} | |
msg.encryptedContent = { | |
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 new Error('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 encryptedContent attribute of the | |
* message object. | |
* | |
* @param The PKCS#7 message object. | |
*/ | |
function _decryptContent(msg) { | |
if(msg.encryptedContent.key === undefined) { | |
throw new Error('Symmetric key not available.'); | |
} | |
if(msg.content === undefined) { | |
var ciph; | |
switch(msg.encryptedContent.algorithm) { | |
case forge.pki.oids['aes128-CBC']: | |
case forge.pki.oids['aes192-CBC']: | |
case forge.pki.oids['aes256-CBC']: | |
ciph = forge.aes.createDecryptionCipher(msg.encryptedContent.key); | |
break; | |
case forge.pki.oids['desCBC']: | |
case forge.pki.oids['des-EDE3-CBC']: | |
ciph = forge.des.createDecryptionCipher(msg.encryptedContent.key); | |
break; | |
default: | |
throw new Error('Unsupported symmetric cipher, OID ' + | |
msg.encryptedContent.algorithm); | |
} | |
ciph.start(msg.encryptedContent.parameter); | |
ciph.update(msg.encryptedContent.content); | |
if(!ciph.finish()) { | |
throw new Error('Symmetric decryption failed.'); | |
} | |
msg.content = ciph.output; | |
} | |
} | |