@brightchain/brightchain-lib 0.29.26 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -4
- package/src/index.d.ts +1 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +2 -1
- package/src/index.js.map +1 -1
- package/src/lib/constants.d.ts +3 -26
- package/src/lib/constants.d.ts.map +1 -1
- package/src/lib/constants.js +2 -1
- package/src/lib/constants.js.map +1 -1
- package/src/lib/db/collection.d.ts +1 -1
- package/src/lib/db/collection.d.ts.map +1 -1
- package/src/lib/db/collection.js +2 -1
- package/src/lib/db/collection.js.map +1 -1
- package/src/lib/enumeration-translations/index.d.ts +2 -0
- package/src/lib/enumeration-translations/index.d.ts.map +1 -1
- package/src/lib/enumeration-translations/index.js +1 -0
- package/src/lib/enumeration-translations/index.js.map +1 -1
- package/src/lib/enumeration-translations/memberStatusType.d.ts +5 -0
- package/src/lib/enumeration-translations/memberStatusType.d.ts.map +1 -0
- package/src/lib/enumeration-translations/memberStatusType.js +58 -0
- package/src/lib/enumeration-translations/memberStatusType.js.map +1 -0
- package/src/lib/enumerations/brightChainStrings.d.ts +61 -0
- package/src/lib/enumerations/brightChainStrings.d.ts.map +1 -1
- package/src/lib/enumerations/brightChainStrings.js +64 -0
- package/src/lib/enumerations/brightChainStrings.js.map +1 -1
- package/src/lib/enumerations/brightchainFeatures.d.ts +1 -0
- package/src/lib/enumerations/brightchainFeatures.d.ts.map +1 -1
- package/src/lib/enumerations/brightchainFeatures.js +1 -0
- package/src/lib/enumerations/brightchainFeatures.js.map +1 -1
- package/src/lib/enumerations/communication.d.ts +7 -1
- package/src/lib/enumerations/communication.d.ts.map +1 -1
- package/src/lib/enumerations/communication.js +6 -0
- package/src/lib/enumerations/communication.js.map +1 -1
- package/src/lib/enumerations/friendRequestStatus.d.ts +7 -0
- package/src/lib/enumerations/friendRequestStatus.d.ts.map +1 -0
- package/src/lib/enumerations/friendRequestStatus.js +11 -0
- package/src/lib/enumerations/friendRequestStatus.js.map +1 -0
- package/src/lib/enumerations/friendsErrorCode.d.ts +10 -0
- package/src/lib/enumerations/friendsErrorCode.d.ts.map +1 -0
- package/src/lib/enumerations/friendsErrorCode.js +14 -0
- package/src/lib/enumerations/friendsErrorCode.js.map +1 -0
- package/src/lib/enumerations/friendshipStatus.d.ts +7 -0
- package/src/lib/enumerations/friendshipStatus.d.ts.map +1 -0
- package/src/lib/enumerations/friendshipStatus.js +11 -0
- package/src/lib/enumerations/friendshipStatus.js.map +1 -0
- package/src/lib/enumerations/index.d.ts +3 -0
- package/src/lib/enumerations/index.d.ts.map +1 -1
- package/src/lib/enumerations/index.js +4 -0
- package/src/lib/enumerations/index.js.map +1 -1
- package/src/lib/enumerations/messaging/emailErrorType.d.ts +15 -2
- package/src/lib/enumerations/messaging/emailErrorType.d.ts.map +1 -1
- package/src/lib/enumerations/messaging/emailErrorType.js +17 -1
- package/src/lib/enumerations/messaging/emailErrorType.js.map +1 -1
- package/src/lib/enumerations/messaging/messageEncryptionScheme.d.ts +4 -2
- package/src/lib/enumerations/messaging/messageEncryptionScheme.d.ts.map +1 -1
- package/src/lib/enumerations/messaging/messageEncryptionScheme.js +3 -1
- package/src/lib/enumerations/messaging/messageEncryptionScheme.js.map +1 -1
- package/src/lib/errors/encryptionErrors.d.ts +71 -0
- package/src/lib/errors/encryptionErrors.d.ts.map +1 -0
- package/src/lib/errors/encryptionErrors.js +112 -0
- package/src/lib/errors/encryptionErrors.js.map +1 -0
- package/src/lib/errors/friendsServiceError.d.ts +20 -0
- package/src/lib/errors/friendsServiceError.d.ts.map +1 -0
- package/src/lib/errors/friendsServiceError.js +48 -0
- package/src/lib/errors/friendsServiceError.js.map +1 -0
- package/src/lib/errors/index.d.ts +15 -0
- package/src/lib/errors/index.d.ts.map +1 -1
- package/src/lib/errors/index.js +21 -0
- package/src/lib/errors/index.js.map +1 -1
- package/src/lib/i18n/i18n-setup.d.ts +2 -2
- package/src/lib/i18n/i18n-setup.d.ts.map +1 -1
- package/src/lib/i18n/strings/englishUK.d.ts +2 -2
- package/src/lib/i18n/strings/englishUK.d.ts.map +1 -1
- package/src/lib/i18n/strings/englishUK.js.map +1 -1
- package/src/lib/i18n/strings/englishUs.d.ts +2 -2
- package/src/lib/i18n/strings/englishUs.d.ts.map +1 -1
- package/src/lib/i18n/strings/englishUs.js +65 -1
- package/src/lib/i18n/strings/englishUs.js.map +1 -1
- package/src/lib/i18n/strings/french.d.ts +2 -2
- package/src/lib/i18n/strings/french.d.ts.map +1 -1
- package/src/lib/i18n/strings/french.js +78 -13
- package/src/lib/i18n/strings/french.js.map +1 -1
- package/src/lib/i18n/strings/german.d.ts +2 -2
- package/src/lib/i18n/strings/german.d.ts.map +1 -1
- package/src/lib/i18n/strings/german.js +77 -12
- package/src/lib/i18n/strings/german.js.map +1 -1
- package/src/lib/i18n/strings/japanese.d.ts +2 -2
- package/src/lib/i18n/strings/japanese.d.ts.map +1 -1
- package/src/lib/i18n/strings/japanese.js +77 -12
- package/src/lib/i18n/strings/japanese.js.map +1 -1
- package/src/lib/i18n/strings/mandarin.d.ts +2 -2
- package/src/lib/i18n/strings/mandarin.d.ts.map +1 -1
- package/src/lib/i18n/strings/mandarin.js +77 -12
- package/src/lib/i18n/strings/mandarin.js.map +1 -1
- package/src/lib/i18n/strings/spanish.d.ts +2 -2
- package/src/lib/i18n/strings/spanish.d.ts.map +1 -1
- package/src/lib/i18n/strings/spanish.js +77 -12
- package/src/lib/i18n/strings/spanish.js.map +1 -1
- package/src/lib/i18n/strings/ukrainian.d.ts +2 -2
- package/src/lib/i18n/strings/ukrainian.d.ts.map +1 -1
- package/src/lib/i18n/strings/ukrainian.js +77 -12
- package/src/lib/i18n/strings/ukrainian.js.map +1 -1
- package/src/lib/index.d.ts +31 -0
- package/src/lib/index.d.ts.map +1 -1
- package/src/lib/index.js +29 -1
- package/src/lib/index.js.map +1 -1
- package/src/lib/interfaces/appSubsystemPlugin.d.ts +69 -0
- package/src/lib/interfaces/appSubsystemPlugin.d.ts.map +1 -0
- package/src/lib/interfaces/appSubsystemPlugin.js +3 -0
- package/src/lib/interfaces/appSubsystemPlugin.js.map +1 -0
- package/src/lib/interfaces/auth/writeProof.d.ts +2 -0
- package/src/lib/interfaces/auth/writeProof.d.ts.map +1 -1
- package/src/lib/interfaces/auth/writeProofUtils.d.ts +1 -1
- package/src/lib/interfaces/auth/writeProofUtils.d.ts.map +1 -1
- package/src/lib/interfaces/auth/writeProofUtils.js +2 -2
- package/src/lib/interfaces/auth/writeProofUtils.js.map +1 -1
- package/src/lib/interfaces/availability/gossipService.d.ts +99 -1
- package/src/lib/interfaces/availability/gossipService.d.ts.map +1 -1
- package/src/lib/interfaces/availability/gossipService.js +4 -0
- package/src/lib/interfaces/availability/gossipService.js.map +1 -1
- package/src/lib/interfaces/communication/blockContentStore.d.ts +57 -0
- package/src/lib/interfaces/communication/blockContentStore.d.ts.map +1 -0
- package/src/lib/interfaces/communication/blockContentStore.js +21 -0
- package/src/lib/interfaces/communication/blockContentStore.js.map +1 -0
- package/src/lib/interfaces/communication/chatStorageProvider.d.ts +77 -0
- package/src/lib/interfaces/communication/chatStorageProvider.d.ts.map +1 -0
- package/src/lib/interfaces/communication/chatStorageProvider.js +25 -0
- package/src/lib/interfaces/communication/chatStorageProvider.js.map +1 -0
- package/src/lib/interfaces/communication/index.d.ts +4 -0
- package/src/lib/interfaces/communication/index.d.ts.map +1 -0
- package/src/lib/interfaces/communication/index.js +3 -0
- package/src/lib/interfaces/communication/index.js.map +1 -0
- package/src/lib/interfaces/communication/server.d.ts +57 -0
- package/src/lib/interfaces/communication/server.d.ts.map +1 -0
- package/src/lib/interfaces/communication/server.js +14 -0
- package/src/lib/interfaces/communication/server.js.map +1 -0
- package/src/lib/interfaces/communication.d.ts +62 -5
- package/src/lib/interfaces/communication.d.ts.map +1 -1
- package/src/lib/interfaces/communication.js +8 -0
- package/src/lib/interfaces/communication.js.map +1 -1
- package/src/lib/interfaces/communicationEvents.d.ts +59 -1
- package/src/lib/interfaces/communicationEvents.d.ts.map +1 -1
- package/src/lib/interfaces/constants.d.ts +2 -0
- package/src/lib/interfaces/constants.d.ts.map +1 -1
- package/src/lib/interfaces/events/communicationEventEmitter.d.ts +9 -0
- package/src/lib/interfaces/events/communicationEventEmitter.d.ts.map +1 -1
- package/src/lib/interfaces/events/communicationEventEmitter.js +9 -0
- package/src/lib/interfaces/events/communicationEventEmitter.js.map +1 -1
- package/src/lib/interfaces/friends/baseFriendRequest.d.ts +13 -0
- package/src/lib/interfaces/friends/baseFriendRequest.d.ts.map +1 -0
- package/src/lib/interfaces/friends/baseFriendRequest.js +3 -0
- package/src/lib/interfaces/friends/baseFriendRequest.js.map +1 -0
- package/src/lib/interfaces/friends/baseFriendship.d.ts +11 -0
- package/src/lib/interfaces/friends/baseFriendship.d.ts.map +1 -0
- package/src/lib/interfaces/friends/baseFriendship.js +3 -0
- package/src/lib/interfaces/friends/baseFriendship.js.map +1 -0
- package/src/lib/interfaces/friends/friendsService.d.ts +32 -0
- package/src/lib/interfaces/friends/friendsService.d.ts.map +1 -0
- package/src/lib/interfaces/friends/friendsService.js +3 -0
- package/src/lib/interfaces/friends/friendsService.js.map +1 -0
- package/src/lib/interfaces/friends/friendsSuggestionProvider.d.ts +17 -0
- package/src/lib/interfaces/friends/friendsSuggestionProvider.d.ts.map +1 -0
- package/src/lib/interfaces/friends/friendsSuggestionProvider.js +3 -0
- package/src/lib/interfaces/friends/friendsSuggestionProvider.js.map +1 -0
- package/src/lib/interfaces/friends/index.d.ts +6 -0
- package/src/lib/interfaces/friends/index.d.ts.map +1 -0
- package/src/lib/interfaces/friends/index.js +3 -0
- package/src/lib/interfaces/friends/index.js.map +1 -0
- package/src/lib/interfaces/friends/pagination.d.ts +11 -0
- package/src/lib/interfaces/friends/pagination.d.ts.map +1 -0
- package/src/lib/interfaces/friends/pagination.js +3 -0
- package/src/lib/interfaces/friends/pagination.js.map +1 -0
- package/src/lib/interfaces/index.d.ts +6 -1
- package/src/lib/interfaces/index.d.ts.map +1 -1
- package/src/lib/interfaces/index.js +3 -0
- package/src/lib/interfaces/index.js.map +1 -1
- package/src/lib/interfaces/messaging/gpgKey.d.ts +93 -0
- package/src/lib/interfaces/messaging/gpgKey.d.ts.map +1 -0
- package/src/lib/interfaces/messaging/gpgKey.js +12 -0
- package/src/lib/interfaces/messaging/gpgKey.js.map +1 -0
- package/src/lib/interfaces/messaging/index.d.ts +4 -0
- package/src/lib/interfaces/messaging/index.d.ts.map +1 -1
- package/src/lib/interfaces/messaging/index.js +4 -0
- package/src/lib/interfaces/messaging/index.js.map +1 -1
- package/src/lib/interfaces/messaging/keyStore.d.ts +100 -0
- package/src/lib/interfaces/messaging/keyStore.d.ts.map +1 -0
- package/src/lib/interfaces/messaging/keyStore.js +13 -0
- package/src/lib/interfaces/messaging/keyStore.js.map +1 -0
- package/src/lib/interfaces/messaging/recipientKeyResolver.d.ts +92 -0
- package/src/lib/interfaces/messaging/recipientKeyResolver.d.ts.map +1 -0
- package/src/lib/interfaces/messaging/recipientKeyResolver.js +13 -0
- package/src/lib/interfaces/messaging/recipientKeyResolver.js.map +1 -0
- package/src/lib/interfaces/messaging/smimeCertificate.d.ts +99 -0
- package/src/lib/interfaces/messaging/smimeCertificate.d.ts.map +1 -0
- package/src/lib/interfaces/messaging/smimeCertificate.js +12 -0
- package/src/lib/interfaces/messaging/smimeCertificate.js.map +1 -0
- package/src/lib/interfaces/responses/adminDashboardResponse.d.ts +8 -0
- package/src/lib/interfaces/responses/adminDashboardResponse.d.ts.map +1 -1
- package/src/lib/interfaces/responses/communicationResponses.d.ts +25 -0
- package/src/lib/interfaces/responses/communicationResponses.d.ts.map +1 -1
- package/src/lib/interfaces/storage/documentTypes.d.ts +4 -0
- package/src/lib/interfaces/storage/documentTypes.d.ts.map +1 -1
- package/src/lib/services/blockService.d.ts +11 -3
- package/src/lib/services/blockService.d.ts.map +1 -1
- package/src/lib/services/blockService.js +22 -2
- package/src/lib/services/blockService.js.map +1 -1
- package/src/lib/services/communication/__tests__/mockChatStorageProvider.d.ts +108 -0
- package/src/lib/services/communication/__tests__/mockChatStorageProvider.d.ts.map +1 -0
- package/src/lib/services/communication/__tests__/mockChatStorageProvider.js +284 -0
- package/src/lib/services/communication/__tests__/mockChatStorageProvider.js.map +1 -0
- package/src/lib/services/communication/attachmentUtils.d.ts +20 -0
- package/src/lib/services/communication/attachmentUtils.d.ts.map +1 -0
- package/src/lib/services/communication/attachmentUtils.js +43 -0
- package/src/lib/services/communication/attachmentUtils.js.map +1 -0
- package/src/lib/services/communication/channelService.d.ts +143 -14
- package/src/lib/services/communication/channelService.d.ts.map +1 -1
- package/src/lib/services/communication/channelService.js +562 -41
- package/src/lib/services/communication/channelService.js.map +1 -1
- package/src/lib/services/communication/conversationService.d.ts +91 -4
- package/src/lib/services/communication/conversationService.d.ts.map +1 -1
- package/src/lib/services/communication/conversationService.js +269 -7
- package/src/lib/services/communication/conversationService.js.map +1 -1
- package/src/lib/services/communication/eciesKeyEncryptionHandler.d.ts +36 -0
- package/src/lib/services/communication/eciesKeyEncryptionHandler.d.ts.map +1 -0
- package/src/lib/services/communication/eciesKeyEncryptionHandler.js +30 -0
- package/src/lib/services/communication/eciesKeyEncryptionHandler.js.map +1 -0
- package/src/lib/services/communication/groupService.d.ts +99 -21
- package/src/lib/services/communication/groupService.d.ts.map +1 -1
- package/src/lib/services/communication/groupService.js +387 -41
- package/src/lib/services/communication/groupService.js.map +1 -1
- package/src/lib/services/communication/index.d.ts +6 -1
- package/src/lib/services/communication/index.d.ts.map +1 -1
- package/src/lib/services/communication/index.js +20 -1
- package/src/lib/services/communication/index.js.map +1 -1
- package/src/lib/services/communication/keyEpochManager.d.ts +41 -0
- package/src/lib/services/communication/keyEpochManager.d.ts.map +1 -0
- package/src/lib/services/communication/keyEpochManager.js +59 -0
- package/src/lib/services/communication/keyEpochManager.js.map +1 -0
- package/src/lib/services/communication/rehydrationHelpers.d.ts +32 -0
- package/src/lib/services/communication/rehydrationHelpers.d.ts.map +1 -0
- package/src/lib/services/communication/rehydrationHelpers.js +58 -0
- package/src/lib/services/communication/rehydrationHelpers.js.map +1 -0
- package/src/lib/services/communication/serverService.d.ts +193 -0
- package/src/lib/services/communication/serverService.d.ts.map +1 -0
- package/src/lib/services/communication/serverService.js +495 -0
- package/src/lib/services/communication/serverService.js.map +1 -0
- package/src/lib/services/copyOnWrite.service.d.ts +110 -0
- package/src/lib/services/copyOnWrite.service.d.ts.map +1 -0
- package/src/lib/services/copyOnWrite.service.js +256 -0
- package/src/lib/services/copyOnWrite.service.js.map +1 -0
- package/src/lib/services/index.d.ts +1 -0
- package/src/lib/services/index.d.ts.map +1 -1
- package/src/lib/services/index.js +1 -0
- package/src/lib/services/index.js.map +1 -1
- package/src/lib/services/memberStore.d.ts +17 -1
- package/src/lib/services/memberStore.d.ts.map +1 -1
- package/src/lib/services/memberStore.js +98 -17
- package/src/lib/services/memberStore.js.map +1 -1
- package/src/lib/services/messaging/emailEncryptionService.d.ts +162 -0
- package/src/lib/services/messaging/emailEncryptionService.d.ts.map +1 -1
- package/src/lib/services/messaging/emailEncryptionService.js +293 -0
- package/src/lib/services/messaging/emailEncryptionService.js.map +1 -1
- package/src/lib/services/messaging/emailMessageService.d.ts +64 -0
- package/src/lib/services/messaging/emailMessageService.d.ts.map +1 -1
- package/src/lib/services/messaging/emailMessageService.js +142 -13
- package/src/lib/services/messaging/emailMessageService.js.map +1 -1
- package/src/lib/services/messaging/gpgKeyManager.d.ts +130 -0
- package/src/lib/services/messaging/gpgKeyManager.d.ts.map +1 -0
- package/src/lib/services/messaging/gpgKeyManager.js +381 -0
- package/src/lib/services/messaging/gpgKeyManager.js.map +1 -0
- package/src/lib/services/messaging/index.d.ts +3 -0
- package/src/lib/services/messaging/index.d.ts.map +1 -1
- package/src/lib/services/messaging/index.js +3 -0
- package/src/lib/services/messaging/index.js.map +1 -1
- package/src/lib/services/messaging/recipientKeyResolver.d.ts +47 -0
- package/src/lib/services/messaging/recipientKeyResolver.d.ts.map +1 -0
- package/src/lib/services/messaging/recipientKeyResolver.js +132 -0
- package/src/lib/services/messaging/recipientKeyResolver.js.map +1 -0
- package/src/lib/services/messaging/smimeCertificateManager.d.ts +207 -0
- package/src/lib/services/messaging/smimeCertificateManager.d.ts.map +1 -0
- package/src/lib/services/messaging/smimeCertificateManager.js +696 -0
- package/src/lib/services/messaging/smimeCertificateManager.js.map +1 -0
- package/src/lib/utils/index.d.ts +6 -0
- package/src/lib/utils/index.d.ts.map +1 -1
- package/src/lib/utils/index.js +9 -0
- package/src/lib/utils/index.js.map +1 -1
- package/src/lib/utils/sortPair.d.ts +6 -0
- package/src/lib/utils/sortPair.d.ts.map +1 -0
- package/src/lib/utils/sortPair.js +11 -0
- package/src/lib/utils/sortPair.js.map +1 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SmimeCertificateManager = void 0;
|
|
4
|
+
const emailErrorType_1 = require("../../enumerations/messaging/emailErrorType");
|
|
5
|
+
const emailError_1 = require("../../errors/messaging/emailError");
|
|
6
|
+
/**
|
|
7
|
+
* Manages S/MIME X.509 certificate lifecycle: import, export, validation,
|
|
8
|
+
* and CMS encryption/signing operations.
|
|
9
|
+
*
|
|
10
|
+
* Wraps @peculiar/x509 for certificate parsing and pkijs for CMS operations.
|
|
11
|
+
*
|
|
12
|
+
* @see Requirements 6.1, 6.2, 6.4, 6.5, 6.7, 7.1, 8.1, 9.2
|
|
13
|
+
*/
|
|
14
|
+
class SmimeCertificateManager {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
_x509;
|
|
17
|
+
async x509() {
|
|
18
|
+
if (!this._x509) {
|
|
19
|
+
this._x509 = await import('@peculiar/x509');
|
|
20
|
+
}
|
|
21
|
+
return this._x509;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Import a PEM or DER X.509 certificate, validate structure, and extract metadata.
|
|
25
|
+
*
|
|
26
|
+
* @param content - PEM string or DER Uint8Array certificate data
|
|
27
|
+
* @param format - Certificate format: 'pem' or 'der'
|
|
28
|
+
* @returns Metadata extracted from the imported certificate
|
|
29
|
+
* @throws EmailError with SMIME_INVALID_CERT if the certificate is malformed
|
|
30
|
+
*
|
|
31
|
+
* @see Requirement 6.1 — Validate well-formed X.509 certificate in PEM or DER format
|
|
32
|
+
* @see Requirement 6.7 — Import S/MIME certificate from another user or CA
|
|
33
|
+
*/
|
|
34
|
+
async importCertificate(content, format) {
|
|
35
|
+
try {
|
|
36
|
+
let cert;
|
|
37
|
+
if (format === 'pem') {
|
|
38
|
+
if (typeof content !== 'string') {
|
|
39
|
+
throw new Error('PEM format requires string content');
|
|
40
|
+
}
|
|
41
|
+
cert = new (await this.x509()).X509Certificate(content);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
if (!(content instanceof Uint8Array)) {
|
|
45
|
+
throw new Error('DER format requires Uint8Array content');
|
|
46
|
+
}
|
|
47
|
+
// @peculiar/x509 accepts ArrayBuffer for DER data
|
|
48
|
+
cert = new (await this.x509()).X509Certificate(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
49
|
+
}
|
|
50
|
+
return this.extractMetadata(cert);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof emailError_1.EmailError) {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
throw new emailError_1.EmailError(emailErrorType_1.EmailErrorType.SMIME_INVALID_CERT, `Failed to import S/MIME certificate: ${error instanceof Error ? error.message : String(error)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Import a PKCS#12 bundle (.p12/.pfx), extract certificate and private key.
|
|
61
|
+
*
|
|
62
|
+
* @param data - Raw PKCS#12 binary data
|
|
63
|
+
* @param password - Password to decrypt the PKCS#12 bundle
|
|
64
|
+
* @returns Certificate bundle with PEM certificate, optional PEM private key, and metadata
|
|
65
|
+
* @throws EmailError with SMIME_PKCS12_FAILED on extraction failure
|
|
66
|
+
*
|
|
67
|
+
* @see Requirement 6.2 — Extract and store certificate and private key from PKCS#12
|
|
68
|
+
*/
|
|
69
|
+
async importPkcs12(data, password) {
|
|
70
|
+
try {
|
|
71
|
+
const pkijs = await import('pkijs');
|
|
72
|
+
const dataBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
73
|
+
// PFX.fromBER parses the ASN.1 structure directly
|
|
74
|
+
const pfx = pkijs.PFX.fromBER(dataBuffer);
|
|
75
|
+
// Convert password string to ArrayBuffer for pkijs
|
|
76
|
+
const passwordBuffer = SmimeCertificateManager.stringToArrayBuffer(password);
|
|
77
|
+
// Parse the PFX to extract bags
|
|
78
|
+
await pfx.parseInternalValues({
|
|
79
|
+
password: passwordBuffer,
|
|
80
|
+
checkIntegrity: true,
|
|
81
|
+
});
|
|
82
|
+
if (!pfx.parsedValue) {
|
|
83
|
+
throw new Error('Failed to parse PKCS#12 internal values');
|
|
84
|
+
}
|
|
85
|
+
let certificatePem;
|
|
86
|
+
let privateKeyPem;
|
|
87
|
+
// Iterate through authenticated safe contents to find certs and keys
|
|
88
|
+
for (const safeContent of pfx.parsedValue.authenticatedSafe?.parsedValue
|
|
89
|
+
?.safeContents ?? []) {
|
|
90
|
+
for (const safeBag of safeContent.value?.safeBags ?? []) {
|
|
91
|
+
// Certificate bag (OID 1.2.840.113549.1.12.10.1.3)
|
|
92
|
+
if (safeBag.bagId === '1.2.840.113549.1.12.10.1.3') {
|
|
93
|
+
const certBag = safeBag.bagValue;
|
|
94
|
+
if (certBag &&
|
|
95
|
+
'parsedValue' in certBag &&
|
|
96
|
+
certBag.parsedValue &&
|
|
97
|
+
typeof certBag
|
|
98
|
+
.parsedValue.toSchema === 'function') {
|
|
99
|
+
const parsedValue = certBag.parsedValue;
|
|
100
|
+
const certDer = parsedValue.toSchema().toBER(false);
|
|
101
|
+
const certX509 = new (await this.x509()).X509Certificate(certDer);
|
|
102
|
+
certificatePem = certX509.toString('pem');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// PKCS8 shrouded key bag (OID 1.2.840.113549.1.12.10.1.2)
|
|
106
|
+
if (safeBag.bagId === '1.2.840.113549.1.12.10.1.2') {
|
|
107
|
+
const shroudedBag = safeBag.bagValue;
|
|
108
|
+
if (shroudedBag && 'parseInternalValues' in shroudedBag) {
|
|
109
|
+
const keyInfo = await shroudedBag.parseInternalValues({
|
|
110
|
+
password: passwordBuffer,
|
|
111
|
+
});
|
|
112
|
+
const keyDer = keyInfo.toSchema().toBER(false);
|
|
113
|
+
privateKeyPem = SmimeCertificateManager.derToPem(new Uint8Array(keyDer), 'PRIVATE KEY');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Plain key bag (OID 1.2.840.113549.1.12.10.1.1)
|
|
117
|
+
if (safeBag.bagId === '1.2.840.113549.1.12.10.1.1') {
|
|
118
|
+
const keyBag = safeBag.bagValue;
|
|
119
|
+
if (keyBag && 'toSchema' in keyBag) {
|
|
120
|
+
const keyDer = keyBag
|
|
121
|
+
.toSchema()
|
|
122
|
+
.toBER(false);
|
|
123
|
+
privateKeyPem = SmimeCertificateManager.derToPem(new Uint8Array(keyDer), 'PRIVATE KEY');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!certificatePem) {
|
|
129
|
+
throw new Error('No certificate found in PKCS#12 bundle');
|
|
130
|
+
}
|
|
131
|
+
const cert = new (await this.x509()).X509Certificate(certificatePem);
|
|
132
|
+
const metadata = await this.extractMetadata(cert);
|
|
133
|
+
return {
|
|
134
|
+
certificatePem,
|
|
135
|
+
privateKeyPem,
|
|
136
|
+
metadata,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error instanceof emailError_1.EmailError) {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
throw new emailError_1.EmailError(emailErrorType_1.EmailErrorType.SMIME_PKCS12_FAILED, `Failed to import PKCS#12 bundle: ${error instanceof Error ? error.message : String(error)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Export a certificate as PEM, validating it first.
|
|
148
|
+
*
|
|
149
|
+
* @param certificatePem - PEM-encoded X.509 certificate to export
|
|
150
|
+
* @returns The validated PEM-encoded certificate string
|
|
151
|
+
* @throws EmailError with SMIME_INVALID_CERT if the certificate is malformed
|
|
152
|
+
*
|
|
153
|
+
* @see Requirement 15.2 — PEM round-trip property
|
|
154
|
+
*/
|
|
155
|
+
async exportCertificatePem(certificatePem) {
|
|
156
|
+
try {
|
|
157
|
+
const cert = new (await this.x509()).X509Certificate(certificatePem);
|
|
158
|
+
return cert.toString('pem');
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error instanceof emailError_1.EmailError) {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
throw new emailError_1.EmailError(emailErrorType_1.EmailErrorType.SMIME_INVALID_CERT, `Failed to export S/MIME certificate: ${error instanceof Error ? error.message : String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Validate that content is a well-formed X.509 certificate.
|
|
169
|
+
*
|
|
170
|
+
* @param content - PEM string or DER Uint8Array certificate data
|
|
171
|
+
* @param format - Certificate format: 'pem' or 'der'
|
|
172
|
+
* @returns true if the content is a valid X.509 certificate
|
|
173
|
+
*
|
|
174
|
+
* @see Requirement 6.5 — Reject invalid X.509 certificates
|
|
175
|
+
*/
|
|
176
|
+
async validateCertificate(content, format) {
|
|
177
|
+
try {
|
|
178
|
+
if (format === 'pem') {
|
|
179
|
+
if (typeof content !== 'string') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const trimmed = content.trim();
|
|
183
|
+
if (!trimmed.includes('-----BEGIN CERTIFICATE-----') ||
|
|
184
|
+
!trimmed.includes('-----END CERTIFICATE-----')) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
// Attempt to parse — will throw if malformed
|
|
188
|
+
new (await this.x509()).X509Certificate(content);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
if (!(content instanceof Uint8Array)) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
if (content.length === 0) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
// Attempt to parse — will throw if malformed
|
|
199
|
+
new (await this.x509()).X509Certificate(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Encrypt content using CMS/PKCS#7 enveloped-data for one or more recipients.
|
|
209
|
+
*
|
|
210
|
+
* Uses pkijs EnvelopedData with AES-256-CBC content encryption and
|
|
211
|
+
* RSA-OAEP (SHA-256) key transport per RFC 5751.
|
|
212
|
+
*
|
|
213
|
+
* @param content - Plaintext content to encrypt
|
|
214
|
+
* @param recipientCertificatesPem - PEM-encoded X.509 certificates of recipients
|
|
215
|
+
* @returns CMS enveloped-data result with DER-encoded content and MIME content type
|
|
216
|
+
* @throws EmailError with SMIME_ENCRYPT_FAILED on any failure
|
|
217
|
+
*
|
|
218
|
+
* @see Requirement 7.1 — CMS/PKCS#7 encryption per RFC 5751
|
|
219
|
+
* @see Requirement 7.3 — S/MIME encrypted message output (application/pkcs7-mime)
|
|
220
|
+
* @see Requirement 7.4 — Error on invalid/expired certificate
|
|
221
|
+
*/
|
|
222
|
+
async encrypt(content, recipientCertificatesPem) {
|
|
223
|
+
try {
|
|
224
|
+
const pkijs = await import('pkijs');
|
|
225
|
+
const asn1js = await import('asn1js');
|
|
226
|
+
// Set up the pkijs crypto engine
|
|
227
|
+
SmimeCertificateManager.ensureCryptoEngine(pkijs);
|
|
228
|
+
if (recipientCertificatesPem.length === 0) {
|
|
229
|
+
throw new Error('At least one recipient certificate is required');
|
|
230
|
+
}
|
|
231
|
+
// Parse each recipient PEM certificate into a pkijs Certificate
|
|
232
|
+
const recipientCerts = recipientCertificatesPem.map((pem, index) => {
|
|
233
|
+
try {
|
|
234
|
+
return SmimeCertificateManager.pemToPkijsCertificate(pem, asn1js, pkijs);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
throw new Error(`Failed to parse recipient certificate at index ${index}: ${err instanceof Error ? err.message : String(err)}`);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
// Create the EnvelopedData structure
|
|
241
|
+
const envelopedData = new pkijs.EnvelopedData();
|
|
242
|
+
// Add each recipient
|
|
243
|
+
for (const cert of recipientCerts) {
|
|
244
|
+
envelopedData.addRecipientByCertificate(cert, {
|
|
245
|
+
oaepHashAlgorithm: 'SHA-256',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// Encrypt the content with AES-256-CBC
|
|
249
|
+
await envelopedData.encrypt({ name: 'AES-CBC', length: 256 }, content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
250
|
+
// Wrap in ContentInfo (OID 1.2.840.113549.1.7.3 = enveloped-data)
|
|
251
|
+
const contentInfo = new pkijs.ContentInfo({
|
|
252
|
+
contentType: '1.2.840.113549.1.7.3',
|
|
253
|
+
content: envelopedData.toSchema(),
|
|
254
|
+
});
|
|
255
|
+
// Serialize to DER
|
|
256
|
+
const derBuffer = contentInfo.toSchema().toBER(false);
|
|
257
|
+
const encryptedContent = new Uint8Array(derBuffer);
|
|
258
|
+
return {
|
|
259
|
+
encryptedContent,
|
|
260
|
+
contentType: 'application/pkcs7-mime; smime-type=enveloped-data',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (error instanceof emailError_1.EmailError) {
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
throw new emailError_1.EmailError(emailErrorType_1.EmailErrorType.SMIME_ENCRYPT_FAILED, `S/MIME CMS encryption failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Decrypt CMS/PKCS#7 enveloped-data content using a recipient's certificate and private key.
|
|
272
|
+
*
|
|
273
|
+
* Parses the DER-encoded ContentInfo, extracts the EnvelopedData, and decrypts
|
|
274
|
+
* using the provided certificate and private key.
|
|
275
|
+
*
|
|
276
|
+
* @param encryptedContent - DER-encoded CMS/PKCS#7 enveloped-data
|
|
277
|
+
* @param certificatePem - PEM-encoded X.509 certificate of the recipient
|
|
278
|
+
* @param privateKeyPem - PEM-encoded PKCS#8 private key of the recipient
|
|
279
|
+
* @returns Decrypted plaintext content
|
|
280
|
+
* @throws EmailError with SMIME_DECRYPT_FAILED on any failure
|
|
281
|
+
*
|
|
282
|
+
* @see Requirement 9.1 — S/MIME decryption with private key
|
|
283
|
+
* @see Requirement 9.5 — Error when private key is missing/invalid
|
|
284
|
+
*/
|
|
285
|
+
async decrypt(encryptedContent, certificatePem, privateKeyPem) {
|
|
286
|
+
try {
|
|
287
|
+
const pkijs = await import('pkijs');
|
|
288
|
+
const asn1js = await import('asn1js');
|
|
289
|
+
// Set up the pkijs crypto engine
|
|
290
|
+
SmimeCertificateManager.ensureCryptoEngine(pkijs);
|
|
291
|
+
// Parse the DER content into ContentInfo
|
|
292
|
+
const contentBuffer = encryptedContent.buffer.slice(encryptedContent.byteOffset, encryptedContent.byteOffset + encryptedContent.byteLength);
|
|
293
|
+
const asn1Result = asn1js.fromBER(contentBuffer);
|
|
294
|
+
if (asn1Result.offset === -1) {
|
|
295
|
+
throw new Error('Failed to parse ASN.1 structure from encrypted content');
|
|
296
|
+
}
|
|
297
|
+
const contentInfo = new pkijs.ContentInfo({ schema: asn1Result.result });
|
|
298
|
+
const envelopedData = new pkijs.EnvelopedData({
|
|
299
|
+
schema: contentInfo.content,
|
|
300
|
+
});
|
|
301
|
+
// Parse the recipient's certificate
|
|
302
|
+
const recipientCert = SmimeCertificateManager.pemToPkijsCertificate(certificatePem, asn1js, pkijs);
|
|
303
|
+
// Import the recipient's private key
|
|
304
|
+
const privateKey = await SmimeCertificateManager.pemToPrivateKey(privateKeyPem);
|
|
305
|
+
// Find the matching recipient index
|
|
306
|
+
const recipientIndex = SmimeCertificateManager.findRecipientIndex(envelopedData, recipientCert);
|
|
307
|
+
// Decrypt the content
|
|
308
|
+
const decryptedBuffer = await envelopedData.decrypt(recipientIndex, {
|
|
309
|
+
recipientCertificate: recipientCert,
|
|
310
|
+
recipientPrivateKey: privateKey,
|
|
311
|
+
});
|
|
312
|
+
return new Uint8Array(decryptedBuffer);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error instanceof emailError_1.EmailError) {
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
throw new emailError_1.EmailError(emailErrorType_1.EmailErrorType.SMIME_DECRYPT_FAILED, `S/MIME CMS decryption failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Create a CMS detached signature over the given content.
|
|
323
|
+
*
|
|
324
|
+
* Uses pkijs SignedData with RSASSA-PKCS1-v1_5 / SHA-256 to produce
|
|
325
|
+
* a DER-encoded CMS detached signature conforming to RFC 5751.
|
|
326
|
+
*
|
|
327
|
+
* @param content - The content to sign
|
|
328
|
+
* @param certificatePem - PEM-encoded X.509 certificate of the signer
|
|
329
|
+
* @param privateKeyPem - PEM-encoded PKCS#8 private key of the signer
|
|
330
|
+
* @returns Signature result with DER-encoded CMS signature and signer subject
|
|
331
|
+
* @throws EmailError with SMIME_VERIFY_FAILED on any failure
|
|
332
|
+
*
|
|
333
|
+
* @see Requirement 8.1 — CMS detached signature production
|
|
334
|
+
* @see Requirement 8.2 — Sign before encrypt ordering
|
|
335
|
+
*/
|
|
336
|
+
async sign(content, certificatePem, privateKeyPem) {
|
|
337
|
+
try {
|
|
338
|
+
const pkijs = await import('pkijs');
|
|
339
|
+
const asn1js = await import('asn1js');
|
|
340
|
+
// Set up the pkijs crypto engine
|
|
341
|
+
SmimeCertificateManager.ensureCryptoEngine(pkijs);
|
|
342
|
+
// Parse the signer's PEM certificate into a pkijs Certificate
|
|
343
|
+
const signerCert = SmimeCertificateManager.pemToPkijsCertificate(certificatePem, asn1js, pkijs);
|
|
344
|
+
// Import the private key for signing (RSASSA-PKCS1-v1_5 with SHA-256)
|
|
345
|
+
const privateKey = await SmimeCertificateManager.pemToSigningKey(privateKeyPem);
|
|
346
|
+
// Create the SignedData structure with detached content
|
|
347
|
+
const signedData = new pkijs.SignedData({
|
|
348
|
+
encapContentInfo: new pkijs.EncapsulatedContentInfo({
|
|
349
|
+
eContentType: '1.2.840.113549.1.7.1', // id-data
|
|
350
|
+
}),
|
|
351
|
+
certificates: [signerCert],
|
|
352
|
+
signerInfos: [
|
|
353
|
+
new pkijs.SignerInfo({
|
|
354
|
+
sid: new pkijs.IssuerAndSerialNumber({
|
|
355
|
+
issuer: signerCert.issuer,
|
|
356
|
+
serialNumber: signerCert.serialNumber,
|
|
357
|
+
}),
|
|
358
|
+
}),
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
// Sign the content (detached — content is passed as data parameter, not embedded)
|
|
362
|
+
const contentBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
|
363
|
+
await signedData.sign(privateKey, 0, 'SHA-256', contentBuffer);
|
|
364
|
+
// Wrap in ContentInfo (OID 1.2.840.113549.1.7.2 = signed-data)
|
|
365
|
+
const contentInfo = new pkijs.ContentInfo({
|
|
366
|
+
contentType: '1.2.840.113549.1.7.2',
|
|
367
|
+
content: signedData.toSchema(true),
|
|
368
|
+
});
|
|
369
|
+
// Serialize to DER
|
|
370
|
+
const derBuffer = contentInfo.toSchema().toBER(false);
|
|
371
|
+
const signature = new Uint8Array(derBuffer);
|
|
372
|
+
// Extract the signer's subject from the certificate
|
|
373
|
+
const signerCertSubject = SmimeCertificateManager.extractPkijsCertSubject(signerCert);
|
|
374
|
+
return {
|
|
375
|
+
signature,
|
|
376
|
+
signerCertSubject,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (error instanceof emailError_1.EmailError) {
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
throw new emailError_1.EmailError(emailErrorType_1.EmailErrorType.SMIME_VERIFY_FAILED, `S/MIME CMS signing failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Verify a CMS detached signature against the given content and signer certificate.
|
|
388
|
+
*
|
|
389
|
+
* Parses the DER-encoded CMS signature, extracts the SignedData, and verifies
|
|
390
|
+
* using the provided signer certificate as a trusted certificate.
|
|
391
|
+
*
|
|
392
|
+
* @param content - The original content that was signed
|
|
393
|
+
* @param signature - DER-encoded CMS detached signature
|
|
394
|
+
* @param signerCertificatePem - PEM-encoded X.509 certificate of the expected signer
|
|
395
|
+
* @returns Verification result with valid status, signer subject, and optional reason
|
|
396
|
+
*
|
|
397
|
+
* @see Requirement 9.2 — S/MIME signature verification
|
|
398
|
+
* @see Requirement 9.3 — Verification result with signer subject and valid status
|
|
399
|
+
* @see Requirement 9.4 — Verification failure with reason
|
|
400
|
+
*/
|
|
401
|
+
async verify(content, signature, signerCertificatePem) {
|
|
402
|
+
try {
|
|
403
|
+
const pkijs = await import('pkijs');
|
|
404
|
+
const asn1js = await import('asn1js');
|
|
405
|
+
// Set up the pkijs crypto engine
|
|
406
|
+
SmimeCertificateManager.ensureCryptoEngine(pkijs);
|
|
407
|
+
// Parse the DER signature into ContentInfo
|
|
408
|
+
const sigBuffer = signature.buffer.slice(signature.byteOffset, signature.byteOffset + signature.byteLength);
|
|
409
|
+
const asn1Result = asn1js.fromBER(sigBuffer);
|
|
410
|
+
if (asn1Result.offset === -1) {
|
|
411
|
+
return {
|
|
412
|
+
valid: false,
|
|
413
|
+
reason: 'Failed to parse ASN.1 structure from signature',
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const contentInfo = new pkijs.ContentInfo({ schema: asn1Result.result });
|
|
417
|
+
const signedData = new pkijs.SignedData({ schema: contentInfo.content });
|
|
418
|
+
// Parse the signer's PEM certificate
|
|
419
|
+
const signerCert = SmimeCertificateManager.pemToPkijsCertificate(signerCertificatePem, asn1js, pkijs);
|
|
420
|
+
// Prepare the content buffer for verification
|
|
421
|
+
const contentBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
|
422
|
+
// Verify the signature
|
|
423
|
+
const verifyResult = await signedData.verify({
|
|
424
|
+
signer: 0,
|
|
425
|
+
data: contentBuffer,
|
|
426
|
+
trustedCerts: [signerCert],
|
|
427
|
+
});
|
|
428
|
+
const signerSubject = SmimeCertificateManager.extractPkijsCertSubject(signerCert);
|
|
429
|
+
if (verifyResult) {
|
|
430
|
+
return {
|
|
431
|
+
valid: true,
|
|
432
|
+
signerSubject,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
return {
|
|
437
|
+
valid: false,
|
|
438
|
+
signerSubject,
|
|
439
|
+
reason: 'CMS signature verification returned false',
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
// Verification failures should return a result, not throw
|
|
445
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
446
|
+
return {
|
|
447
|
+
valid: false,
|
|
448
|
+
reason: `CMS signature verification failed: ${reason}`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Extract metadata from an X.509 certificate.
|
|
454
|
+
*
|
|
455
|
+
* @param cert - The parsed X.509 certificate
|
|
456
|
+
* @returns Certificate metadata
|
|
457
|
+
*/
|
|
458
|
+
async extractMetadata(cert) {
|
|
459
|
+
const subject = cert.subject;
|
|
460
|
+
const issuer = cert.issuer;
|
|
461
|
+
const serialNumber = cert.serialNumber;
|
|
462
|
+
const validFrom = cert.notBefore;
|
|
463
|
+
const validTo = cert.notAfter;
|
|
464
|
+
const isExpired = validTo < new Date();
|
|
465
|
+
// Extract email addresses from Subject Alternative Name extension
|
|
466
|
+
const emailAddresses = await this.extractEmailAddresses(cert);
|
|
467
|
+
// Compute SHA-256 fingerprint
|
|
468
|
+
const fingerprint = await this.computeFingerprint(cert);
|
|
469
|
+
return {
|
|
470
|
+
subject,
|
|
471
|
+
issuer,
|
|
472
|
+
serialNumber,
|
|
473
|
+
validFrom,
|
|
474
|
+
validTo,
|
|
475
|
+
emailAddresses,
|
|
476
|
+
fingerprint,
|
|
477
|
+
isExpired,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Extract email addresses from the Subject Alternative Name extension.
|
|
482
|
+
*
|
|
483
|
+
* @param cert - The parsed X.509 certificate
|
|
484
|
+
* @returns Array of email addresses found in the SAN extension
|
|
485
|
+
*/
|
|
486
|
+
async extractEmailAddresses(cert) {
|
|
487
|
+
const emails = [];
|
|
488
|
+
try {
|
|
489
|
+
const x509Mod = await this.x509();
|
|
490
|
+
const sanExt = cert.getExtension(x509Mod.SubjectAlternativeNameExtension);
|
|
491
|
+
if (sanExt) {
|
|
492
|
+
for (const name of sanExt.names.items) {
|
|
493
|
+
if (name.type === 'email') {
|
|
494
|
+
emails.push(name.value);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// SAN extension not present or malformed — return empty array
|
|
501
|
+
}
|
|
502
|
+
return emails;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Compute SHA-256 fingerprint of a certificate.
|
|
506
|
+
*
|
|
507
|
+
* @param cert - The parsed X.509 certificate
|
|
508
|
+
* @returns Hex-encoded SHA-256 fingerprint
|
|
509
|
+
*/
|
|
510
|
+
async computeFingerprint(cert) {
|
|
511
|
+
const thumbprint = await cert.getThumbprint('SHA-256');
|
|
512
|
+
return Array.from(new Uint8Array(thumbprint))
|
|
513
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
514
|
+
.join('');
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Convert a string to ArrayBuffer (UTF-16 encoding as expected by pkijs).
|
|
518
|
+
*
|
|
519
|
+
* @param str - The string to convert
|
|
520
|
+
* @returns ArrayBuffer with UTF-16 encoded string
|
|
521
|
+
*/
|
|
522
|
+
static stringToArrayBuffer(str) {
|
|
523
|
+
const buf = new ArrayBuffer(str.length * 2);
|
|
524
|
+
const view = new Uint16Array(buf);
|
|
525
|
+
for (let i = 0; i < str.length; i++) {
|
|
526
|
+
view[i] = str.charCodeAt(i);
|
|
527
|
+
}
|
|
528
|
+
return buf;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Convert DER-encoded data to PEM format.
|
|
532
|
+
*
|
|
533
|
+
* @param der - DER-encoded binary data
|
|
534
|
+
* @param tag - PEM tag (e.g., 'CERTIFICATE', 'PRIVATE KEY')
|
|
535
|
+
* @returns PEM-encoded string
|
|
536
|
+
*/
|
|
537
|
+
static derToPem(der, tag) {
|
|
538
|
+
const base64 = Buffer.from(der).toString('base64');
|
|
539
|
+
const lines = [];
|
|
540
|
+
for (let i = 0; i < base64.length; i += 64) {
|
|
541
|
+
lines.push(base64.substring(i, i + 64));
|
|
542
|
+
}
|
|
543
|
+
return `-----BEGIN ${tag}-----\n${lines.join('\n')}\n-----END ${tag}-----`;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Ensure the pkijs crypto engine is initialized with WebCrypto.
|
|
547
|
+
*
|
|
548
|
+
* @param pkijs - The pkijs module
|
|
549
|
+
*/
|
|
550
|
+
static ensureCryptoEngine(pkijs) {
|
|
551
|
+
const crypto = globalThis.crypto;
|
|
552
|
+
pkijs.setEngine('webcrypto', new pkijs.CryptoEngine({ name: 'webcrypto', crypto }));
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Parse a PEM-encoded X.509 certificate into a pkijs Certificate object.
|
|
556
|
+
*
|
|
557
|
+
* @param pem - PEM-encoded certificate string
|
|
558
|
+
* @param asn1js - The asn1js module
|
|
559
|
+
* @param pkijs - The pkijs module
|
|
560
|
+
* @returns pkijs Certificate instance
|
|
561
|
+
*/
|
|
562
|
+
static pemToPkijsCertificate(pem, asn1js, pkijs) {
|
|
563
|
+
// Strip PEM headers and decode base64
|
|
564
|
+
const b64 = pem
|
|
565
|
+
.replace(/-----BEGIN CERTIFICATE-----/g, '')
|
|
566
|
+
.replace(/-----END CERTIFICATE-----/g, '')
|
|
567
|
+
.replace(/\s/g, '');
|
|
568
|
+
const derBuffer = Buffer.from(b64, 'base64');
|
|
569
|
+
const arrayBuffer = derBuffer.buffer.slice(derBuffer.byteOffset, derBuffer.byteOffset + derBuffer.byteLength);
|
|
570
|
+
const asn1Result = asn1js.fromBER(arrayBuffer);
|
|
571
|
+
if (asn1Result.offset === -1) {
|
|
572
|
+
throw new Error('Failed to parse certificate ASN.1 structure');
|
|
573
|
+
}
|
|
574
|
+
return new pkijs.Certificate({ schema: asn1Result.result });
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Import a PEM-encoded PKCS#8 private key as a CryptoKey for signing.
|
|
578
|
+
*
|
|
579
|
+
* Uses RSASSA-PKCS1-v1_5 with SHA-256, which is the standard algorithm
|
|
580
|
+
* for CMS/S/MIME digital signatures.
|
|
581
|
+
*
|
|
582
|
+
* @param pem - PEM-encoded PKCS#8 private key
|
|
583
|
+
* @returns CryptoKey suitable for signing
|
|
584
|
+
*/
|
|
585
|
+
static async pemToSigningKey(pem) {
|
|
586
|
+
const b64 = pem
|
|
587
|
+
.replace(/-----BEGIN PRIVATE KEY-----/g, '')
|
|
588
|
+
.replace(/-----END PRIVATE KEY-----/g, '')
|
|
589
|
+
.replace(/\s/g, '');
|
|
590
|
+
const derBuffer = Buffer.from(b64, 'base64');
|
|
591
|
+
const arrayBuffer = derBuffer.buffer.slice(derBuffer.byteOffset, derBuffer.byteOffset + derBuffer.byteLength);
|
|
592
|
+
return globalThis.crypto.subtle.importKey('pkcs8', arrayBuffer, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['sign']);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Extract the subject distinguished name string from a pkijs Certificate.
|
|
596
|
+
*
|
|
597
|
+
* @param cert - pkijs Certificate instance
|
|
598
|
+
* @returns Subject DN string (e.g., "CN=John Doe, O=Example Corp")
|
|
599
|
+
*/
|
|
600
|
+
static extractPkijsCertSubject(cert) {
|
|
601
|
+
try {
|
|
602
|
+
return cert.subject.typesAndValues
|
|
603
|
+
.map((tv) => {
|
|
604
|
+
const oid = tv.type;
|
|
605
|
+
const value = tv.value.valueBlock.value;
|
|
606
|
+
// Map common OIDs to readable names
|
|
607
|
+
const oidMap = {
|
|
608
|
+
'2.5.4.3': 'CN',
|
|
609
|
+
'2.5.4.6': 'C',
|
|
610
|
+
'2.5.4.7': 'L',
|
|
611
|
+
'2.5.4.8': 'ST',
|
|
612
|
+
'2.5.4.10': 'O',
|
|
613
|
+
'2.5.4.11': 'OU',
|
|
614
|
+
'1.2.840.113549.1.9.1': 'E',
|
|
615
|
+
};
|
|
616
|
+
const name = oidMap[oid] || oid;
|
|
617
|
+
return `${name}=${value}`;
|
|
618
|
+
})
|
|
619
|
+
.join(', ');
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
return 'Unknown Subject';
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Import a PEM-encoded PKCS#8 private key as a CryptoKey for decryption.
|
|
627
|
+
*
|
|
628
|
+
* Tries RSA-OAEP first, then falls back to ECDH if RSA import fails.
|
|
629
|
+
*
|
|
630
|
+
* @param pem - PEM-encoded PKCS#8 private key
|
|
631
|
+
* @returns CryptoKey suitable for decryption
|
|
632
|
+
*/
|
|
633
|
+
static async pemToPrivateKey(pem) {
|
|
634
|
+
const b64 = pem
|
|
635
|
+
.replace(/-----BEGIN PRIVATE KEY-----/g, '')
|
|
636
|
+
.replace(/-----END PRIVATE KEY-----/g, '')
|
|
637
|
+
.replace(/\s/g, '');
|
|
638
|
+
const derBuffer = Buffer.from(b64, 'base64');
|
|
639
|
+
const arrayBuffer = derBuffer.buffer.slice(derBuffer.byteOffset, derBuffer.byteOffset + derBuffer.byteLength);
|
|
640
|
+
// Try RSA-OAEP first (most common for S/MIME)
|
|
641
|
+
try {
|
|
642
|
+
return await globalThis.crypto.subtle.importKey('pkcs8', arrayBuffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['decrypt']);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// Fall back to RSA-OAEP with SHA-1 (legacy)
|
|
646
|
+
try {
|
|
647
|
+
return await globalThis.crypto.subtle.importKey('pkcs8', arrayBuffer, { name: 'RSA-OAEP', hash: 'SHA-1' }, true, ['decrypt']);
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
throw new Error('Failed to import private key: unsupported key algorithm');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Find the recipient index in an EnvelopedData structure that matches
|
|
656
|
+
* the given certificate.
|
|
657
|
+
*
|
|
658
|
+
* @param envelopedData - The pkijs EnvelopedData structure
|
|
659
|
+
* @param recipientCert - The pkijs Certificate to match
|
|
660
|
+
* @returns The zero-based index of the matching recipient
|
|
661
|
+
*/
|
|
662
|
+
static findRecipientIndex(envelopedData, recipientCert) {
|
|
663
|
+
// Try each recipient index — pkijs will match internally
|
|
664
|
+
// For KeyTransRecipientInfo, match by issuer+serialNumber
|
|
665
|
+
const recipientInfos = envelopedData.recipientInfos;
|
|
666
|
+
if (recipientInfos.length === 0) {
|
|
667
|
+
throw new Error('No recipient info found in enveloped data');
|
|
668
|
+
}
|
|
669
|
+
// Get the recipient cert's serial number for matching
|
|
670
|
+
const certSerial = recipientCert.serialNumber.valueBlock.toString();
|
|
671
|
+
const certIssuer = recipientCert.issuer.typesAndValues
|
|
672
|
+
.map((tv) => `${tv.type}=${tv.value.valueBlock.value}`)
|
|
673
|
+
.join(',');
|
|
674
|
+
for (let i = 0; i < recipientInfos.length; i++) {
|
|
675
|
+
const ri = recipientInfos[i];
|
|
676
|
+
// KeyTransRecipientInfo has rid (RecipientIdentifier)
|
|
677
|
+
if (ri.value && 'rid' in ri.value) {
|
|
678
|
+
const rid = ri.value.rid;
|
|
679
|
+
if (rid.issuer && rid.serialNumber) {
|
|
680
|
+
const riSerial = rid.serialNumber.valueBlock.toString();
|
|
681
|
+
const riIssuer = rid.issuer.typesAndValues
|
|
682
|
+
.map((tv) => `${tv.type}=${tv.value.valueBlock.value}`)
|
|
683
|
+
.join(',');
|
|
684
|
+
if (riSerial === certSerial && riIssuer === certIssuer) {
|
|
685
|
+
return i;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// If no exact match found, default to index 0 (single recipient case)
|
|
691
|
+
// pkijs decrypt will validate the match internally
|
|
692
|
+
return 0;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
exports.SmimeCertificateManager = SmimeCertificateManager;
|
|
696
|
+
//# sourceMappingURL=smimeCertificateManager.js.map
|