Skip to content

Commit

Permalink
Comply with RFC 5753 when computing ECDH keys in EnvelopedData
Browse files Browse the repository at this point in the history
Fixes #334
  • Loading branch information
gnarea committed Oct 28, 2021
1 parent bb3a1c2 commit 03cc200
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 56 deletions.
122 changes: 66 additions & 56 deletions src/EnvelopedData.js
Original file line number Diff line number Diff line change
Expand Up @@ -1384,76 +1384,84 @@ export default class EnvelopedData
);
//endregion
//region Apply KDF function to shared secret
function applyKDF(includeAlgorithmParams) {
includeAlgorithmParams = includeAlgorithmParams || false;

//region Get length of used AES-KW algorithm
const aesKWAlgorithm = new AlgorithmIdentifier({ schema: _this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmParams });

const KWalgorithm = getAlgorithmByOID(aesKWAlgorithm.algorithmId);
if(("name" in KWalgorithm) === false)
return Promise.reject(`Incorrect OID for key encryption algorithm: ${aesKWAlgorithm.algorithmId}`);
//endregion

//region Translate AES-KW length to ArrayBuffer
let kwLength = KWalgorithm.length;

const kwLengthBuffer = new ArrayBuffer(4);
const kwLengthView = new Uint8Array(kwLengthBuffer);

for(let j = 3; j >= 0; j--)
{
kwLengthView[j] = kwLength;
kwLength >>= 8;
}
//endregion

//region Create and encode "ECC-CMS-SharedInfo" structure
const keyInfoAlgorithm = {
algorithmId: aesKWAlgorithm.algorithmId
};
if (includeAlgorithmParams) {
keyInfoAlgorithm.algorithmParams = new asn1js.Null();
}
const eccInfo = new ECCCMSSharedInfo({
keyInfo: new AlgorithmIdentifier(keyInfoAlgorithm),
entityUInfo: _this.recipientInfos[index].value.ukm,
suppPubInfo: new asn1js.OctetString({ valueHex: kwLengthBuffer })
});

const encodedInfo = eccInfo.toSchema().toBER(false);
//endregion

//region Get SHA algorithm used together with ECDH
const ecdhAlgorithm = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
if(("name" in ecdhAlgorithm) === false)
return Promise.reject(`Incorrect OID for key encryption algorithm: ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);
//endregion

return kdf(ecdhAlgorithm.kdf, sharedSecret, KWalgorithm.length, encodedInfo);
}
let sharedSecret;
currentSequence = currentSequence.then(
/**
* @param {ArrayBuffer} result
*/
result =>
{
//region Get length of used AES-KW algorithm
const aesKWAlgorithm = new AlgorithmIdentifier({ schema: _this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmParams });

const KWalgorithm = getAlgorithmByOID(aesKWAlgorithm.algorithmId);
if(("name" in KWalgorithm) === false)
return Promise.reject(`Incorrect OID for key encryption algorithm: ${aesKWAlgorithm.algorithmId}`);
//endregion

//region Translate AES-KW length to ArrayBuffer
let kwLength = KWalgorithm.length;

const kwLengthBuffer = new ArrayBuffer(4);
const kwLengthView = new Uint8Array(kwLengthBuffer);

for(let j = 3; j >= 0; j--)
{
kwLengthView[j] = kwLength;
kwLength >>= 8;
}
//endregion

//region Create and encode "ECC-CMS-SharedInfo" structure
const eccInfo = new ECCCMSSharedInfo({
keyInfo: new AlgorithmIdentifier({
algorithmId: aesKWAlgorithm.algorithmId,
/*
Initially RFC5753 says that AES algorithms have absent parameters.
But since early implementations all put NULL here. Thus, in order to be
"backward compatible", index also put NULL here.
*/
algorithmParams: new asn1js.Null()
}),
entityUInfo: _this.recipientInfos[index].value.ukm,
suppPubInfo: new asn1js.OctetString({ valueHex: kwLengthBuffer })
});

const encodedInfo = eccInfo.toSchema().toBER(false);
//endregion

//region Get SHA algorithm used together with ECDH
const ecdhAlgorithm = getAlgorithmByOID(_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId);
if(("name" in ecdhAlgorithm) === false)
return Promise.reject(`Incorrect OID for key encryption algorithm: ${_this.recipientInfos[index].value.keyEncryptionAlgorithm.algorithmId}`);
//endregion

return kdf(ecdhAlgorithm.kdf, result, KWalgorithm.length, encodedInfo);
sharedSecret = result;
return applyKDF();
},
error =>
Promise.reject(error)
);
//endregion
//region Import AES-KW key from result of KDF function
currentSequence = currentSequence.then(result =>
crypto.importKey("raw",
result,
function importAesKwKey(kdfResult) {
return crypto.importKey("raw",
kdfResult,
{ name: "AES-KW" },
true,
["unwrapKey"]),
error => Promise.reject(error)
["unwrapKey"]
);
}
currentSequence = currentSequence.then(
importAesKwKey,
error => Promise.reject(error)
);
//endregion
//region Finally unwrap session key
currentSequence = currentSequence.then(result =>
{
function unwrapSessionKey(aesKwKey) {
//region Get WebCrypto form of content encryption algorithm
const contentEncryptionAlgorithm = getAlgorithmByOID(_this.encryptedContentInfo.contentEncryptionAlgorithm.algorithmId);
if(("name" in contentEncryptionAlgorithm) === false)
Expand All @@ -1462,13 +1470,15 @@ export default class EnvelopedData

return crypto.unwrapKey("raw",
_this.recipientInfos[index].value.recipientEncryptedKeys.encryptedKeys[0].encryptedKey.valueBlock.valueHex,
result,
aesKwKey,
{ name: "AES-KW" },
contentEncryptionAlgorithm,
true,
["decrypt"]);
}, error =>
Promise.reject(error)
}
currentSequence = currentSequence.then(
result => unwrapSessionKey(result).catch(() => applyKDF(true).then(importAesKwKey).then(unwrapSessionKey)),
error => Promise.reject(error)
);
//endregion

Expand Down
77 changes: 77 additions & 0 deletions test/s_ECCCMSSharedInfo_before_RFC5753.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* eslint-disable no-undef */
import * as asn1js from "asn1js";
import * as pkijs from "../src/index.js";

const { Crypto } = require("@peculiar/webcrypto");
const crypto = new Crypto();

const assert = require("assert");
pkijs.setEngine("newEngine", crypto, new pkijs.CryptoEngine({ name: "", crypto: crypto, subtle: crypto.subtle }));

const recipientPrivateKeyPem = `
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgSgh0H70tbGuageLjXJ8+OhH6wzoSQJ96
qJ4PQ8RP2jagCgYIKoZIzj0DAQehRANCAAToW17Szlrc7F6JXyn3HggMkHx1TluBrteZ0WAQHV31u4yi
LaaR70atxlhCdMaTpFey+lnnjfSns3TipH47meUO
-----END PRIVATE KEY-----
`;
const recipientCertificatePem = `
-----BEGIN CERTIFICATE-----
MIIDrDCCAmCgAwIBAgIICTX8CVCpVZcwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgEFAKEcMBoG
CSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgMIGQMYGNMIGKBgNVBAMegYIAMAA2ADQAMABjAGYA
NQAyADMAMgA4ADkAZQBiAGYAMgBiADkAOAA4ADIAYwA2AGIAYQAxADgAYQAxAGIAOQBkAGMAYwAwAGEA
MABmADMAMABiADcAOAA2ADIAZABjADgAMgAxADEAZQA4ADAAZAAyAGIANAA1AGEANQBmAGEANQBkMB4X
DTIxMTAxNDEwMzMxOVoXDTIxMTAxNTEwMzMxOVowgZAxgY0wgYoGA1UEAx6BggAwADYANAAwAGMAZgA1
ADIAMwAyADgAOQBlAGIAZgAyAGIAOQA4ADgAMgBjADYAYgBhADEAOABhADEAYgA5AGQAYwBjADAAYQAw
AGYAMwAwAGIANwA4ADYAMgBkAGMAOAAyADEAMQBlADgAMABkADIAYgA0ADUAYQA1AGYAYQA1AGQwWTAT
BgcqhkjOPQIBBggqhkjOPQMBBwNCAAToW17Szlrc7F6JXyn3HggMkHx1TluBrteZ0WAQHV31u4yiLaaR
70atxlhCdMaTpFey+lnnjfSns3TipH47meUOo2swaTAPBgNVHRMBAf8EBTADAgEAMCsGA1UdIwQkMCKA
IGQM9SMonr8rmILGuhihudzAoPMLeGLcghHoDStFpfpdMCkGA1UdDgQiBCC4UlB2jJfY9+OEU+7mFsXS
qF+Cg/gKWUSxfD/jHpQ1XDBBBgkqhkiG9w0BAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcN
AQEIMA0GCWCGSAFlAwQCAQUAogMCASADggEBAFT2wAXEKwmK0YgFWhX/QdWUAG4mlvcxqF+Re+UyW0/k
hfHKhgKP/z+CWdAKm1DD668rf7nQo4lQH3o8F3ksK3sTqTi5UXDB3S7xWnv1YFh73oQep3aDfKzpccLm
kFMUFatMJZmd+3N9uav5IA8TIIkFCqDVB59X9OCGvNubRZA+5q41b7TovTA04WBpiUxCWtKWJtArcU1I
hmu2w50768pQp9adVJCy7byQIzA1VE4g+85srzEiML2ICC1AVm25OzNs73nkDtdivZF81Wk1qheN0m57
NgeGVBTBKS4YLMeiMowMXJKoFnFqwyv+0JL0ZeFCh0al2RF+FDk86b1rykY===
-----END CERTIFICATE-----
`;
const envelopedDataPem = `
-----BEGIN CMS-----
MIIB/wYJKoZIhvcNAQcDoIIB8DCCAewCAQIxggF3oYIBcwIBA6BRoU8wCQYHKoZIzj0CAQNCAATqtUjV
kFlUhI0eYHRpRUFYeCJL7OXFTDGbFzLo75544lkdQ6QqUOAdjlCctmY1aURutiyp8ClU8S5B1rLPBJuZ
oUIEQO0Gsde4OdIqXuwrBdW+b+5JU4b4LmUJZCzEVpsmtAUBZ3wd7TsjuHbHtSvRpGUyw8Hu8hfyDefA
FQzaMnZjCoYwFwYGK4EEAQsDMA0GCWCGSAFlAwQBLQUAMIG9MIG6MIGdMIGQMYGNMIGKBgNVBAMegYIA
MAA2ADQAMABjAGYANQAyADMAMgA4ADkAZQBiAGYAMgBiADkAOAA4ADIAYwA2AGIAYQAxADgAYQAxAGIA
OQBkAGMAYwAwAGEAMABmADMAMABiADcAOAA2ADIAZABjADgAMgAxADEAZQA4ADAAZAAyAGIANAA1AGEA
NQBmAGEANQBkAggJNfwJUKlVlwQYMK8dCPR+rZ+p8f9JoB2+ns5mGIV45F7MMIAGCSqGSIb3DQEHATAd
BglghkgBZQMEAQIEEFlT3Jb453LG9SVYa7MaWiCggAQgXNT1MFuyL+mH3XORYWrmsk9a1qui+48NZZbJ
qH9W/zoAAAAAoRgwFgYIBAB/ABEAAQAxCgIIMltJu53L4Og===
-----END CMS-----
`;

it("EnvelopedData with an ECCCMSSharedInfo containing algorithmParams should be decrypted", async () => {
const recipientPrivateKey = pemToDer(recipientPrivateKeyPem);

const recipientCertificate = new pkijs.Certificate({schema: pemToAsn1(recipientCertificatePem)});

const contentInfo = new pkijs.ContentInfo({schema: pemToAsn1(envelopedDataPem)});
const envelopedData = new pkijs.EnvelopedData({schema: contentInfo.content});

const plaintext = await envelopedData.decrypt(0, {recipientCertificate, recipientPrivateKey});
assert.equal("Hi. My name is Alice.", Buffer.from(plaintext).toString());
});

function pemToDer(pemString) {
const derBase64 = pemString.replace(/(-----(BEGIN|END) [\w ]+-----|\n)/g, "");
const der = Buffer.from(derBase64, "base64");
return der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength);
}

function pemToAsn1(pemString) {
const der = pemToDer(pemString);
const asn1 = asn1js.fromBER(der);
if (asn1.offset === -1) {
throw new Error("Value is not DER-encoded");
}
return asn1.result;
}

0 comments on commit 03cc200

Please sign in to comment.