Skip to content

Commit c68044a

Browse files
committed
#252: Fix support for legacy S/MIME signing envelope
1 parent 512ccbb commit c68044a

File tree

9 files changed

+232
-62
lines changed

9 files changed

+232
-62
lines changed
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package org.simplejavamail.api.internal.smimesupport.builder;
22

3+
import org.jetbrains.annotations.NotNull;
4+
import org.jetbrains.annotations.Nullable;
35
import org.simplejavamail.api.email.AttachmentResource;
46
import org.simplejavamail.api.email.OriginalSmimeDetails;
7+
import org.simplejavamail.api.internal.smimesupport.model.AttachmentDecryptionResult;
58

69
import java.util.List;
710

811
public interface SmimeParseResult {
9-
OriginalSmimeDetails getOriginalSmimeDetails();
10-
AttachmentResource getSmimeSignedEmail();
11-
List<AttachmentResource> getDecryptedAttachments();
12-
}
12+
@NotNull OriginalSmimeDetails getOriginalSmimeDetails();
13+
@Nullable AttachmentResource getSmimeSignedEmail();
14+
@NotNull List<AttachmentDecryptionResult> getDecryptedAttachmentResults();
15+
@NotNull List<AttachmentResource> getDecryptedAttachments();
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.simplejavamail.api.internal.smimesupport.model;
2+
3+
import org.simplejavamail.api.email.AttachmentResource;
4+
import org.simplejavamail.api.email.OriginalSmimeDetails;
5+
6+
/**
7+
* Used by the S/MIME module to return the decrypted content as well as the indication of how the content was encrypted / signed.
8+
*/
9+
public interface AttachmentDecryptionResult {
10+
OriginalSmimeDetails.SmimeMode getSmimeMode();
11+
AttachmentResource getAttachmentResource();
12+
}

modules/core-module/src/main/java/org/simplejavamail/internal/modules/SMIMEModule.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.simplejavamail.api.email.OriginalSmimeDetails;
66
import org.simplejavamail.api.internal.outlooksupport.model.OutlookMessage;
77
import org.simplejavamail.api.internal.smimesupport.builder.SmimeParseResult;
8+
import org.simplejavamail.api.internal.smimesupport.model.AttachmentDecryptionResult;
89
import org.simplejavamail.api.internal.smimesupport.model.SmimeDetails;
910
import org.simplejavamail.api.mailer.config.Pkcs12Config;
1011

@@ -37,7 +38,7 @@ public interface SMIMEModule {
3738
* @return A copy of given original 'true' attachments, with S/MIME encrypted / signed attachments replaced with the actual attachment.
3839
*/
3940
@NotNull
40-
List<AttachmentResource> decryptAttachments(@NotNull List<AttachmentResource> attachments, @Nullable Pkcs12Config pkcs12Config, @NotNull OriginalSmimeDetails messageSmimeDetails);
41+
List<AttachmentDecryptionResult> decryptAttachments(@NotNull List<AttachmentResource> attachments, @Nullable Pkcs12Config pkcs12Config, @NotNull OriginalSmimeDetails messageSmimeDetails);
4142

4243
/**
4344
* @return Whether the given attachment is S/MIME signed / encrypted. Defers to {@code SmimeRecognitionUtil.isSmimeAttachment(..)}.

modules/simple-java-mail/src/main/java/org/simplejavamail/converter/EmailConverter.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.simplejavamail.converter;
22

3+
import org.jetbrains.annotations.NotNull;
4+
import org.jetbrains.annotations.Nullable;
35
import org.simplejavamail.api.email.CalendarMethod;
46
import org.simplejavamail.api.email.Email;
57
import org.simplejavamail.api.email.EmailPopulatingBuilder;
@@ -19,8 +21,6 @@
1921
import org.simplejavamail.internal.smimesupport.model.OriginalSmimeDetailsImpl;
2022

2123
import javax.activation.DataSource;
22-
import org.jetbrains.annotations.NotNull;
23-
import org.jetbrains.annotations.Nullable;
2424
import javax.mail.MessagingException;
2525
import javax.mail.Session;
2626
import javax.mail.internet.InternetAddress;
@@ -217,7 +217,7 @@ private static EmailPopulatingBuilder decryptAttachments(final EmailPopulatingBu
217217
if (ModuleLoader.smimeModuleAvailable()) {
218218
SmimeParseResult smimeParseResult = loadSmimeModule().decryptAttachments(emailBuilder.getAttachments(), outlookMessage, pkcs12Config);
219219
handleSmimeParseResult((InternalEmailPopulatingBuilder) emailBuilder, smimeParseResult);
220-
updateEmailIfBothSignedAndEncrypted(emailBuilder);
220+
updateEmailIfBothSignedAndEncrypted(emailBuilder, smimeParseResult);
221221
}
222222
return emailBuilder;
223223
}
@@ -226,7 +226,7 @@ private static EmailPopulatingBuilder decryptAttachments(final EmailPopulatingBu
226226
if (ModuleLoader.smimeModuleAvailable()) {
227227
SmimeParseResult smimeParseResult = loadSmimeModule().decryptAttachments(emailBuilder.getAttachments(), mimeMessage, pkcs12Config);
228228
handleSmimeParseResult((InternalEmailPopulatingBuilder) emailBuilder, smimeParseResult);
229-
updateEmailIfBothSignedAndEncrypted(emailBuilder);
229+
updateEmailIfBothSignedAndEncrypted(emailBuilder, smimeParseResult);
230230
}
231231
return emailBuilder;
232232
}
@@ -235,12 +235,15 @@ private static EmailPopulatingBuilder decryptAttachments(final EmailPopulatingBu
235235
* if we have both an encrypted and signed part in the email, have the
236236
* top-level email reflect this as {@link SmimeMode#SIGNED_ENCRYPTED}.
237237
*/
238-
private static void updateEmailIfBothSignedAndEncrypted(final EmailPopulatingBuilder emailBuilder) {
238+
private static void updateEmailIfBothSignedAndEncrypted(final EmailPopulatingBuilder emailBuilder, final SmimeParseResult smimeParseResult) {
239239
if (emailBuilder.getSmimeSignedEmail() != null) {
240240
OriginalSmimeDetails nestedSmime = emailBuilder.getSmimeSignedEmail().getOriginalSmimeDetails();
241241
OriginalSmimeDetailsImpl originalSmimeDetails = (OriginalSmimeDetailsImpl) emailBuilder.getOriginalSmimeDetails();
242242
if (nestedSmime.getSmimeMode() != PLAIN && nestedSmime.getSmimeMode() != originalSmimeDetails.getSmimeMode()) {
243243
originalSmimeDetails.completeWithSmimeMode(SmimeMode.SIGNED_ENCRYPTED);
244+
} else if (smimeParseResult.getDecryptedAttachmentResults().size() == 1) {
245+
final SmimeMode smimeMode = smimeParseResult.getDecryptedAttachmentResults().get(0).getSmimeMode();
246+
originalSmimeDetails.completeWithSmimeMode(smimeMode);
244247
}
245248
}
246249
}

modules/simple-java-mail/src/main/java/org/simplejavamail/converter/internal/mimemessage/MimeMessageParser.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,7 @@ private static String parseResourceNameOrUnnamed(@Nullable final String possible
269269
@NotNull
270270
private static String parseResourceName(@Nullable String possibleWrappedContentID, @NotNull String fileName) {
271271
if (valueNullOrEmpty(fileName) && !valueNullOrEmpty(possibleWrappedContentID)) {
272-
// https://regex101.com/r/46ulb2/1
273-
return possibleWrappedContentID.replaceAll("^<?(.*?)>?$", "$1");
272+
return possibleWrappedContentID.replaceAll("^<?(.*?)>?$", "$1"); // https://regex101.com/r/46ulb2/1
274273
} else {
275274
return fileName;
276275
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.simplejavamail.internal.smimesupport;
2+
3+
import org.simplejavamail.api.email.AttachmentResource;
4+
import org.simplejavamail.api.email.OriginalSmimeDetails;
5+
import org.simplejavamail.api.internal.smimesupport.model.AttachmentDecryptionResult;
6+
7+
// FIXME lombok
8+
class AttachmentDecryptionResultImpl implements AttachmentDecryptionResult {
9+
private final OriginalSmimeDetails.SmimeMode smimeMode;
10+
private final AttachmentResource attachmentResource;
11+
12+
public AttachmentDecryptionResultImpl(final OriginalSmimeDetails.SmimeMode smimeMode, final AttachmentResource attachmentResource) {
13+
this.smimeMode = smimeMode;
14+
this.attachmentResource = attachmentResource;
15+
}
16+
17+
public OriginalSmimeDetails.SmimeMode getSmimeMode() {
18+
return smimeMode;
19+
}
20+
21+
public AttachmentResource getAttachmentResource() {
22+
return attachmentResource;
23+
}
24+
}

modules/smime-module/src/main/java/org/simplejavamail/internal/smimesupport/SMIMESupport.java

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import net.markenwerk.utils.mail.smime.SmimeKey;
44
import net.markenwerk.utils.mail.smime.SmimeKeyStore;
5-
import net.markenwerk.utils.mail.smime.SmimeState;
65
import net.markenwerk.utils.mail.smime.SmimeUtil;
76
import org.bouncycastle.asn1.x500.RDN;
87
import org.bouncycastle.asn1.x500.X500Name;
@@ -29,9 +28,11 @@
2928
import org.simplejavamail.api.internal.outlooksupport.model.OutlookMessage;
3029
import org.simplejavamail.api.internal.outlooksupport.model.OutlookSmime.OutlookSmimeApplicationSmime;
3130
import org.simplejavamail.api.internal.outlooksupport.model.OutlookSmime.OutlookSmimeMultipartSigned;
31+
import org.simplejavamail.api.internal.smimesupport.model.AttachmentDecryptionResult;
3232
import org.simplejavamail.api.internal.smimesupport.model.SmimeDetails;
3333
import org.simplejavamail.api.mailer.config.Pkcs12Config;
3434
import org.simplejavamail.internal.modules.SMIMEModule;
35+
import org.simplejavamail.internal.smimesupport.SmimeUtilFixed.SmimeStateFixed;
3536
import org.simplejavamail.internal.smimesupport.builder.SmimeParseResultBuilder;
3637
import org.simplejavamail.internal.smimesupport.model.OriginalSmimeDetailsImpl;
3738
import org.simplejavamail.internal.smimesupport.model.SmimeDetailsImpl;
@@ -59,8 +60,6 @@
5960

6061
import static java.lang.String.format;
6162
import static java.util.Arrays.asList;
62-
import static net.markenwerk.utils.mail.smime.SmimeState.ENCRYPTED;
63-
import static net.markenwerk.utils.mail.smime.SmimeState.SIGNED;
6463
import static org.simplejavamail.internal.smimesupport.SmimeException.ERROR_DECRYPTING_SMIME_SIGNED_ATTACHMENT;
6564
import static org.simplejavamail.internal.smimesupport.SmimeException.ERROR_DETERMINING_SMIME_SIGNER;
6665
import static org.simplejavamail.internal.smimesupport.SmimeException.ERROR_EXTRACTING_SIGNEDBY_FROM_SMIME_SIGNED_ATTACHMENT;
@@ -171,15 +170,15 @@ private boolean checkSignature(@NotNull final MimeMessage mimeMessage, @Nullable
171170
private void decryptAttachments(@NotNull final SmimeParseResultBuilder smimeBuilder, @NotNull final List<AttachmentResource> attachments,
172171
@Nullable final Pkcs12Config pkcs12Config) {
173172
LOGGER.debug("checking for S/MIME signed / encrypted attachments...");
174-
List<AttachmentResource> decryptedAttachments = decryptAttachments(attachments, pkcs12Config, smimeBuilder.getOriginalSmimeDetails());
173+
List<AttachmentDecryptionResult> decryptedAttachments = decryptAttachments(attachments, pkcs12Config, smimeBuilder.getOriginalSmimeDetails());
175174
smimeBuilder.addDecryptedAttachments(decryptedAttachments);
176175

177176
if (attachments.size() == 1) {
178177
final AttachmentResource onlyAttachment = attachments.get(0);
179-
final AttachmentResource onlyAttachmentDecrypted = smimeBuilder.getDecryptedAttachments().get(0);
180-
if (isSmimeAttachment(onlyAttachment) && isMimeMessageAttachment(onlyAttachmentDecrypted)) {
178+
final AttachmentDecryptionResult onlyAttachmentDecrypted = smimeBuilder.getDecryptedAttachmentResults().get(0);
179+
if (isSmimeAttachment(onlyAttachment) && isMimeMessageAttachment(onlyAttachmentDecrypted.getAttachmentResource())) {
181180
smimeBuilder.getOriginalSmimeDetails().completeWith(determineSmimeDetails(onlyAttachment));
182-
smimeBuilder.setSmimeSignedEmailToProcess(onlyAttachmentDecrypted);
181+
smimeBuilder.setSmimeSignedEmailToProcess(onlyAttachmentDecrypted.getAttachmentResource());
183182
}
184183
}
185184
}
@@ -204,24 +203,24 @@ private OriginalSmimeDetailsImpl determineSmimeDetails(final AttachmentResource
204203
*/
205204
@NotNull
206205
@Override
207-
public List<AttachmentResource> decryptAttachments(
206+
public List<AttachmentDecryptionResult> decryptAttachments(
208207
@NotNull final List<AttachmentResource> attachments,
209208
@Nullable final Pkcs12Config pkcs12Config,
210209
@NotNull final OriginalSmimeDetails messageSmimeDetails) {
211-
final List<AttachmentResource> decryptedAttachments;
212-
decryptedAttachments = new ArrayList<>(attachments);
213-
214-
for (int i = 0; i < decryptedAttachments.size(); i++) {
215-
final AttachmentResource attachment = decryptedAttachments.get(i);
210+
final List<AttachmentDecryptionResult> decryptedAttachments = new ArrayList<>();
211+
for (final AttachmentResource attachment : attachments) {
216212
if (isSmimeAttachment(attachment)) {
217213
try {
218214
LOGGER.debug("decrypting S/MIME signed attachment '{}'...", attachment.getName());
219-
decryptedAttachments.set(i, decryptAndUnsignAttachment(attachment, pkcs12Config, messageSmimeDetails));
215+
decryptedAttachments.add(decryptAndUnsignAttachment(attachment, pkcs12Config, messageSmimeDetails));
220216
} catch (Exception e) {
221217
throw new SmimeException(format(ERROR_DECRYPTING_SMIME_SIGNED_ATTACHMENT, attachment), e);
222218
}
219+
} else {
220+
decryptedAttachments.add(new AttachmentDecryptionResultImpl(SmimeMode.PLAIN, attachment));
223221
}
224222
}
223+
225224
return decryptedAttachments;
226225
}
227226

@@ -233,42 +232,58 @@ public boolean isSmimeAttachment(@NotNull final AttachmentResource attachment) {
233232
return SMIME_MIMETYPES.contains(attachment.getDataSource().getContentType());
234233
}
235234

236-
private AttachmentResource decryptAndUnsignAttachment(
235+
private AttachmentDecryptionResult decryptAndUnsignAttachment(
237236
@NotNull final AttachmentResource attachment,
238237
@Nullable final Pkcs12Config pkcs12Config,
239238
@NotNull final OriginalSmimeDetails messageSmimeDetails) {
240239
try {
241-
final InternetHeaders internetHeaders = new InternetHeaders();
242-
internetHeaders.addHeader("Content-Type", restoreSmimeContentType(attachment, messageSmimeDetails));
243-
final MimeBodyPart mimeBodyPart = new MimeBodyPart(internetHeaders, attachment.readAllBytes());
244-
245-
AttachmentResource liberatedContent = null;
246-
247-
SmimeState smimeState = determineStatus(mimeBodyPart, messageSmimeDetails);
248-
if (smimeState == SIGNED) {
249-
if (SmimeUtil.checkSignature(mimeBodyPart)) {
250-
MimeBodyPart liberatedBodyPart = SmimeUtil.getSignedContent(mimeBodyPart);
251-
liberatedContent = handleLiberatedContent(liberatedBodyPart.getContent());
252-
} else {
253-
LOGGER.warn("Content is S/MIME signed, but signature is not valid; skipping S/MIME interpeter...");
254-
}
255-
} else if (smimeState == ENCRYPTED) {
256-
if (pkcs12Config != null) {
257-
LOGGER.warn("Message was encrypted, but no Pkcs12Config was given to decrypt it with, skipping attachment...");
258-
SmimeKey smimeKey = retrieveSmimeKeyFromPkcs12Keystore(pkcs12Config);
259-
MimeBodyPart liberatedBodyPart = SmimeUtil.decrypt(mimeBodyPart, smimeKey);
260-
liberatedContent = handleLiberatedContent(liberatedBodyPart.getContent());
261-
}
240+
final MimeBodyPart mimeBodyPart = new MimeBodyPart(new InternetHeaders(), attachment.readAllBytes());
241+
mimeBodyPart.addHeader("Content-Type", restoreSmimeContentType(attachment, messageSmimeDetails));
242+
243+
AttachmentDecryptionResult liberatedContent = null;
244+
245+
final SmimeStateFixed smimeState = determineStatus(mimeBodyPart, messageSmimeDetails);
246+
if (smimeState == SmimeStateFixed.ENCRYPTED) {
247+
liberatedContent = getEncryptedContent(pkcs12Config, mimeBodyPart);
248+
} else if (smimeState == SmimeStateFixed.SIGNED) {
249+
liberatedContent = getSignedContent(mimeBodyPart);
262250
}
263251

264-
return liberatedContent != null
265-
? decryptAndUnsignAttachment(liberatedContent, pkcs12Config, messageSmimeDetails)
266-
: attachment;
252+
return liberatedContent != null ? liberatedContent : new AttachmentDecryptionResultImpl(SmimeMode.PLAIN, attachment);
267253
} catch (MessagingException | IOException e) {
268254
throw new SmimeException(format(ERROR_DECRYPTING_SMIME_SIGNED_ATTACHMENT, attachment), e);
269255
}
270256
}
271257

258+
@Nullable
259+
private AttachmentDecryptionResult getEncryptedContent(final @Nullable Pkcs12Config pkcs12Config, final MimeBodyPart mimeBodyPart)
260+
throws MessagingException, IOException {
261+
if (pkcs12Config != null) {
262+
MimeBodyPart liberatedBodyPart = SmimeUtil.decrypt(mimeBodyPart, retrieveSmimeKeyFromPkcs12Keystore(pkcs12Config));
263+
if (SmimeUtilFixed.getStatus(liberatedBodyPart) == SmimeStateFixed.SIGNED_ENVELOPED) {
264+
final AttachmentDecryptionResult signedContent = getSignedContent(liberatedBodyPart);
265+
if (signedContent != null) {
266+
return new AttachmentDecryptionResultImpl(SmimeMode.SIGNED_ENCRYPTED, signedContent.getAttachmentResource());
267+
}
268+
// apparently the sign was invalid, so ignore and continue with the decrypted attachment instead
269+
}
270+
return new AttachmentDecryptionResultImpl(SmimeMode.ENCRYPTED, handleLiberatedContent(liberatedBodyPart.getContent()));
271+
}
272+
LOGGER.warn("Message was encrypted, but no Pkcs12Config was given to decrypt it with, skipping attachment...");
273+
return null;
274+
}
275+
276+
@Nullable
277+
private AttachmentDecryptionResult getSignedContent(final MimeBodyPart mimeBodyPart)
278+
throws MessagingException, IOException {
279+
if (SmimeUtil.checkSignature(mimeBodyPart)) {
280+
MimeBodyPart liberatedBodyPart = SmimeUtil.getSignedContent(mimeBodyPart);
281+
return new AttachmentDecryptionResultImpl(SmimeMode.SIGNED, handleLiberatedContent(liberatedBodyPart.getContent()));
282+
}
283+
LOGGER.warn("Content is S/MIME signed, but signature is not valid; skipping S/MIME interpeter...");
284+
return null;
285+
}
286+
272287
private String restoreSmimeContentType(@NotNull final AttachmentResource attachment, final OriginalSmimeDetails originalSmimeDetails) {
273288
String contentType = attachment.getDataSource().getContentType();
274289
if (contentType.contains("multipart/signed") && !contentType.contains("protocol") && originalSmimeDetails.getSmimeProtocol() != null) {
@@ -299,10 +314,13 @@ protected void updateMessageID() throws MessagingException {
299314
return null;
300315
}
301316

302-
private SmimeState determineStatus(@NotNull final MimePart mimeBodyPart, @NotNull final OriginalSmimeDetails messageSmimeDetails) {
303-
SmimeState status = SmimeUtil.getStatus(mimeBodyPart);
304-
boolean trustStatus = status != ENCRYPTED || messageSmimeDetails.getSmimeMode() == SmimeMode.PLAIN;
305-
return trustStatus ? status : "signed-data".equals(messageSmimeDetails.getSmimeType()) ? SIGNED : ENCRYPTED;
317+
private SmimeStateFixed determineStatus(@NotNull final MimePart mimeBodyPart, @NotNull final OriginalSmimeDetails messageSmimeDetails) {
318+
SmimeStateFixed status = SmimeUtilFixed.getStatus(mimeBodyPart);
319+
boolean trustStatus = status != SmimeStateFixed.ENCRYPTED || messageSmimeDetails.getSmimeMode() == SmimeMode.PLAIN;
320+
if (trustStatus) {
321+
return status;
322+
}
323+
return "signed-data".equals(messageSmimeDetails.getSmimeType()) ? SmimeStateFixed.SIGNED : SmimeStateFixed.ENCRYPTED;
306324
}
307325

308326
/**
@@ -346,7 +364,7 @@ public String getSignedByAddress(@NotNull MimePart mimePart) {
346364
}
347365

348366
public boolean verifyValidSignature(@NotNull MimeMessage mimeMessage, @NotNull OriginalSmimeDetails messageSmimeDetails) {
349-
return determineStatus(mimeMessage, messageSmimeDetails) != SIGNED || SmimeUtil.checkSignature(mimeMessage);
367+
return determineStatus(mimeMessage, messageSmimeDetails) != SmimeStateFixed.SIGNED || SmimeUtil.checkSignature(mimeMessage);
350368
}
351369

352370
@NotNull

0 commit comments

Comments
 (0)