feat: integrate webcrypto-liner for legacy algorithm support in S/MIME handling
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Crypto engine backed by webcrypto-liner for legacy algorithm support.
|
||||
*
|
||||
* webcrypto-liner extends the native Web Crypto API with algorithms
|
||||
* like DES-EDE3-CBC (3DES) that are commonly found in S/MIME messages
|
||||
* and PKCS#12 files produced by legacy clients (Outlook, Thunderbird, etc.).
|
||||
*
|
||||
* Native Web Crypto calls are passed through to the real implementation;
|
||||
* liner only intercepts algorithms that the browser doesn't natively support.
|
||||
*/
|
||||
|
||||
import * as pkijs from 'pkijs';
|
||||
|
||||
// webcrypto-liner exports a Crypto constructor at runtime that extends native
|
||||
// Web Crypto with legacy algorithms (3DES, etc.). Its type declarations only
|
||||
// expose the type alias, so we import the module dynamically and cast.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const liner = require('webcrypto-liner') as {
|
||||
Crypto: { new (): Crypto };
|
||||
setCrypto: (subtle: SubtleCrypto) => void;
|
||||
nativeCrypto: Crypto | Record<string, never>;
|
||||
};
|
||||
|
||||
let linerEngine: pkijs.CryptoEngine | null = null;
|
||||
let linerCryptoInstance: Crypto | null = null;
|
||||
|
||||
function ensureLiner() {
|
||||
if (!linerCryptoInstance) {
|
||||
// In Node.js, webcrypto-liner can't auto-detect the native crypto
|
||||
// (it looks for self.crypto which doesn't exist). Feed it manually
|
||||
// so that native algorithms (RSA, AES, etc.) stay hardware-accelerated
|
||||
// and only truly missing algorithms (3DES) use the software fallback.
|
||||
if (
|
||||
typeof liner.nativeCrypto?.getRandomValues !== 'function' &&
|
||||
typeof globalThis.crypto?.subtle !== 'undefined'
|
||||
) {
|
||||
liner.setCrypto(globalThis.crypto.subtle);
|
||||
}
|
||||
linerCryptoInstance = new liner.Crypto();
|
||||
}
|
||||
if (!linerEngine) {
|
||||
linerEngine = new pkijs.CryptoEngine({
|
||||
crypto: linerCryptoInstance,
|
||||
subtle: linerCryptoInstance.subtle,
|
||||
name: 'webcrypto-liner',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Get a PKI.js CryptoEngine with 3DES (and other legacy algorithm) support. */
|
||||
export function getLinerCryptoEngine(): pkijs.CryptoEngine {
|
||||
ensureLiner();
|
||||
return linerEngine!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an async operation with the global PKI.js engine set to webcrypto-liner,
|
||||
* then restore the previous engine afterwards.
|
||||
*
|
||||
* Required for operations that use the global engine internally
|
||||
* (e.g. PFX.parseInternalValues for PKCS#12 import).
|
||||
*/
|
||||
export async function withLinerEngine<T>(fn: () => Promise<T>): Promise<T> {
|
||||
ensureLiner();
|
||||
|
||||
// Save the current global engine so we can restore it
|
||||
const prev = pkijs.getEngine();
|
||||
|
||||
pkijs.setEngine('webcrypto-liner', linerCryptoInstance!, linerEngine!);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
// Restore the previous engine
|
||||
pkijs.setEngine(prev.name, prev.crypto as unknown as pkijs.CryptoEngine);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
classifyCapabilities,
|
||||
} from './certificate-utils';
|
||||
import type { SmimeKeyRecord, Pkcs12ImportResult } from './types';
|
||||
import { withLinerEngine } from './crypto-engine';
|
||||
|
||||
const KDF_ITERATIONS = 600_000;
|
||||
const AES_KEY_LENGTH = 256;
|
||||
@@ -39,9 +40,12 @@ export async function importPkcs12(
|
||||
// PKIjs handles MAC verification internally during parseInternalValues
|
||||
}
|
||||
|
||||
// Parse internal values
|
||||
await pfx.parseInternalValues({
|
||||
password: stringToAB(p12Passphrase),
|
||||
// Use webcrypto-liner as the global engine for 3DES support.
|
||||
// Many PKCS#12 files use pbeWithSHAAnd3-KeyTripleDES-CBC internally.
|
||||
await withLinerEngine(async () => {
|
||||
await pfx.parseInternalValues({
|
||||
password: stringToAB(p12Passphrase),
|
||||
});
|
||||
});
|
||||
|
||||
// Extract certificates and private key from parsed PKCS#12
|
||||
@@ -63,7 +67,9 @@ export async function importPkcs12(
|
||||
}
|
||||
return {};
|
||||
});
|
||||
await authSafe.parseInternalValues({ safeContents: safeContentsParams });
|
||||
await withLinerEngine(async () => {
|
||||
await authSafe.parseInternalValues({ safeContents: safeContentsParams });
|
||||
});
|
||||
|
||||
for (const safeContent of authSafe.parsedValue.safeContents) {
|
||||
const sc = safeContent.value ?? safeContent.parsedValue;
|
||||
@@ -115,8 +121,10 @@ export async function importPkcs12(
|
||||
privateKeyInfo = shroudedBag.parsedValue;
|
||||
} else {
|
||||
// Decrypt shrouded key bag to get private key info
|
||||
await (shroudedBag as unknown as { parseInternalValues(params: { password: ArrayBuffer }): Promise<void> }).parseInternalValues({
|
||||
password: stringToAB(p12Passphrase),
|
||||
await withLinerEngine(async () => {
|
||||
await (shroudedBag as unknown as { parseInternalValues(params: { password: ArrayBuffer }): Promise<void> }).parseInternalValues({
|
||||
password: stringToAB(p12Passphrase),
|
||||
});
|
||||
});
|
||||
if (shroudedBag.parsedValue) {
|
||||
privateKeyInfo = shroudedBag.parsedValue;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import * as pkijs from 'pkijs';
|
||||
import * as asn1js from 'asn1js';
|
||||
import type { SmimeKeyRecord } from './types';
|
||||
import { getLinerCryptoEngine } from './crypto-engine';
|
||||
|
||||
export interface DecryptionInput {
|
||||
/** Raw CMS EnvelopedData bytes (DER) */
|
||||
@@ -359,11 +360,8 @@ async function decryptWithKey(
|
||||
const certAsn1 = asn1js.fromBER(keyRecord.certificate);
|
||||
const cert = new pkijs.Certificate({ schema: certAsn1.result });
|
||||
|
||||
const cryptoEngine = new pkijs.CryptoEngine({
|
||||
crypto: crypto,
|
||||
subtle: crypto.subtle,
|
||||
name: 'webcrypto',
|
||||
});
|
||||
// Use webcrypto-liner engine for legacy algorithm support (e.g. 3DES)
|
||||
const cryptoEngine = getLinerCryptoEngine();
|
||||
|
||||
const result = await envelopedData.decrypt(
|
||||
recipientIndex,
|
||||
|
||||
Reference in New Issue
Block a user