2013-06-10 11:57:33 -04:00
|
|
|
|
/**
|
|
|
|
|
* Javascript implementation of PKCS#12.
|
|
|
|
|
*
|
|
|
|
|
* @author Dave Longley
|
|
|
|
|
* @author Stefan Siegl <stesie@brokenpipe.de>
|
|
|
|
|
*
|
|
|
|
|
* Copyright (c) 2010-2013 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
|
|
|
|
|
* }
|
|
|
|
|
*/
|
|
|
|
|
(function() {
|
|
|
|
|
/* ########## Begin module implementation ########## */
|
|
|
|
|
function initModule(forge) {
|
|
|
|
|
|
|
|
|
|
// shortcut for asn.1 & PKI API
|
|
|
|
|
var asn1 = forge.asn1;
|
|
|
|
|
var pki = forge.pki;
|
|
|
|
|
|
|
|
|
|
// shortcut for PKCS#12 API
|
|
|
|
|
var p12 = 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.
|
|
|
|
|
*
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* @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.
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(bag.attributes[attrName] !== undefined &&
|
2013-08-05 10:45:02 -04:00
|
|
|
|
bag.attributes[attrName].indexOf(attrValue) >= 0) {
|
2013-06-10 11:57:33 -04:00
|
|
|
|
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.
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* @param strict true to use strict DER decoding, false not to (default: true).
|
|
|
|
|
* @param {String} password Password to decrypt with (optional).
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*
|
|
|
|
|
* @return PKCS#12 PFX object.
|
|
|
|
|
*/
|
2013-08-05 10:45:02 -04:00
|
|
|
|
p12.pkcs12FromAsn1 = function(obj, strict, password) {
|
|
|
|
|
// handle args
|
|
|
|
|
if(typeof strict === 'string') {
|
|
|
|
|
password = strict;
|
|
|
|
|
strict = true;
|
|
|
|
|
}
|
|
|
|
|
else if(strict === undefined) {
|
|
|
|
|
strict = true;
|
|
|
|
|
}
|
|
|
|
|
|
2013-06-10 11:57:33 -04:00
|
|
|
|
// validate PFX and capture data
|
|
|
|
|
var capture = {};
|
|
|
|
|
var errors = [];
|
|
|
|
|
if(!asn1.validate(obj, pfxValidator, capture, errors)) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Cannot read PKCS#12 PFX. ' +
|
|
|
|
|
'ASN.1 object is not an PKCS#12 PFX.',
|
|
|
|
|
errors: errors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pfx = {
|
|
|
|
|
version: capture.version.charCodeAt(0),
|
|
|
|
|
safeContents: [],
|
|
|
|
|
|
|
|
|
|
/**
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* 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.
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* @return a map of attribute type to an array of matching bags.
|
|
|
|
|
*/
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*/
|
|
|
|
|
getBagsByFriendlyName: function(friendlyName, bagType) {
|
2013-08-05 10:45:02 -04:00
|
|
|
|
return _getBagsByAttribute(
|
|
|
|
|
pfx.safeContents, 'friendlyName', friendlyName, bagType);
|
2013-06-10 11:57:33 -04:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* 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.
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* @return an array of bags with matching localKeyId attribute.
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*/
|
|
|
|
|
getBagsByLocalKeyId: function(localKeyId, bagType) {
|
2013-08-05 10:45:02 -04:00
|
|
|
|
return _getBagsByAttribute(
|
|
|
|
|
pfx.safeContents, 'localKeyId', localKeyId, bagType);
|
2013-06-10 11:57:33 -04:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if(capture.version.charCodeAt(0) !== 3) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'PKCS#12 PFX of version other than 3 not supported.',
|
|
|
|
|
version: capture.version.charCodeAt(0)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(asn1.derToOid(capture.contentType) !== pki.oids.data) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Only PKCS#12 PFX in password integrity mode supported.',
|
|
|
|
|
oid: asn1.derToOid(capture.contentType)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var data = capture.content.value[0];
|
|
|
|
|
if(data.tagClass !== asn1.Class.UNIVERSAL ||
|
|
|
|
|
data.type !== asn1.Type.OCTETSTRING) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'PKCS#12 authSafe content data is not an OCTET STRING.'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
message: '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 {
|
|
|
|
|
message: 'PKCS#12 MAC could not be verified. Invalid password?'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-08-05 10:45:02 -04:00
|
|
|
|
_decodeAuthenticatedSafe(pfx, data.value, strict, password);
|
2013-06-10 11:57:33 -04:00
|
|
|
|
return pfx;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* @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).
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*/
|
2013-08-05 10:45:02 -04:00
|
|
|
|
function _decodeAuthenticatedSafe(pfx, authSafe, strict, password) {
|
|
|
|
|
authSafe = asn1.fromDer(authSafe, strict); /* actually it's BER encoded */
|
2013-06-10 11:57:33 -04:00
|
|
|
|
|
|
|
|
|
if(authSafe.tagClass !== asn1.Class.UNIVERSAL ||
|
|
|
|
|
authSafe.type !== asn1.Type.SEQUENCE ||
|
|
|
|
|
authSafe.constructed !== true) {
|
|
|
|
|
throw {
|
|
|
|
|
message: '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)) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Cannot read ContentInfo.',
|
|
|
|
|
errors: errors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
message: 'PKCS#12 SafeContents Data is not an OCTET STRING.'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
safeContents = data.value;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case pki.oids.encryptedData:
|
|
|
|
|
if(password === undefined) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Found PKCS#12 Encrypted SafeContents Data but ' +
|
|
|
|
|
'no password available.'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
safeContents = _decryptSafeContents(data, password);
|
|
|
|
|
obj.encrypted = true;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Unsupported PKCS#12 contentType.',
|
|
|
|
|
contentType: asn1.derToOid(capture.contentType)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2013-08-05 10:45:02 -04:00
|
|
|
|
obj.safeBags = _decodeSafeContents(safeContents, strict, password);
|
2013-06-10 11:57:33 -04:00
|
|
|
|
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)) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Cannot read EncryptedContentInfo. ',
|
|
|
|
|
errors: errors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var oid = asn1.derToOid(capture.contentType);
|
|
|
|
|
if(oid !== pki.oids.data) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'PKCS#12 EncryptedContentInfo ContentType is not Data.',
|
|
|
|
|
oid: oid
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// get cipher
|
|
|
|
|
oid = asn1.derToOid(capture.encAlgorithm);
|
|
|
|
|
var cipher = pki.pbe.getCipher(oid, capture.encParameter, password);
|
|
|
|
|
|
|
|
|
|
// get encrypted data
|
|
|
|
|
var encrypted = forge.util.createBuffer(capture.encContent);
|
|
|
|
|
|
|
|
|
|
cipher.update(encrypted);
|
|
|
|
|
if(!cipher.finish()) {
|
|
|
|
|
throw {
|
|
|
|
|
message: '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
|
|
|
|
|
*
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* @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).
|
|
|
|
|
*
|
2013-06-10 11:57:33 -04:00
|
|
|
|
* @return {Array} Array of Bag objects.
|
|
|
|
|
*/
|
2013-08-05 10:45:02 -04:00
|
|
|
|
function _decodeSafeContents(safeContents, strict, password) {
|
|
|
|
|
// actually it's BER-encoded
|
|
|
|
|
safeContents = asn1.fromDer(safeContents, strict);
|
2013-06-10 11:57:33 -04:00
|
|
|
|
|
|
|
|
|
if(safeContents.tagClass !== asn1.Class.UNIVERSAL ||
|
2013-08-05 10:45:02 -04:00
|
|
|
|
safeContents.type !== asn1.Type.SEQUENCE ||
|
|
|
|
|
safeContents.constructed !== true) {
|
2013-06-10 11:57:33 -04:00
|
|
|
|
throw {
|
|
|
|
|
message: '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)) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Cannot read SafeBag.',
|
|
|
|
|
errors: errors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 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. */
|
|
|
|
|
if(password === undefined) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Found PKCS#8 ShroudedKeyBag but no password available.'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bagAsn1 = pki.decryptPrivateKeyInfo(bagAsn1, password);
|
|
|
|
|
if(bagAsn1 === null) {
|
|
|
|
|
throw {
|
|
|
|
|
message: '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. */
|
|
|
|
|
bag.key = pki.privateKeyFromAsn1(bagAsn1);
|
|
|
|
|
continue; /* Nothing more to do. */
|
|
|
|
|
|
|
|
|
|
case pki.oids.certBag:
|
2013-08-05 10:45:02 -04:00
|
|
|
|
/* A PKCS#12 certBag can wrap both X.509 and sdsi certificates.
|
2013-06-10 11:57:33 -04:00
|
|
|
|
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) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Unsupported certificate type, only X.509 supported.',
|
|
|
|
|
oid: asn1.derToOid(capture.certId)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2013-08-05 10:45:02 -04:00
|
|
|
|
// true=produce cert hash
|
2013-06-10 11:57:33 -04:00
|
|
|
|
bag.cert = pki.certificateFromAsn1(
|
2013-08-05 10:45:02 -04:00
|
|
|
|
asn1.fromDer(capture.cert, strict), true);
|
2013-06-10 11:57:33 -04:00
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Unsupported PKCS#12 SafeBag type.',
|
|
|
|
|
oid: bag.type
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Validate SafeBag value (i.e. CertBag, etc.) and capture data if needed. */
|
|
|
|
|
if(validator !== undefined &&
|
|
|
|
|
!asn1.validate(bagAsn1, validator, capture, errors)) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Cannot read PKCS#12 ' + validator.name,
|
|
|
|
|
errors: errors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Call decoder function from above to store the results. */
|
|
|
|
|
decoder();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* Decode PKCS#12 SET OF PKCS12Attribute into JavaScript object.
|
|
|
|
|
*
|
|
|
|
|
* @param attributes SET OF PKCS12Attribute (ASN.1 object).
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* @return the decoded attributes.
|
2013-06-10 11:57:33 -04:00
|
|
|
|
*/
|
|
|
|
|
function _decodeBagAttributes(attributes) {
|
|
|
|
|
var decodedAttrs = {};
|
|
|
|
|
|
|
|
|
|
if(attributes !== undefined) {
|
2013-08-05 10:45:02 -04:00
|
|
|
|
for(var i = 0; i < attributes.length; ++i) {
|
2013-06-10 11:57:33 -04:00
|
|
|
|
var capture = {};
|
|
|
|
|
var errors = [];
|
|
|
|
|
if(!asn1.validate(attributes[i], attributeValidator, capture, errors)) {
|
|
|
|
|
throw {
|
|
|
|
|
message: 'Cannot read PKCS#12 BagAttribute.',
|
|
|
|
|
errors: errors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var oid = asn1.derToOid(capture.oid);
|
|
|
|
|
if(pki.oids[oid] === undefined) {
|
|
|
|
|
// unsupported attribute type, ignore.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2013-08-05 10:45:02 -04:00
|
|
|
|
|
2013-06-10 11:57:33 -04:00
|
|
|
|
decodedAttrs[pki.oids[oid]] = [];
|
2013-08-05 10:45:02 -04:00
|
|
|
|
for(var j = 0; j < capture.values.length; ++j) {
|
2013-06-10 11:57:33 -04:00
|
|
|
|
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.
|
|
|
|
|
* @param options:
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* algorithm the encryption algorithm to use
|
2013-06-10 11:57:33 -04:00
|
|
|
|
* ('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.
|
2013-08-05 10:45:02 -04:00
|
|
|
|
* friendlyName the friendly name to use.
|
2013-06-10 11:57:33 -04:00
|
|
|
|
* 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;
|
2013-08-05 10:45:02 -04:00
|
|
|
|
options.algorithm = options.algorithm || options.encAlgorithm || 'aes128';
|
2013-06-10 11:57:33 -04:00
|
|
|
|
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 = undefined;
|
|
|
|
|
if(localKeyId !== null) {
|
|
|
|
|
localKeyId = forge.util.hexToBytes(localKeyId);
|
|
|
|
|
}
|
|
|
|
|
else if(options.generateLocalKeyId) {
|
2013-08-05 10:45:02 -04:00
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
// FIXME: consider using SHA-1 of public key (which can be generated
|
|
|
|
|
// from private key components)
|
|
|
|
|
// generate random bytes
|
|
|
|
|
else {
|
|
|
|
|
localKeyId = forge.random.getBytes(20);
|
|
|
|
|
}
|
2013-06-10 11:57:33 -04:00
|
|
|
|
}
|
|
|
|
|
|
2013-08-05 10:45:02 -04:00
|
|
|
|
var attrs = [];
|
2013-06-10 11:57:33 -04:00
|
|
|
|
if(localKeyId !== null) {
|
2013-08-05 10:45:02 -04:00
|
|
|
|
attrs.push(
|
2013-06-10 11:57:33 -04:00
|
|
|
|
// 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)
|
|
|
|
|
])
|
2013-08-05 10:45:02 -04:00
|
|
|
|
]));
|
2013-06-10 11:57:33 -04:00
|
|
|
|
}
|
2013-08-05 10:45:02 -04:00
|
|
|
|
if('friendlyName' in options) {
|
|
|
|
|
attrs.push(
|
|
|
|
|
// friendlyName
|
2013-06-10 11:57:33 -04:00
|
|
|
|
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
|
2013-08-05 10:45:02 -04:00
|
|
|
|
// attrId
|
2013-06-10 11:57:33 -04:00
|
|
|
|
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false,
|
2013-08-05 10:45:02 -04:00
|
|
|
|
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)
|
2013-06-10 11:57:33 -04:00
|
|
|
|
])
|
2013-08-05 10:45:02 -04:00
|
|
|
|
]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(attrs.length > 0) {
|
|
|
|
|
bagAttrs = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, attrs);
|
2013-06-10 11:57:33 -04:00
|
|
|
|
}
|
|
|
|
|
|
2013-08-05 10:45:02 -04:00
|
|
|
|
// collect contents for AuthenticatedSafe
|
|
|
|
|
var contents = [];
|
|
|
|
|
|
2013-06-10 11:57:33 -04:00
|
|
|
|
// create safe bag(s) for certificate chain
|
|
|
|
|
var chain = [];
|
|
|
|
|
if(cert !== null) {
|
2013-08-05 10:45:02 -04:00
|
|
|
|
if(forge.util.isArray(cert)) {
|
2013-06-10 11:57:33 -04:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2013-08-05 10:45:02 -04:00
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2013-06-10 11:57:33 -04:00
|
|
|
|
// create AuthenticatedSafe by stringing together the contents
|
|
|
|
|
var safe = asn1.create(
|
|
|
|
|
asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, contents);
|
|
|
|
|
|
|
|
|
|
var macData = undefined;
|
|
|
|
|
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,
|
|
|
|
|
forge.util.hexToBytes(count.toString(16))
|
|
|
|
|
)
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PFX
|
|
|
|
|
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
|
|
|
|
|
// version (3)
|
|
|
|
|
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.INTEGER, false,
|
|
|
|
|
String.fromCharCode(0x03)),
|
|
|
|
|
// 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 {String} password the password to derive the key material from.
|
|
|
|
|
* @param {ByteBuffer} salt the salt to use.
|
|
|
|
|
* @param {int} id the PKCS#12 ID byte (1 = key material, 2 = IV, 3 = MAC).
|
|
|
|
|
* @param {int} iter the iteration count.
|
|
|
|
|
* @param {int} n the number of bytes to derive from the password.
|
|
|
|
|
* @param md the message digest to use, defaults to SHA-1.
|
|
|
|
|
*
|
|
|
|
|
* @return {ByteBuffer} The bytes derived from the password.
|
|
|
|
|
*/
|
|
|
|
|
p12.generateKey = function(password, salt, id, iter, n, md) {
|
|
|
|
|
var j, l;
|
|
|
|
|
|
|
|
|
|
if(typeof md === 'undefined' || md === null) {
|
|
|
|
|
md = forge.md.sha1.create();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var u = md.digestLength;
|
|
|
|
|
var v = md.blockLength;
|
|
|
|
|
var result = new forge.util.ByteBuffer();
|
|
|
|
|
|
|
|
|
|
/* Convert password to Unicode byte buffer + trailing 0-byte. */
|
|
|
|
|
var passBuf = new forge.util.ByteBuffer();
|
|
|
|
|
for(l = 0; l < password.length; l++) {
|
|
|
|
|
passBuf.putInt16(password.charCodeAt(l));
|
|
|
|
|
}
|
|
|
|
|
passBuf.putInt16(0);
|
|
|
|
|
|
|
|
|
|
/* Length of salt and password in BYTES. */
|
|
|
|
|
var p = passBuf.length();
|
|
|
|
|
var s = salt.length();
|
|
|
|
|
|
|
|
|
|
/* 1. Construct a string, D (the "diversifier"), by concatenating
|
|
|
|
|
v copies of ID. */
|
|
|
|
|
var D = new forge.util.ByteBuffer();
|
|
|
|
|
D.fillWithByte(id, v);
|
|
|
|
|
|
|
|
|
|
/* 2. Concatenate copies of the salt together to create a string S of length
|
|
|
|
|
v * ceil(s / v) bytes (the final copy of the salt may be trunacted
|
|
|
|
|
to create S).
|
|
|
|
|
Note that if the salt is the empty string, then so is S. */
|
|
|
|
|
var Slen = v * Math.ceil(s / v);
|
|
|
|
|
var S = new forge.util.ByteBuffer();
|
|
|
|
|
for(l = 0; l < Slen; l ++) {
|
|
|
|
|
S.putByte(salt.at(l % s));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 3. Concatenate copies of the password together to create a string P of
|
|
|
|
|
length v * ceil(p / v) bytes (the final copy of the password may be
|
|
|
|
|
truncated to create P).
|
|
|
|
|
Note that if the password is the empty string, then so is P. */
|
|
|
|
|
var Plen = v * Math.ceil(p / v);
|
|
|
|
|
var P = new forge.util.ByteBuffer();
|
|
|
|
|
for(l = 0; l < Plen; l ++) {
|
|
|
|
|
P.putByte(passBuf.at(l % p));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 4. Set I=S||P to be the concatenation of S and P. */
|
|
|
|
|
var I = S;
|
|
|
|
|
I.putBuffer(P);
|
|
|
|
|
|
|
|
|
|
/* 5. Set c=ceil(n / u). */
|
|
|
|
|
var c = Math.ceil(n / u);
|
|
|
|
|
|
|
|
|
|
/* 6. For i=1, 2, ..., c, do the following: */
|
|
|
|
|
for(var i = 1; i <= c; i ++) {
|
|
|
|
|
/* a) Set Ai=H^r(D||I). (l.e. the rth hash of D||I, H(H(H(...H(D||I)))) */
|
|
|
|
|
var buf = new forge.util.ByteBuffer();
|
|
|
|
|
buf.putBytes(D.bytes());
|
|
|
|
|
buf.putBytes(I.bytes());
|
|
|
|
|
for(var round = 0; round < iter; round ++) {
|
|
|
|
|
md.start();
|
|
|
|
|
md.update(buf.getBytes());
|
|
|
|
|
buf = md.digest();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* b) Concatenate copies of Ai to create a string B of length v bytes (the
|
|
|
|
|
final copy of Ai may be truncated to create B). */
|
|
|
|
|
var B = new forge.util.ByteBuffer();
|
|
|
|
|
for(l = 0; l < v; l ++) {
|
|
|
|
|
B.putByte(buf.at(l % u));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* c) Treating I as a concatenation I0, I1, ..., Ik-1 of v-byte blocks,
|
|
|
|
|
where k=ceil(s / v) + ceil(p / v), modify I by setting
|
|
|
|
|
Ij=(Ij+B+1) mod 2v for each j. */
|
|
|
|
|
var k = Math.ceil(s / v) + Math.ceil(p / v);
|
|
|
|
|
var Inew = new forge.util.ByteBuffer();
|
|
|
|
|
for(j = 0; j < k; j ++) {
|
|
|
|
|
var chunk = new forge.util.ByteBuffer(I.getBytes(v));
|
|
|
|
|
var x = 0x1ff;
|
|
|
|
|
for(l = B.length() - 1; l >= 0; l --) {
|
|
|
|
|
x = x >> 8;
|
|
|
|
|
x += B.at(l) + chunk.at(l);
|
|
|
|
|
chunk.setAt(l, x & 0xff);
|
|
|
|
|
}
|
|
|
|
|
Inew.putBuffer(chunk);
|
|
|
|
|
}
|
|
|
|
|
I = Inew;
|
|
|
|
|
|
|
|
|
|
/* Add Ai to A. */
|
|
|
|
|
result.putBuffer(buf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.truncate(result.length() - n);
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // end module implementation
|
|
|
|
|
|
|
|
|
|
/* ########## Begin module wrapper ########## */
|
|
|
|
|
var name = 'pkcs12';
|
|
|
|
|
if(typeof define !== 'function') {
|
|
|
|
|
// NodeJS -> AMD
|
|
|
|
|
if(typeof module === 'object' && module.exports) {
|
2013-08-05 10:45:02 -04:00
|
|
|
|
var nodeJS = true;
|
|
|
|
|
define = function(ids, factory) {
|
2013-06-10 11:57:33 -04:00
|
|
|
|
factory(require, module);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
// <script>
|
|
|
|
|
else {
|
|
|
|
|
if(typeof forge === 'undefined') {
|
|
|
|
|
forge = {};
|
|
|
|
|
}
|
2013-08-05 10:45:02 -04:00
|
|
|
|
return initModule(forge);
|
2013-06-10 11:57:33 -04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// AMD
|
2013-08-05 10:45:02 -04:00
|
|
|
|
var deps;
|
|
|
|
|
var defineFunc = function(require, module) {
|
|
|
|
|
module.exports = function(forge) {
|
|
|
|
|
var mods = deps.map(function(dep) {
|
|
|
|
|
return require(dep);
|
|
|
|
|
}).concat(initModule);
|
|
|
|
|
// handle circular dependencies
|
|
|
|
|
forge = forge || {};
|
|
|
|
|
forge.defined = forge.defined || {};
|
|
|
|
|
if(forge.defined[name]) {
|
2013-06-10 11:57:33 -04:00
|
|
|
|
return forge[name];
|
2013-08-05 10:45:02 -04:00
|
|
|
|
}
|
|
|
|
|
forge.defined[name] = true;
|
|
|
|
|
for(var i = 0; i < mods.length; ++i) {
|
|
|
|
|
mods[i](forge);
|
|
|
|
|
}
|
|
|
|
|
return forge[name];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
var tmpDefine = define;
|
|
|
|
|
define = function(ids, factory) {
|
|
|
|
|
deps = (typeof ids === 'string') ? factory.slice(2) : ids.slice(2);
|
|
|
|
|
if(nodeJS) {
|
|
|
|
|
delete define;
|
|
|
|
|
return tmpDefine.apply(null, Array.prototype.slice.call(arguments, 0));
|
|
|
|
|
}
|
|
|
|
|
define = tmpDefine;
|
|
|
|
|
return define.apply(null, Array.prototype.slice.call(arguments, 0));
|
|
|
|
|
};
|
|
|
|
|
define([
|
|
|
|
|
'require',
|
|
|
|
|
'module',
|
|
|
|
|
'./asn1',
|
|
|
|
|
'./sha1',
|
|
|
|
|
'./pkcs7asn1',
|
|
|
|
|
'./pki',
|
|
|
|
|
'./util',
|
|
|
|
|
'./random',
|
|
|
|
|
'./hmac'
|
|
|
|
|
], function() {
|
|
|
|
|
defineFunc.apply(null, Array.prototype.slice.call(arguments, 0));
|
|
|
|
|
});
|
2013-06-10 11:57:33 -04:00
|
|
|
|
})();
|