diff --git a/lib/xmlenc.js b/lib/xmlenc.js
index a880744..2b1b212 100644
--- a/lib/xmlenc.js
+++ b/lib/xmlenc.js
@@ -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) {
@@ -294,10 +296,14 @@ 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
@@ -305,7 +311,12 @@ function decryptKeyInfo(doc, options) {
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));
}
}
diff --git a/test/test-okta-sibling-key.xml b/test/test-okta-sibling-key.xml
new file mode 100644
index 0000000..fb58a7f
--- /dev/null
+++ b/test/test-okta-sibling-key.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+ vsSeiTCI4zf0LRuFBxUmIhyhbPO6kVN152a3rMPGeNE4I5WrYzbd0JRUTAl6fXKF
+
+
+
+
+
+
+
+ JhwBazYQHN81PuOKRbHt4lbCttx6F0BZvjeCGrU7tziGi5ncMwd0ld2kcvYaOwncgVWv3nPCVJGGtoJUILg8z71mZJXuoz3MRufQ0G1TACKOAKbWhvI5ZIDEOAzuZzr385zKrNV7Zs7yTGAV/RezogddIUD6e/UokXQgvOcByeOh+2xAP8QLWqrBgrFwSIr+HKwEp3T921y7EHjS5SzRXzjubwXw8PTZlwS/nj8VJ9IBL3yqjEEaX0fsPoCgbmNnJddxW7g9SVaI/1YmAkHGMlLx2ruY++UQDL/uz9Y0Dm91IMF0ZRLA0LHSjHDJvdu2FckWkwM98JpjYQnfopSS8w==
+
+
+
+
diff --git a/test/xmlenc.digest.js b/test/xmlenc.digest.js
index 4af25e5..6f3c7c5 100644
--- a/test/xmlenc.digest.js
+++ b/test/xmlenc.digest.js
@@ -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 = //.exec(result);
+ assert(m, 'expected an EncryptedKey element');
+ var encryptedKey = m[0].replace('');
+ 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 '' +
+ data + key + '';
+ });
+ 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 '' + data + key + second + '';
+ });
+ 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 '' + data + '' + key + '';
+ });
+ xmlenc.decrypt(encryptedDataOf(xml), decryptOptions, function (err2) {
+ assert(err2, 'expected failure');
+ assert.match(err2.message, /cant find encryption algorithm/);
+ done();
+ });
+ });
+ });
+});
diff --git a/test/xmlenc.integration.js b/test/xmlenc.integration.js
index c78dcb6..64fabac 100644
--- a/test/xmlenc.integration.js
+++ b/test/xmlenc.integration.js
@@ -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();
+ }
+ );
+ });
});