Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions lib/xmlenc.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,16 @@ function decrypt(xml, options, callback) {
var doc = typeof xml === 'string' ? new xmldom.DOMParser().parseFromString(xml) : xml;

var symmetricKey = decryptKeyInfo(doc, options);
var encryptionMethod = dom.byPath(doc, ['EncryptedData', 'EncryptionMethod']);
// Same EncryptedData that decryptKeyInfo resolved the key for.
var encryptedData = dom.byPath(doc, ['EncryptedData']);
var encryptionMethod = dom.child(encryptedData, 'EncryptionMethod');
var encryptionAlgorithm = encryptionMethod.getAttribute('Algorithm');

if (options.disallowDecryptionWithInsecureAlgorithm !== false
&& insecureAlgorithms.indexOf(encryptionAlgorithm) >= 0) {
return callback(new Error('encryption algorithm ' + encryptionAlgorithm + ' is not secure, fail to decrypt'));
}
var encryptedContent = dom.byPath(doc, ['EncryptedData', 'CipherData', 'CipherValue']);
var encryptedContent = dom.child(dom.child(encryptedData, 'CipherData'), 'CipherValue');

var encrypted = Buffer.from(encryptedContent.textContent, 'base64');
switch (encryptionAlgorithm) {
Expand Down Expand Up @@ -294,18 +296,27 @@ function decryptKeyInfo(doc, options) {
// element once and read both the EncryptionMethod and the ciphertext from it:
// the two describe the same key, and taking them from one parent is what
// keeps them consistent.
var encryptedKeyElement = dom.byPath(doc, ['KeyInfo', 'EncryptedKey']);
// Resolve the EncryptedData once so the key comes from the same block that
// is decrypted. With no EncryptedData the input is a bare KeyInfo.
var encryptedData = dom.byPath(doc, ['EncryptedData']);
var keyInfo = encryptedData ? dom.child(encryptedData, 'KeyInfo') : dom.byPath(doc, ['KeyInfo']);
var encryptedKeyElement = dom.child(keyInfo, 'EncryptedKey');

if (!encryptedKeyElement) { // try with EncryptedData->KeyInfo->RetrievalMethod
var keyRetrievalMethod = dom.byPath(doc, ['EncryptedData', 'KeyInfo', 'RetrievalMethod']);
var keyRetrievalMethod = dom.child(keyInfo, 'RetrievalMethod');
var keyRetrievalMethodUri = keyRetrievalMethod ? keyRetrievalMethod.getAttribute('URI') : null;
if (keyRetrievalMethodUri) {
// A same-document reference only. An external URL or a bare fragment is
// not something this resolves.
if (keyRetrievalMethodUri.charAt(0) !== '#' || keyRetrievalMethodUri.length < 2) {
throw new Error('RetrievalMethod URI must be a same-document reference');
}
encryptedKeyElement = dom.elementById(doc, 'EncryptedKey', keyRetrievalMethodUri.substring(1));
// The EncryptedKey may be a sibling of EncryptedData (e.g. Okta), so
// search the enclosing element (EncryptedAssertion) rather than only
// the EncryptedData subtree.
var parent = encryptedData && encryptedData.parentNode;
var scope = parent && parent.nodeType === 1 ? parent : doc;
encryptedKeyElement = dom.elementById(scope, 'EncryptedKey', keyRetrievalMethodUri.substring(1));
}
}

Expand Down
22 changes: 22 additions & 0 deletions test/test-okta-sibling-key.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response Destination="https://example.com/login/callback" ID="id0000000000000000000000000001" Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
<saml2:EncryptedAssertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<xenc:EncryptedData Id="_00000000000000000000000000000001" Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"/>
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:RetrievalMethod Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey" URI="#_00000000000000000000000000000002"/>
</ds:KeyInfo>
<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:CipherValue>vsSeiTCI4zf0LRuFBxUmIhyhbPO6kVN152a3rMPGeNE4I5WrYzbd0JRUTAl6fXKF</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
<xenc:EncryptedKey Id="_00000000000000000000000000000002" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>
</xenc:EncryptionMethod>
<xenc:CipherData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:CipherValue>JhwBazYQHN81PuOKRbHt4lbCttx6F0BZvjeCGrU7tziGi5ncMwd0ld2kcvYaOwncgVWv3nPCVJGGtoJUILg8z71mZJXuoz3MRufQ0G1TACKOAKbWhvI5ZIDEOAzuZzr385zKrNV7Zs7yTGAV/RezogddIUD6e/UokXQgvOcByeOh+2xAP8QLWqrBgrFwSIr+HKwEp3T921y7EHjS5SzRXzjubwXw8PTZlwS/nj8VJ9IBL3yqjEEaX0fsPoCgbmNnJddxW7g9SVaI/1YmAkHGMlLx2ruY++UQDL/uz9Y0Dm91IMF0ZRLA0LHSjHDJvdu2FckWkwM98JpjYQnfopSS8w==</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedKey>
</saml2:EncryptedAssertion>
</saml2p:Response>
88 changes: 88 additions & 0 deletions test/xmlenc.digest.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,3 +603,91 @@ describe('OAEPparams', function () {
});
});
});

describe('RetrievalMethod to a sibling EncryptedKey (Okta layout)', function () {
var xmldom = require('@xmldom/xmldom');
var options = {
rsa_pub: fs.readFileSync(__dirname + '/test-auth0_rsa.pub'),
pem: fs.readFileSync(__dirname + '/test-auth0.pem'),
encryptionAlgorithm: 'http://www.w3.org/2009/xmlenc11#aes256-gcm',
keyEncryptionAlgorithm: RSA_OAEP,
keyEncryptionDigest: 'sha256'
};
var decryptOptions = { key: fs.readFileSync(__dirname + '/test-auth0.key') };

// Wraps the result in an EncryptedAssertion, with the EncryptedKey placed
// as a sibling of EncryptedData and referenced by RetrievalMethod.
function siblingLayout(result, wrap) {
var m = /<e:EncryptedKey[\s\S]*<\/e:EncryptedKey>/.exec(result);
assert(m, 'expected an EncryptedKey element');
var encryptedKey = m[0].replace('<e:EncryptedKey', '<e:EncryptedKey Id="ek1"');
var encryptedData = result.replace(m[0], '<RetrievalMethod URI="#ek1" />');
return wrap(encryptedData, encryptedKey);
}

function encryptedDataOf(xml) {
var doc = new xmldom.DOMParser().parseFromString(xml);
return doc.getElementsByTagNameNS('http://www.w3.org/2001/04/xmlenc#', 'EncryptedData')[0];
}

it('resolves the key from the EncryptedData parent', function (done) {
xmlenc.encrypt('sibling content', options, function (err, result) {
if (err) return done(err);
var xml = siblingLayout(result, function (data, key) {
return '<saml2:EncryptedAssertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:e="http://www.w3.org/2001/04/xmlenc#">' +
data + key + '</saml2:EncryptedAssertion>';
});
var element = encryptedDataOf(xml);
assert.equal(element.parentNode.localName, 'EncryptedAssertion');
xmlenc.decrypt(element, decryptOptions, function (err2, decrypted) {
if (err2) return done(err2);
assert.equal(decrypted, 'sibling content');
done();
});
});
});

it('finds the key in an Okta-shaped response (fails at the RSA unwrap, not the lookup)', function () {
// Okta layout with all identifiers and ciphertext replaced by dummies, so the
// key cannot unwrap; failing at the RSA step proves the key was located.
var xml = fs.readFileSync(__dirname + '/test-okta-sibling-key.xml', 'utf8');
var element = encryptedDataOf(xml);
assert.throws(function () {
xmlenc.decryptKeyInfo(element, decryptOptions);
}, function (err) {
return !/cant find encryption algorithm/.test(err.message);
});
});

it('uses the key of the EncryptedData being decrypted when another block has an inline key', function (done) {
xmlenc.encrypt('first block', options, function (err, first) {
if (err) return done(err);
xmlenc.encrypt('second block', options, function (err2, second) {
if (err2) return done(err2);
var xml = siblingLayout(first, function (data, key) {
return '<Root xmlns:e="http://www.w3.org/2001/04/xmlenc#">' + data + key + second + '</Root>';
});
var doc = new xmldom.DOMParser().parseFromString(xml);
xmlenc.decrypt(doc, decryptOptions, function (err3, decrypted) {
if (err3) return done(err3);
assert.equal(decrypted, 'first block');
done();
});
});
});
});

it('does not resolve an EncryptedKey outside the EncryptedData parent', function (done) {
xmlenc.encrypt('sibling content', options, function (err, result) {
if (err) return done(err);
var xml = siblingLayout(result, function (data, key) {
return '<Root xmlns:e="http://www.w3.org/2001/04/xmlenc#"><A>' + data + '</A><B>' + key + '</B></Root>';
});
xmlenc.decrypt(encryptedDataOf(xml), decryptOptions, function (err2) {
assert(err2, 'expected failure');
assert.match(err2.message, /cant find encryption algorithm/);
done();
});
});
});
});
21 changes: 21 additions & 0 deletions test/xmlenc.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,25 @@ describe('integration', function() {
}
);
});

// passport-wsfed-saml2 passes only the EncryptedData element, while Okta
// places the referenced EncryptedKey beside it under EncryptedAssertion.
it('should decrypt Okta assertion when given only the EncryptedData element', function (done) {
var doc = new xmldom.DOMParser().parseFromString(
fs.readFileSync(__dirname + '/test-okta-enc-response.xml').toString()
);
var encryptedData = doc.getElementsByTagNameNS('http://www.w3.org/2001/04/xmlenc#', 'EncryptedData')[0]
|| doc.getElementsByTagName('EncryptedData')[0];
assert.equal(encryptedData.parentNode.localName, 'EncryptedAssertion');

xmlenc.decrypt(
encryptedData,
{key: fs.readFileSync(__dirname + '/test-okta.pem'), disallowDecryptionWithInsecureAlgorithm: false},
(err, res) => {
assert.ifError(err);
assert(/Assertion/.test(res));
done();
}
);
});
});
Loading