- Implemented Zustand store for S/MIME functionality, including state management for key records and public certificates. - Added methods for importing PKCS#12 files and public certificates, binding identities to keys, and managing unlocked keys. - Introduced session storage for remembering unlocked keys across sessions. - Enhanced error handling and loading states during data operations.
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import * as asn1js from 'asn1js';
|
|
import * as pkijs from 'pkijs';
|
|
import { parseCertificateDer } from './certificate-utils';
|
|
|
|
/**
|
|
* Produce CMS EnvelopedData for the given MIME content.
|
|
*
|
|
* Content type: application/pkcs7-mime; smime-type=enveloped-data
|
|
*
|
|
* Always includes the sender's cert so the sender can decrypt their Sent mail.
|
|
*/
|
|
export async function smimeEncrypt(
|
|
mimeBytes: Uint8Array,
|
|
recipientCertsDer: ArrayBuffer[],
|
|
senderCertDer: ArrayBuffer,
|
|
useAes128?: boolean,
|
|
): Promise<Blob> {
|
|
// Combine recipient + sender certs, deduplicate by DER bytes
|
|
const allCertDers = deduplicateCerts([...recipientCertsDer, senderCertDer]);
|
|
|
|
if (allCertDers.length === 0) {
|
|
throw new Error('No recipient certificates provided');
|
|
}
|
|
|
|
// Parse all certificates
|
|
const recipientCerts = allCertDers.map((der) => parseCertificateDer(der));
|
|
|
|
// Build EnvelopedData
|
|
const cmsEnveloped = new pkijs.EnvelopedData();
|
|
|
|
// Add recipient info for each certificate
|
|
for (const cert of recipientCerts) {
|
|
cmsEnveloped.addRecipientByCertificate(cert, {
|
|
oaepHashAlgorithm: 'SHA-256',
|
|
}, undefined, new pkijs.CryptoEngine({
|
|
crypto: crypto,
|
|
subtle: crypto.subtle,
|
|
name: 'webcrypto',
|
|
}));
|
|
}
|
|
|
|
// Encrypt the content
|
|
const contentEncryptionAlgorithm = useAes128
|
|
? { name: 'AES-GCM', length: 128 }
|
|
: { name: 'AES-GCM', length: 256 };
|
|
|
|
await cmsEnveloped.encrypt(contentEncryptionAlgorithm, mimeBytes.buffer.slice(mimeBytes.byteOffset, mimeBytes.byteOffset + mimeBytes.byteLength) as ArrayBuffer, new pkijs.CryptoEngine({
|
|
crypto: crypto,
|
|
subtle: crypto.subtle,
|
|
name: 'webcrypto',
|
|
}));
|
|
|
|
// Wrap in ContentInfo
|
|
const cms = new pkijs.ContentInfo({
|
|
contentType: '1.2.840.113549.1.7.3', // id-envelopedData
|
|
content: cmsEnveloped.toSchema(),
|
|
});
|
|
|
|
const cmsBytes = cms.toSchema().toBER(false);
|
|
return new Blob([cmsBytes], { type: 'application/pkcs7-mime; smime-type=enveloped-data' });
|
|
}
|
|
|
|
/** Remove duplicate DER-encoded certificates based on byte equality. */
|
|
function deduplicateCerts(certs: ArrayBuffer[]): ArrayBuffer[] {
|
|
const seen = new Set<string>();
|
|
const result: ArrayBuffer[] = [];
|
|
for (const cert of certs) {
|
|
const key = arrayBufferToHex(cert);
|
|
if (!seen.has(key)) {
|
|
seen.add(key);
|
|
result.push(cert);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function arrayBufferToHex(buf: ArrayBuffer): string {
|
|
return Array.from(new Uint8Array(buf))
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|