fix: extend CryptoEngine to support legacy algorithms and integrate with LinerEngine for decryption

This commit is contained in:
Linus Rath
2026-03-21 20:45:20 +01:00
parent 83a0a1e235
commit e6d09546b1
2 changed files with 61 additions and 16 deletions
+45 -3
View File
@@ -145,11 +145,50 @@ function passwordToBMP(password: ArrayBuffer): Uint8Array {
return bmp; return bmp;
} }
// ── CMS content encryption OIDs (for EnvelopedData decryption) ─────
const OID_DES_EDE3_CBC = '1.2.840.113549.3.7'; // des-EDE3-CBC (3DES)
const OID_DES_CBC = '1.3.14.3.2.7'; // desCBC
const OID_RC2_CBC = '1.2.840.113549.3.2'; // rc2CBC
/** /**
* Extended CryptoEngine that handles legacy PKCS#12 PBE algorithms. * Extended CryptoEngine that handles legacy algorithms (3DES, etc.)
* Falls through to the base CryptoEngine for everything else. * not recognized by pkijs's default CryptoEngine.
*
* - Adds OID→algorithm mappings for DES-EDE3-CBC so that
* EnvelopedData.decrypt() can process 3DES-encrypted S/MIME messages.
* - Handles legacy PKCS#12 PBE algorithms via custom KDF.
*/ */
class Pkcs12CryptoEngine extends pkijs.CryptoEngine { class Pkcs12CryptoEngine extends pkijs.CryptoEngine {
/**
* Extend OID→algorithm mapping with legacy algorithms that webcrypto-liner
* supports but pkijs does not know about.
*/
getAlgorithmByOID(oid: string, safety?: boolean, target?: string): object {
switch (oid) {
case OID_DES_EDE3_CBC:
return { name: 'DES-EDE3-CBC', length: 192 };
case OID_DES_CBC:
return { name: 'DES-CBC', length: 64 };
case OID_RC2_CBC:
return { name: 'RC2-CBC', length: 128 };
default:
return super.getAlgorithmByOID(oid, safety, target);
}
}
getOIDByAlgorithm(algorithm: { name: string; length?: number }, safety?: boolean, target?: string): string {
switch (algorithm.name.toUpperCase()) {
case 'DES-EDE3-CBC':
return OID_DES_EDE3_CBC;
case 'DES-CBC':
return OID_DES_CBC;
case 'RC2-CBC':
return OID_RC2_CBC;
default:
return super.getOIDByAlgorithm(algorithm, safety, target);
}
}
async decryptEncryptedContentInfo( async decryptEncryptedContentInfo(
parameters: Parameters<pkijs.CryptoEngine['decryptEncryptedContentInfo']>[0], parameters: Parameters<pkijs.CryptoEngine['decryptEncryptedContentInfo']>[0],
): Promise<ArrayBuffer> { ): Promise<ArrayBuffer> {
@@ -182,9 +221,11 @@ class Pkcs12CryptoEngine extends pkijs.CryptoEngine {
const ivBytes = await pkcs12KDF(bmpPassword, salt, iterations, 2, ivLen); const ivBytes = await pkcs12KDF(bmpPassword, salt, iterations, 2, ivLen);
// Import key via webcrypto-liner (supports DES-EDE3-CBC) // Import key via webcrypto-liner (supports DES-EDE3-CBC)
const keyData = new Uint8Array(keyBytes.buffer as ArrayBuffer, keyBytes.byteOffset, keyBytes.byteLength);
const cryptoKey = await this.importKey( const cryptoKey = await this.importKey(
'raw', 'raw',
new Uint8Array(keyBytes.buffer as ArrayBuffer, keyBytes.byteOffset, keyBytes.byteLength) as unknown as BufferSource, keyData,
// eslint-disable-next-line no-undef
{ name: algName, length: keyLen * 8 } as Algorithm, { name: algName, length: keyLen * 8 } as Algorithm,
false, false,
['decrypt'], ['decrypt'],
@@ -193,6 +234,7 @@ class Pkcs12CryptoEngine extends pkijs.CryptoEngine {
// Decrypt // Decrypt
const ciphertext = parameters.encryptedContentInfo.getEncryptedContent(); const ciphertext = parameters.encryptedContentInfo.getEncryptedContent();
return this.decrypt( return this.decrypt(
// eslint-disable-next-line no-undef
{ name: algName, iv: ivBytes } as Algorithm, { name: algName, iv: ivBytes } as Algorithm,
cryptoKey, cryptoKey,
ciphertext, ciphertext,
+16 -13
View File
@@ -8,7 +8,7 @@
import * as pkijs from 'pkijs'; import * as pkijs from 'pkijs';
import * as asn1js from 'asn1js'; import * as asn1js from 'asn1js';
import type { SmimeKeyRecord } from './types'; import type { SmimeKeyRecord } from './types';
import { getLinerCryptoEngine } from './crypto-engine'; import { getLinerCryptoEngine, withLinerEngine } from './crypto-engine';
export interface DecryptionInput { export interface DecryptionInput {
/** Raw CMS EnvelopedData bytes (DER) */ /** Raw CMS EnvelopedData bytes (DER) */
@@ -360,17 +360,20 @@ async function decryptWithKey(
const certAsn1 = asn1js.fromBER(keyRecord.certificate); const certAsn1 = asn1js.fromBER(keyRecord.certificate);
const cert = new pkijs.Certificate({ schema: certAsn1.result }); const cert = new pkijs.Certificate({ schema: certAsn1.result });
// Use webcrypto-liner engine for legacy algorithm support (e.g. 3DES) // Use withLinerEngine to set the global pkijs engine to webcrypto-liner.
const cryptoEngine = getLinerCryptoEngine(); // This is required because pkijs internally may use getEngine() for
// OID lookups and crypto operations. Without this, 3DES-encrypted
// messages fail because the default engine doesn't know about DES-EDE3-CBC.
return withLinerEngine(async () => {
const cryptoEngine = getLinerCryptoEngine();
const result = await envelopedData.decrypt( return envelopedData.decrypt(
recipientIndex, recipientIndex,
{ {
recipientCertificate: cert, recipientCertificate: cert,
recipientPrivateKey: privateKey, recipientPrivateKey: privateKey,
}, },
cryptoEngine, cryptoEngine,
); );
});
return result;
} }