Spaces:
Configuration error
Configuration error
/** | |
* Javascript implementation of PKCS#12. | |
* | |
* @author Dave Longley | |
* @author Stefan Siegl <stesie@brokenpipe.de> | |
* | |
* Copyright (c) 2010-2014 Digital Bazaar, Inc. | |
* Copyright (c) 2012 Stefan Siegl <stesie@brokenpipe.de> | |
* | |
* The ASN.1 representation of PKCS#12 is as follows | |
* (see ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-12/pkcs-12-tc1.pdf for details) | |
* | |
* PFX ::= SEQUENCE { | |
* version INTEGER {v3(3)}(v3,...), | |
* authSafe ContentInfo, | |
* macData MacData OPTIONAL | |
* } | |
* | |
* MacData ::= SEQUENCE { | |
* mac DigestInfo, | |
* macSalt OCTET STRING, | |
* iterations INTEGER DEFAULT 1 | |
* } | |
* Note: The iterations default is for historical reasons and its use is | |
* deprecated. A higher value, like 1024, is recommended. | |
* | |
* DigestInfo is defined in PKCS#7 as follows: | |
* | |
* DigestInfo ::= SEQUENCE { | |
* digestAlgorithm DigestAlgorithmIdentifier, | |
* digest Digest | |
* } | |
* | |
* DigestAlgorithmIdentifier ::= AlgorithmIdentifier | |
* | |
* The AlgorithmIdentifier contains an Object Identifier (OID) and parameters | |
* for the algorithm, if any. In the case of SHA1 there is none. | |
* | |
* AlgorithmIdentifer ::= SEQUENCE { | |
* algorithm OBJECT IDENTIFIER, | |
* parameters ANY DEFINED BY algorithm OPTIONAL | |
* } | |
* | |
* Digest ::= OCTET STRING | |
* | |
* | |
* ContentInfo ::= SEQUENCE { | |
* contentType ContentType, | |
* content [0] EXPLICIT ANY DEFINED BY contentType OPTIONAL | |
* } | |
* | |
* ContentType ::= OBJECT IDENTIFIER | |
* | |
* AuthenticatedSafe ::= SEQUENCE OF ContentInfo | |
* -- Data if unencrypted | |
* -- EncryptedData if password-encrypted | |
* -- EnvelopedData if public key-encrypted | |
* | |
* | |
* SafeContents ::= SEQUENCE OF SafeBag | |
* | |
* SafeBag ::= SEQUENCE { | |
* bagId BAG-TYPE.&id ({PKCS12BagSet}) | |
* bagValue [0] EXPLICIT BAG-TYPE.&Type({PKCS12BagSet}{@bagId}), | |
* bagAttributes SET OF PKCS12Attribute OPTIONAL | |
* } | |
* | |
* PKCS12Attribute ::= SEQUENCE { | |
* attrId ATTRIBUTE.&id ({PKCS12AttrSet}), | |
* attrValues SET OF ATTRIBUTE.&Type ({PKCS12AttrSet}{@attrId}) | |
* } -- This type is compatible with the X.500 type 'Attribute' | |
* | |
* PKCS12AttrSet ATTRIBUTE ::= { | |
* friendlyName | -- from PKCS #9 | |
* localKeyId, -- from PKCS #9 | |
* ... -- Other attributes are allowed | |
* } | |
* | |
* CertBag ::= SEQUENCE { | |
* certId BAG-TYPE.&id ({CertTypes}), | |
* certValue [0] EXPLICIT BAG-TYPE.&Type ({CertTypes}{@certId}) | |
* } | |
* | |
* x509Certificate BAG-TYPE ::= {OCTET STRING IDENTIFIED BY {certTypes 1}} | |
* -- DER-encoded X.509 certificate stored in OCTET STRING | |
* | |
* sdsiCertificate BAG-TYPE ::= {IA5String IDENTIFIED BY {certTypes 2}} | |
* -- Base64-encoded SDSI certificate stored in IA5String | |
* | |
* CertTypes BAG-TYPE ::= { | |
* x509Certificate | | |
* sdsiCertificate, | |
* ... -- For future extensions | |
* } | |
*/ | |
var forge = require('./forge'); | |
require('./asn1'); | |
require('./hmac'); | |
require('./oids'); | |
require('./pkcs7asn1'); | |
require('./pbe'); | |
require('./random'); | |
require('./rsa'); | |
require('./sha1'); | |
require('./util'); | |
require('./x509'); | |
// shortcut for asn.1 & PKI API | |
var asn1 = forge.asn1; | |
var pki = forge.pki; | |
// shortcut for PKCS#12 API | |
var p12 = module.exports = forge.pkcs12 = forge.pkcs12 || {}; | |
var contentInfoValidator = { | |
name: 'ContentInfo', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SEQUENCE, // a ContentInfo | |
constructed: true, | |
value: [{ | |
name: 'ContentInfo.contentType', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.OID, | |
constructed: false, | |
capture: 'contentType' | |
}, { | |
name: 'ContentInfo.content', | |
tagClass: asn1.Class.CONTEXT_SPECIFIC, | |
constructed: true, | |
captureAsn1: 'content' | |
}] | |
}; | |
var pfxValidator = { | |
name: 'PFX', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SEQUENCE, | |
constructed: true, | |
value: [{ | |
name: 'PFX.version', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.INTEGER, | |
constructed: false, | |
capture: 'version' | |
}, | |
contentInfoValidator, { | |
name: 'PFX.macData', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SEQUENCE, | |
constructed: true, | |
optional: true, | |
captureAsn1: 'mac', | |
value: [{ | |
name: 'PFX.macData.mac', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SEQUENCE, // DigestInfo | |
constructed: true, | |
value: [{ | |
name: 'PFX.macData.mac.digestAlgorithm', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SEQUENCE, // DigestAlgorithmIdentifier | |
constructed: true, | |
value: [{ | |
name: 'PFX.macData.mac.digestAlgorithm.algorithm', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.OID, | |
constructed: false, | |
capture: 'macAlgorithm' | |
}, { | |
name: 'PFX.macData.mac.digestAlgorithm.parameters', | |
tagClass: asn1.Class.UNIVERSAL, | |
captureAsn1: 'macAlgorithmParameters' | |
}] | |
}, { | |
name: 'PFX.macData.mac.digest', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.OCTETSTRING, | |
constructed: false, | |
capture: 'macDigest' | |
}] | |
}, { | |
name: 'PFX.macData.macSalt', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.OCTETSTRING, | |
constructed: false, | |
capture: 'macSalt' | |
}, { | |
name: 'PFX.macData.iterations', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.INTEGER, | |
constructed: false, | |
optional: true, | |
capture: 'macIterations' | |
}] | |
}] | |
}; | |
var safeBagValidator = { | |
name: 'SafeBag', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SEQUENCE, | |
constructed: true, | |
value: [{ | |
name: 'SafeBag.bagId', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.OID, | |
constructed: false, | |
capture: 'bagId' | |
}, { | |
name: 'SafeBag.bagValue', | |
tagClass: asn1.Class.CONTEXT_SPECIFIC, | |
constructed: true, | |
captureAsn1: 'bagValue' | |
}, { | |
name: 'SafeBag.bagAttributes', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SET, | |
constructed: true, | |
optional: true, | |
capture: 'bagAttributes' | |
}] | |
}; | |
var attributeValidator = { | |
name: 'Attribute', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SEQUENCE, | |
constructed: true, | |
value: [{ | |
name: 'Attribute.attrId', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.OID, | |
constructed: false, | |
capture: 'oid' | |
}, { | |
name: 'Attribute.attrValues', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SET, | |
constructed: true, | |
capture: 'values' | |
}] | |
}; | |
var certBagValidator = { | |
name: 'CertBag', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.SEQUENCE, | |
constructed: true, | |
value: [{ | |
name: 'CertBag.certId', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Type.OID, | |
constructed: false, | |
capture: 'certId' | |
}, { | |
name: 'CertBag.certValue', | |
tagClass: asn1.Class.CONTEXT_SPECIFIC, | |
constructed: true, | |
/* So far we only support X.509 certificates (which are wrapped in | |
an OCTET STRING, hence hard code that here). */ | |
value: [{ | |
name: 'CertBag.certValue[0]', | |
tagClass: asn1.Class.UNIVERSAL, | |
type: asn1.Class.OCTETSTRING, | |
constructed: false, | |
capture: 'cert' | |
}] | |
}] | |
}; | |
/** | |
* Search SafeContents structure for bags with matching attributes. | |
* | |
* The search can optionally be narrowed by a certain bag type. | |
* | |
* @param safeContents the SafeContents structure to search in. | |
* @param attrName the name of the attribute to compare against. | |
* @param attrValue the attribute value to search for. | |
* @param [bagType] bag type to narrow search by. | |
* | |
* @return an array of matching bags. | |
*/ | |
function _getBagsByAttribute(safeContents, attrName, attrValue, bagType) { | |
var result = []; | |
for(var i = 0; i < safeContents.length; i++) { | |
for(var j = 0; j < safeContents[i].safeBags.length; j++) { | |
var bag = safeContents[i].safeBags[j]; | |
if(bagType !== undefined && bag.type !== bagType) { | |
continue; | |
} | |
// only filter by bag type, no attribute specified | |
if(attrName === null) { | |
result.push(bag); | |
continue; | |
} | |
if(bag.attributes[attrName] !== undefined && | |
bag.attributes[attrName].indexOf(attrValue) >= 0) { | |
result.push(bag); | |
} | |
} | |
} | |
return result; | |
} | |
/** | |
* Converts a PKCS#12 PFX in ASN.1 notation into a PFX object. | |
* | |
* @param obj The PKCS#12 PFX in ASN.1 notation. | |
* @param strict true to use strict DER decoding, false not to (default: true). | |
* @param {String} password Password to decrypt with (optional). | |
* | |
* @return PKCS#12 PFX object. | |
*/ | |
p12.pkcs12FromAsn1 = function(obj, strict, password) { | |
// handle args | |
if(typeof strict === 'string') { | |
password = strict; | |
strict = true; | |
} else if(strict === undefined) { | |
strict = true; | |
} | |
// validate PFX and capture data | |
var capture = {}; | |
var errors = []; | |
if(!asn1.validate(obj, pfxValidator, capture, errors)) { | |
var error = new Error('Cannot read PKCS#12 PFX. ' + | |
'ASN.1 object is not an PKCS#12 PFX.'); | |
error.errors = error; | |
throw error; | |
} | |
var pfx = { | |
version: capture.version.charCodeAt(0), | |
safeContents: [], | |
/** | |
* Gets bags with matching attributes. | |
* | |
* @param filter the attributes to filter by: | |
* [localKeyId] the localKeyId to search for. | |
* [localKeyIdHex] the localKeyId in hex to search for. | |
* [friendlyName] the friendly name to search for. | |
* [bagType] bag type to narrow each attribute search by. | |
* | |
* @return a map of attribute type to an array of matching bags or, if no | |
* attribute was given but a bag type, the map key will be the | |
* bag type. | |
*/ | |
getBags: function(filter) { | |
var rval = {}; | |
var localKeyId; | |
if('localKeyId' in filter) { | |
localKeyId = filter.localKeyId; | |
} else if('localKeyIdHex' in filter) { | |
localKeyId = forge.util.hexToBytes(filter.localKeyIdHex); | |
} | |
// filter on bagType only | |
if(localKeyId === undefined && !('friendlyName' in filter) && | |
'bagType' in filter) { | |
rval[filter.bagType] = _getBagsByAttribute( | |
pfx.safeContents, null, null, filter.bagType); | |
} | |
if(localKeyId !== undefined) { | |
rval.localKeyId = _getBagsByAttribute( | |
pfx.safeContents, 'localKeyId', | |
localKeyId, filter.bagType); | |
} | |
if('friendlyName' in filter) { | |
rval.friendlyName = _getBagsByAttribute( | |
pfx.safeContents, 'friendlyName', | |
filter.friendlyName, filter.bagType); | |
} | |
return rval; | |
}, | |
/** | |
* DEPRECATED: use getBags() instead. | |
* | |
* Get bags with matching friendlyName attribute. | |
* | |
* @param friendlyName the friendly name to search for. | |
* @param [bagType] bag type to narrow search by. | |
* | |
* @return an array of bags with matching friendlyName attribute. | |
*/ | |
getBagsByFriendlyName: function(friendlyName, bagType) { | |
return _getBagsByAttribute( | |
pfx.safeContents, 'friendlyName', friendlyName, bagType); | |
}, | |
/** | |
* DEPRECATED: use getBags() instead. | |
* | |
* Get bags with matching localKeyId attribute. | |
* | |
* @param localKeyId the localKeyId to search for. | |
* @param [bagType] bag type to narrow search by. | |
* | |
* @return an array of bags with matching localKeyId attribute. | |
*/ | |
getBagsByLocalKeyId: function(localKeyId, bagType) { | |
return _getBagsByAttribute( | |
pfx.safeContents, 'localKeyId', localKeyId, bagType); | |
} | |
}; | |
if(capture.version.charCodeAt(0) !== 3) { | |
var error = new Error('PKCS#12 PFX of version other than 3 not supported.'); | |
error.version = capture.version.charCodeAt(0); | |
throw error; | |
} | |
if(asn1.derToOid(capture.contentType) !== pki.oids.data) { | |
var error = new Error('Only PKCS#12 PFX in password integrity mode supported.'); | |
error.oid = asn1.derToOid(capture.contentType); | |
throw error; | |
} | |
var data = capture.content.value[0]; | |
if(data.tagClass !== asn1.Class.UNIVERSAL || | |
data.type !== asn1.Type.OCTETSTRING) { | |
throw new Error('PKCS#12 authSafe content data is not an OCTET STRING.'); | |
} | |
data = _decodePkcs7Data(data); | |
// check for MAC | |
if(capture.mac) { | |
var md = null; | |
var macKeyBytes = 0; | |
var macAlgorithm = asn1.derToOid(capture.macAlgorithm); | |
switch(macAlgorithm) { | |
case pki.oids.sha1: | |
md = forge.md.sha1.create(); | |
macKeyBytes = 20; | |
break; | |
case pki.oids.sha256: | |
md = forge.md.sha256.create(); | |
macKeyBytes = 32; | |
break; | |
case pki.oids.sha384: | |
md = forge.md.sha384.create(); | |
macKeyBytes = 48; | |
break; | |
case pki.oids.sha512: | |
md = forge.md.sha512.create(); | |
macKeyBytes = 64; | |
break; | |
case pki.oids.md5: | |
md = forge.md.md5.create(); | |
macKeyBytes = 16; | |
break; | |
} | |
if(md === null) { | |
throw new Error('PKCS#12 uses unsupported MAC algorithm: ' + macAlgorithm); | |
} | |
// verify MAC (iterations default to 1) | |
var macSalt = new forge.util.ByteBuffer(capture.macSalt); | |
var macIterations = (('macIterations' in capture) ? | |
parseInt(forge.util.bytesToHex(capture.macIterations), 16) : 1); | |
var macKey = p12.generateKey( | |
password, macSalt, 3, macIterations, macKeyBytes, md); | |
var mac = forge.hmac.create(); | |
mac.start(md, macKey); | |
mac.update(data.value); | |
var macValue = mac.getMac(); | |
if(macValue.getBytes() !== capture.macDigest) { | |
throw new Error('PKCS#12 MAC could not be verified. Invalid password?'); | |
} | |
} | |
_decodeAuthenticatedSafe(pfx, data.value, strict, password); | |
return pfx; | |
}; | |
/** | |
* Decodes PKCS#7 Data. PKCS#7 (RFC 2315) defines "Data" as an OCTET STRING, | |
* but it is sometimes an OCTET STRING that is composed/constructed of chunks, | |
* each its own OCTET STRING. This is BER-encoding vs. DER-encoding. This | |
* function transforms this corner-case into the usual simple, | |
* non-composed/constructed OCTET STRING. | |
* | |
* This function may be moved to ASN.1 at some point to better deal with | |
* more BER-encoding issues, should they arise. | |
* | |
* @param data the ASN.1 Data object to transform. | |
*/ | |
function _decodePkcs7Data(data) { | |
// handle special case of "chunked" data content: an octet string composed | |
// of other octet strings | |
if(data.composed || data.constructed) { | |
var value = forge.util.createBuffer(); | |
for(var i = 0; i < data.value.length; ++i) { | |
value.putBytes(data.value[i].value); | |
} | |
data.composed = data.constructed = false; | |
data.value = value.getBytes(); | |
} | |
return data; | |
} | |
/** | |
* Decode PKCS#12 AuthenticatedSafe (BER encoded) into PFX object. | |
* | |
* The AuthenticatedSafe is a BER-encoded SEQUENCE OF ContentInfo. | |
* | |
* @param pfx The PKCS#12 PFX object to fill. | |
* @param {String} authSafe BER-encoded AuthenticatedSafe. | |
* @param strict true to use strict DER decoding, false not to. | |
* @param {String} password Password to decrypt with (optional). | |
*/ | |
function _decodeAuthenticatedSafe(pfx, authSafe, strict, password) { | |
authSafe = asn1.fromDer(authSafe, strict); /* actually it's BER encoded */ | |
if(authSafe.tagClass !== asn1.Class.UNIVERSAL || | |
authSafe.type !== asn1.Type.SEQUENCE || | |
authSafe.constructed !== true) { | |
throw new Error('PKCS#12 AuthenticatedSafe expected to be a ' + | |
'SEQUENCE OF ContentInfo'); | |
} | |
for(var i = 0; i < authSafe.value.length; i++) { | |
var contentInfo = authSafe.value[i]; | |
// validate contentInfo and capture data | |
var capture = {}; | |
var errors = []; | |
if(!asn1.validate(contentInfo, contentInfoValidator, capture, errors)) { | |
var error = new Error('Cannot read ContentInfo.'); | |
error.errors = errors; | |
throw error; | |
} | |
var obj = { | |
encrypted: false | |
}; | |
var safeContents = null; | |
var data = capture.content.value[0]; | |
switch(asn1.derToOid(capture.contentType)) { | |
case pki.oids.data: | |
if(data.tagClass !== asn1.Class.UNIVERSAL || | |
data.type !== asn1.Type.OCTETSTRING) { | |
throw new Error('PKCS#12 SafeContents Data is not an OCTET STRING.'); | |
} | |
safeContents = _decodePkcs7Data(data).value; | |
break; | |
case pki.oids.encryptedData: | |
safeContents = _decryptSafeContents(data, password); | |
obj.encrypted = true; | |
break; | |
default: | |
var error = new Error('Unsupported PKCS#12 contentType.'); | |
error.contentType = asn1.derToOid(capture.contentType); | |
throw error; | |
} | |
obj.safeBags = _decodeSafeContents(safeContents, strict, password); | |
pfx.safeContents.push(obj); | |
} | |
} | |
/** | |
* Decrypt PKCS#7 EncryptedData structure. | |
* | |
* @param data ASN.1 encoded EncryptedContentInfo object. | |
* @param password The user-provided password. | |
* | |
* @return The decrypted SafeContents (ASN.1 object). | |
*/ | |
function _decryptSafeContents(data, password) { | |
var capture = {}; | |
var errors = []; | |
if(!asn1.validate( | |
data, forge.pkcs7.asn1.encryptedDataValidator, capture, errors)) { | |
var error = new Error('Cannot read EncryptedContentInfo.'); | |
error.errors = errors; | |
throw error; | |
} | |
var oid = asn1.derToOid(capture.contentType); | |
if(oid !== pki.oids.data) { | |
var error = new Error( | |
'PKCS#12 EncryptedContentInfo ContentType is not Data.'); | |
error.oid = oid; | |
throw error; | |
} | |
// get cipher | |
oid = asn1.derToOid(capture.encAlgorithm); | |
var cipher = pki.pbe.getCipher(oid, capture.encParameter, password); | |
// get encrypted data | |
var encryptedContentAsn1 = _decodePkcs7Data(capture.encryptedContentAsn1); | |
var encrypted = forge.util.createBuffer(encryptedContentAsn1.value); | |
cipher.update(encrypted); | |
if(!cipher.finish()) { | |
throw new Error('Failed to decrypt PKCS#12 SafeContents.'); | |
} | |
return cipher.output.getBytes(); | |
} | |
/** | |
* Decode PKCS#12 SafeContents (BER-encoded) into array of Bag objects. | |
* | |
* The safeContents is a BER-encoded SEQUENCE OF SafeBag. | |
* | |
* @param {String} safeContents BER-encoded safeContents. | |
* @param strict true to use strict DER decoding, false not to. | |
* @param {String} password Password to decrypt with (optional). | |
* | |
* @return {Array} Array of Bag objects. | |
*/ | |
function _decodeSafeContents(safeContents, strict, password) { | |
// if strict and no safe contents, return empty safes | |
if(!strict && safeContents.length === 0) { | |
return []; | |
} | |
// actually it's BER-encoded | |
safeContents = asn1.fromDer(safeContents, strict); | |
if(safeContents.tagClass !== asn1.Class.UNIVERSAL || | |
safeContents.type !== asn1.Type.SEQUENCE || | |
safeContents.constructed !== true) { | |
throw new Error( | |
'PKCS#12 SafeContents expected to be a SEQUENCE OF SafeBag.'); | |
} | |
var res = []; | |
for(var i = 0; i < safeContents.value.length; i++) { | |
var safeBag = safeContents.value[i]; | |
// validate SafeBag and capture data | |
var capture = {}; | |
var errors = []; | |
if(!asn1.validate(safeBag, safeBagValidator, capture, errors)) { | |
var error = new Error('Cannot read SafeBag.'); | |
error.errors = errors; | |
throw error; | |
} | |
/* Create bag object and push to result array. */ | |
var bag = { | |
type: asn1.derToOid(capture.bagId), | |
attributes: _decodeBagAttributes(capture.bagAttributes) | |
}; | |
res.push(bag); | |
var validator, decoder; | |
var bagAsn1 = capture.bagValue.value[0]; | |
switch(bag.type) { | |
case pki.oids.pkcs8ShroudedKeyBag: | |
/* bagAsn1 has a EncryptedPrivateKeyInfo, which we need to decrypt. | |
Afterwards we can handle it like a keyBag, | |
which is a PrivateKeyInfo. */ | |
bagAsn1 = pki.decryptPrivateKeyInfo(bagAsn1, password); | |
if(bagAsn1 === null) { | |
throw new Error( | |
'Unable to decrypt PKCS#8 ShroudedKeyBag, wrong password?'); | |
} | |
/* fall through */ | |
case pki.oids.keyBag: | |
/* A PKCS#12 keyBag is a simple PrivateKeyInfo as understood by our | |
PKI module, hence we don't have to do validation/capturing here, | |
just pass what we already got. */ | |
try { | |
bag.key = pki.privateKeyFromAsn1(bagAsn1); | |
} catch(e) { | |
// ignore unknown key type, pass asn1 value | |
bag.key = null; | |
bag.asn1 = bagAsn1; | |
} | |
continue; /* Nothing more to do. */ | |
case pki.oids.certBag: | |
/* A PKCS#12 certBag can wrap both X.509 and sdsi certificates. | |
Therefore put the SafeBag content through another validator to | |
capture the fields. Afterwards check & store the results. */ | |
validator = certBagValidator; | |
decoder = function() { | |
if(asn1.derToOid(capture.certId) !== pki.oids.x509Certificate) { | |
var error = new Error( | |
'Unsupported certificate type, only X.509 supported.'); | |
error.oid = asn1.derToOid(capture.certId); | |
throw error; | |
} | |
// true=produce cert hash | |
var certAsn1 = asn1.fromDer(capture.cert, strict); | |
try { | |
bag.cert = pki.certificateFromAsn1(certAsn1, true); | |
} catch(e) { | |
// ignore unknown cert type, pass asn1 value | |
bag.cert = null; | |
bag.asn1 = certAsn1; | |
} | |
}; | |
break; | |
default: | |
var error = new Error('Unsupported PKCS#12 SafeBag type.'); | |
error.oid = bag.type; | |
throw error; | |
} | |
/* Validate SafeBag value (i.e. CertBag, etc.) and capture data if needed. */ | |
if(validator !== undefined && | |
!asn1.validate(bagAsn1, validator, capture, errors)) { | |
var error = new Error('Cannot read PKCS#12 ' + validator.name); | |
error.errors = errors; | |
throw error; | |
} | |
/* Call decoder function from above to store the results. */ | |
decoder(); | |
} | |
return res; | |
} | |
/** | |
* Decode PKCS#12 SET OF PKCS12Attribute into JavaScript object. | |
* | |
* @param attributes SET OF PKCS12Attribute (ASN.1 object). | |
* | |
* @return the decoded attributes. | |
*/ | |
function _decodeBagAttributes(attributes) { | |
var decodedAttrs = {}; | |
if(attributes !== undefined) { | |
for(var i = 0; i < attributes.length; ++i) { | |
var capture = {}; | |
var errors = []; | |
if(!asn1.validate(attributes[i], attributeValidator, capture, errors)) { | |
var error = new Error('Cannot read PKCS#12 BagAttribute.'); | |
error.errors = errors; | |
throw error; | |
} | |
var oid = asn1.derToOid(capture.oid); | |
if(pki.oids[oid] === undefined) { | |
// unsupported attribute type, ignore. | |
continue; | |
} | |
decodedAttrs[pki.oids[oid]] = []; | |
for(var j = 0; j < capture.values.length; ++j) { | |
decodedAttrs[pki.oids[oid]].push(capture.values[j].value); | |
} | |
} | |
} | |
return decodedAttrs; | |
} | |
/** | |
* Wraps a private key and certificate in a PKCS#12 PFX wrapper. If a | |
* password is provided then the private key will be encrypted. | |
* | |
* An entire certificate chain may also be included. To do this, pass | |
* an array for the "cert" parameter where the first certificate is | |
* the one that is paired with the private key and each subsequent one | |
* verifies the previous one. The certificates may be in PEM format or | |
* have been already parsed by Forge. | |
* | |
* @todo implement password-based-encryption for the whole package | |
* | |
* @param key the private key. | |
* @param cert the certificate (may be an array of certificates in order | |
* to specify a certificate chain). | |
* @param password the password to use, null for none. | |
* @param options: | |
* algorithm the encryption algorithm to use | |
* ('aes128', 'aes192', 'aes256', '3des'), defaults to 'aes128'. | |
* count the iteration count to use. | |
* saltSize the salt size to use. | |
* useMac true to include a MAC, false not to, defaults to true. | |
* localKeyId the local key ID to use, in hex. | |
* friendlyName the friendly name to use. | |
* generateLocalKeyId true to generate a random local key ID, | |
* false not to, defaults to true. | |
* | |
* @return the PKCS#12 PFX ASN.1 object. | |
*/ | |
p12.toPkcs12Asn1 = function(key, cert, password, options) { | |
// set default options | |
options = options || {}; | |
options.saltSize = options.saltSize || 8; | |
options.count = options.count || 2048; | |
options.algorithm = options.algorithm || options.encAlgorithm || 'aes128'; | |
if(!('useMac' in options)) { | |
options.useMac = true; | |
} | |
if(!('localKeyId' in options)) { | |
options.localKeyId = null; | |
} | |
if(!('generateLocalKeyId' in options)) { | |
options.generateLocalKeyId = true; | |
} | |
var localKeyId = options.localKeyId; | |
var bagAttrs; | |
if(localKeyId !== null) { | |
localKeyId = forge.util.hexToBytes(localKeyId); | |
} else if(options.generateLocalKeyId) { | |
// use SHA-1 of paired cert, if available | |
if(cert) { | |
var pairedCert = forge.util.isArray(cert) ? cert[0] : cert; | |
if(typeof pairedCert === 'string') { | |
pairedCert = pki.certificateFromPem(pairedCert); | |
} | |
var sha1 = forge.md.sha1.create(); | |
sha1.update(asn1.toDer(pki.certificateToAsn1(pairedCert)).getBytes()); | |
localKeyId = sha1.digest().getBytes(); | |
} else { | |
// FIXME: consider using SHA-1 of public key (which can be generated | |
// from private key components), see: cert.generateSubjectKeyIdentifier | |
// generate random bytes | |
localKeyId = forge.random.getBytes(20); | |
} | |
} | |
var attrs = []; | |
if(localKeyId !== null) { | |
attrs.push( | |
// localKeyID | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// attrId | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(pki.oids.localKeyId).getBytes()), | |
// attrValues | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [ | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, | |
localKeyId) | |
]) | |
])); | |
} | |
if('friendlyName' in options) { | |
attrs.push( | |
// friendlyName | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// attrId | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(pki.oids.friendlyName).getBytes()), | |
// attrValues | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [ | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.BMPSTRING, false, | |
options.friendlyName) | |
]) | |
])); | |
} | |
if(attrs.length > 0) { | |
bagAttrs = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, attrs); | |
} | |
// collect contents for AuthenticatedSafe | |
var contents = []; | |
// create safe bag(s) for certificate chain | |
var chain = []; | |
if(cert !== null) { | |
if(forge.util.isArray(cert)) { | |
chain = cert; | |
} else { | |
chain = [cert]; | |
} | |
} | |
var certSafeBags = []; | |
for(var i = 0; i < chain.length; ++i) { | |
// convert cert from PEM as necessary | |
cert = chain[i]; | |
if(typeof cert === 'string') { | |
cert = pki.certificateFromPem(cert); | |
} | |
// SafeBag | |
var certBagAttrs = (i === 0) ? bagAttrs : undefined; | |
var certAsn1 = pki.certificateToAsn1(cert); | |
var certSafeBag = | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// bagId | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(pki.oids.certBag).getBytes()), | |
// bagValue | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ | |
// CertBag | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// certId | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(pki.oids.x509Certificate).getBytes()), | |
// certValue (x509Certificate) | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ | |
asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, | |
asn1.toDer(certAsn1).getBytes()) | |
])])]), | |
// bagAttributes (OPTIONAL) | |
certBagAttrs | |
]); | |
certSafeBags.push(certSafeBag); | |
} | |
if(certSafeBags.length > 0) { | |
// SafeContents | |
var certSafeContents = asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, certSafeBags); | |
// ContentInfo | |
var certCI = | |
// PKCS#7 ContentInfo | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// contentType | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
// OID for the content type is 'data' | |
asn1.oidToDer(pki.oids.data).getBytes()), | |
// content | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ | |
asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, | |
asn1.toDer(certSafeContents).getBytes()) | |
]) | |
]); | |
contents.push(certCI); | |
} | |
// create safe contents for private key | |
var keyBag = null; | |
if(key !== null) { | |
// SafeBag | |
var pkAsn1 = pki.wrapRsaPrivateKey(pki.privateKeyToAsn1(key)); | |
if(password === null) { | |
// no encryption | |
keyBag = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// bagId | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(pki.oids.keyBag).getBytes()), | |
// bagValue | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ | |
// PrivateKeyInfo | |
pkAsn1 | |
]), | |
// bagAttributes (OPTIONAL) | |
bagAttrs | |
]); | |
} else { | |
// encrypted PrivateKeyInfo | |
keyBag = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// bagId | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(pki.oids.pkcs8ShroudedKeyBag).getBytes()), | |
// bagValue | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ | |
// EncryptedPrivateKeyInfo | |
pki.encryptPrivateKeyInfo(pkAsn1, password, options) | |
]), | |
// bagAttributes (OPTIONAL) | |
bagAttrs | |
]); | |
} | |
// SafeContents | |
var keySafeContents = | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [keyBag]); | |
// ContentInfo | |
var keyCI = | |
// PKCS#7 ContentInfo | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// contentType | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
// OID for the content type is 'data' | |
asn1.oidToDer(pki.oids.data).getBytes()), | |
// content | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ | |
asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, | |
asn1.toDer(keySafeContents).getBytes()) | |
]) | |
]); | |
contents.push(keyCI); | |
} | |
// create AuthenticatedSafe by stringing together the contents | |
var safe = asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, contents); | |
var macData; | |
if(options.useMac) { | |
// MacData | |
var sha1 = forge.md.sha1.create(); | |
var macSalt = new forge.util.ByteBuffer( | |
forge.random.getBytes(options.saltSize)); | |
var count = options.count; | |
// 160-bit key | |
var key = p12.generateKey(password, macSalt, 3, count, 20); | |
var mac = forge.hmac.create(); | |
mac.start(sha1, key); | |
mac.update(asn1.toDer(safe).getBytes()); | |
var macValue = mac.getMac(); | |
macData = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// mac DigestInfo | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// digestAlgorithm | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// algorithm = SHA-1 | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
asn1.oidToDer(pki.oids.sha1).getBytes()), | |
// parameters = Null | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') | |
]), | |
// digest | |
asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, | |
false, macValue.getBytes()) | |
]), | |
// macSalt OCTET STRING | |
asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, macSalt.getBytes()), | |
// iterations INTEGER (XXX: Only support count < 65536) | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, | |
asn1.integerToDer(count).getBytes() | |
) | |
]); | |
} | |
// PFX | |
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// version (3) | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false, | |
asn1.integerToDer(3).getBytes()), | |
// PKCS#7 ContentInfo | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ | |
// contentType | |
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, | |
// OID for the content type is 'data' | |
asn1.oidToDer(pki.oids.data).getBytes()), | |
// content | |
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ | |
asn1.create( | |
asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, | |
asn1.toDer(safe).getBytes()) | |
]) | |
]), | |
macData | |
]); | |
}; | |
/** | |
* Derives a PKCS#12 key. | |
* | |
* @param password the password to derive the key material from, null or | |
* undefined for none. | |
* @param salt the salt, as a ByteBuffer, to use. | |
* @param id the PKCS#12 ID byte (1 = key material, 2 = IV, 3 = MAC). | |
* @param iter the iteration count. | |
* @param n the number of bytes to derive from the password. | |
* @param md the message digest to use, defaults to SHA-1. | |
* | |
* @return a ByteBuffer with the bytes derived from the password. | |
*/ | |
p12.generateKey = forge.pbe.generatePkcs12Key; | |