| 1 | /**************************************************************** |
| 2 | * Licensed to the Apache Software Foundation (ASF) under one * |
| 3 | * or more contributor license agreements. See the NOTICE file * |
| 4 | * distributed with this work for additional information * |
| 5 | * regarding copyright ownership. The ASF licenses this file * |
| 6 | * to you under the Apache License, Version 2.0 (the * |
| 7 | * "License"); you may not use this file except in compliance * |
| 8 | * with the License. You may obtain a copy of the License at * |
| 9 | * * |
| 10 | * http://www.apache.org/licenses/LICENSE-2.0 * |
| 11 | * * |
| 12 | * Unless required by applicable law or agreed to in writing, * |
| 13 | * software distributed under the License is distributed on an * |
| 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * |
| 15 | * KIND, either express or implied. See the License for the * |
| 16 | * specific language governing permissions and limitations * |
| 17 | * under the License. * |
| 18 | ****************************************************************/ |
| 19 | |
| 20 | package org.apache.james.jdkim.mailets; |
| 21 | |
| 22 | import java.io.ByteArrayInputStream; |
| 23 | import java.io.IOException; |
| 24 | import java.security.GeneralSecurityException; |
| 25 | import java.security.NoSuchAlgorithmException; |
| 26 | import java.security.PrivateKey; |
| 27 | import java.security.spec.InvalidKeySpecException; |
| 28 | import java.util.Enumeration; |
| 29 | import java.util.HashMap; |
| 30 | import java.util.Iterator; |
| 31 | import java.util.LinkedList; |
| 32 | import java.util.List; |
| 33 | import java.util.Map; |
| 34 | |
| 35 | import javax.mail.Header; |
| 36 | import javax.mail.MessagingException; |
| 37 | import javax.mail.internet.MimeMessage; |
| 38 | |
| 39 | import org.apache.commons.ssl.PKCS8Key; |
| 40 | import org.apache.james.jdkim.DKIMSigner; |
| 41 | import org.apache.james.jdkim.api.BodyHasher; |
| 42 | import org.apache.james.jdkim.api.Headers; |
| 43 | import org.apache.james.jdkim.api.SignatureRecord; |
| 44 | import org.apache.james.jdkim.exceptions.PermFailException; |
| 45 | import org.apache.mailet.Mail; |
| 46 | import org.apache.mailet.base.GenericMailet; |
| 47 | |
| 48 | /** |
| 49 | * This mailet sign a message using the DKIM protocol |
| 50 | * If the privateKey is encoded using a password then you can pass |
| 51 | * the password as privateKeyPassword parameter. |
| 52 | * |
| 53 | * Sample configuration: |
| 54 | * |
| 55 | * <pre><code> |
| 56 | * <mailet match="All" class="DKIMSign"> |
| 57 | * <signatureTemplate>v=1; s=selector; d=example.com; h=from:to:received:received; a=rsa-sha256; bh=; b=;</signatureTemplate> |
| 58 | * <privateKey> |
| 59 | * -----BEGIN RSA PRIVATE KEY----- |
| 60 | * MIICXAIBAAKBgQDYDaYKXzwVYwqWbLhmuJ66aTAN8wmDR+rfHE8HfnkSOax0oIoT |
| 61 | * M5zquZrTLo30870YMfYzxwfB6j/Nz3QdwrUD/t0YMYJiUKyWJnCKfZXHJBJ+yfRH |
| 62 | * r7oW+UW3cVo9CG2bBfIxsInwYe175g9UjyntJpWueqdEIo1c2bhv9Mp66QIDAQAB |
| 63 | * AoGBAI8XcwnZi0Sq5N89wF+gFNhnREFo3rsJDaCY8iqHdA5DDlnr3abb/yhipw0I |
| 64 | * /1HlgC6fIG2oexXOXFWl+USgqRt1kTt9jXhVFExg8mNko2UelAwFtsl8CRjVcYQO |
| 65 | * cedeH/WM/mXjg2wUqqZenBmlKlD6vNb70jFJeVaDJ/7n7j8BAkEA9NkH2D4Zgj/I |
| 66 | * OAVYccZYH74+VgO0e7VkUjQk9wtJ2j6cGqJ6Pfj0roVIMUWzoBb8YfErR8l6JnVQ |
| 67 | * bfy83gJeiQJBAOHk3ow7JjAn8XuOyZx24KcTaYWKUkAQfRWYDFFOYQF4KV9xLSEt |
| 68 | * ycY0kjsdxGKDudWcsATllFzXDCQF6DTNIWECQEA52ePwTjKrVnLTfCLEG4OgHKvl |
| 69 | * Zud4amthwDyJWoMEH2ChNB2je1N4JLrABOE+hk+OuoKnKAKEjWd8f3Jg/rkCQHj8 |
| 70 | * mQmogHqYWikgP/FSZl518jV48Tao3iXbqvU9Mo2T6yzYNCCqIoDLFWseNVnCTZ0Q |
| 71 | * b+IfiEf1UeZVV5o4J+ECQDatNnS3V9qYUKjj/krNRD/U0+7eh8S2ylLqD3RlSn9K |
| 72 | * tYGRMgAtUXtiOEizBH6bd/orzI9V9sw8yBz+ZqIH25Q= |
| 73 | * -----END RSA PRIVATE KEY----- |
| 74 | * </privateKey> |
| 75 | * </mailet> |
| 76 | * </code></pre> |
| 77 | * |
| 78 | * @version CVS $Revision: 713949 $ $Date: 2008-11-14 08:40:21 +0100 (ven, 14 |
| 79 | * nov 2008) $ |
| 80 | * @since 2.2.0 |
| 81 | */ |
| 82 | public class DKIMSign extends GenericMailet { |
| 83 | |
| 84 | /** |
| 85 | * An adapter to let DKIMSigner read headers from MimeMessage |
| 86 | */ |
| 87 | private final class MimeMessageHeaders implements Headers { |
| 88 | |
| 89 | private Map/* String, String */headers; |
| 90 | private List/* String */fields; |
| 91 | |
| 92 | public MimeMessageHeaders(MimeMessage message) |
| 93 | throws MessagingException { |
| 94 | headers = new HashMap(); |
| 95 | fields = new LinkedList(); |
| 96 | for (Enumeration e = message.getAllHeaderLines(); e |
| 97 | .hasMoreElements();) { |
| 98 | String head = (String) e.nextElement(); |
| 99 | int p = head.indexOf(':'); |
| 100 | if (p <= 0) |
| 101 | throw new MessagingException("Bad header line: " + head); |
| 102 | String headerName = head.substring(0, p).trim(); |
| 103 | String headerNameLC = headerName.toLowerCase(); |
| 104 | fields.add(headerName); |
| 105 | List/* String */strings = (List) headers.get(headerNameLC); |
| 106 | if (strings == null) { |
| 107 | strings = new LinkedList(); |
| 108 | headers.put(headerNameLC, strings); |
| 109 | } |
| 110 | strings.add(head); |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | public List getFields() { |
| 115 | return fields; |
| 116 | } |
| 117 | |
| 118 | public List getFields(String name) { |
| 119 | return (List) headers.get(name); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | private String signatureTemplate; |
| 124 | private PrivateKey privateKey; |
| 125 | |
| 126 | public void init() throws MessagingException { |
| 127 | signatureTemplate = getInitParameter("signatureTemplate"); |
| 128 | String privateKeyString = getInitParameter("privateKey"); |
| 129 | String privateKeyPassword = getInitParameter("privateKeyPassword", null); |
| 130 | try { |
| 131 | PKCS8Key pkcs8 = new PKCS8Key(new ByteArrayInputStream( |
| 132 | privateKeyString.getBytes()), |
| 133 | privateKeyPassword != null ? privateKeyPassword |
| 134 | .toCharArray() : null); |
| 135 | privateKey = pkcs8.getPrivateKey(); |
| 136 | // privateKey = DKIMSigner.getPrivateKey(privateKeyString); |
| 137 | } catch (NoSuchAlgorithmException e) { |
| 138 | throw new MessagingException("Unknown private key algorythm: " |
| 139 | + e.getMessage(), e); |
| 140 | } catch (InvalidKeySpecException e) { |
| 141 | throw new MessagingException( |
| 142 | "PrivateKey should be in base64 encoded PKCS8 (der) format: " |
| 143 | + e.getMessage(), e); |
| 144 | } catch (GeneralSecurityException e) { |
| 145 | throw new MessagingException("General security exception: " |
| 146 | + e.getMessage(), e); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | public void service(Mail mail) throws MessagingException { |
| 151 | DKIMSigner signer = new DKIMSigner(signatureTemplate, privateKey); |
| 152 | SignatureRecord signRecord = signer |
| 153 | .newSignatureRecordTemplate(signatureTemplate); |
| 154 | try { |
| 155 | BodyHasher bhj = signer.newBodyHasher(signRecord); |
| 156 | MimeMessage message = mail.getMessage(); |
| 157 | Headers headers = new MimeMessageHeaders(message); |
| 158 | try { |
| 159 | message.writeTo(new HeaderSkippingOutputStream(bhj |
| 160 | .getOutputStream())); |
| 161 | bhj.getOutputStream().close(); |
| 162 | } catch (IOException e) { |
| 163 | throw new MessagingException("Exception calculating bodyhash: " |
| 164 | + e.getMessage(), e); |
| 165 | } |
| 166 | String signatureHeader = signer.sign(headers, bhj); |
| 167 | // Unfortunately JavaMail does not give us a method to add headers |
| 168 | // on top. |
| 169 | // message.addHeaderLine(signatureHeader); |
| 170 | prependHeader(message, signatureHeader); |
| 171 | } catch (PermFailException e) { |
| 172 | throw new MessagingException("PermFail while signing: " |
| 173 | + e.getMessage(), e); |
| 174 | } |
| 175 | |
| 176 | } |
| 177 | |
| 178 | private void prependHeader(MimeMessage message, String signatureHeader) |
| 179 | throws MessagingException { |
| 180 | List prevHeader = new LinkedList(); |
| 181 | // read all the headers |
| 182 | for (Enumeration e = message.getAllHeaderLines(); e.hasMoreElements();) { |
| 183 | String headerLine = (String) e.nextElement(); |
| 184 | prevHeader.add(headerLine); |
| 185 | } |
| 186 | // remove all the headers |
| 187 | for (Enumeration e = message.getAllHeaders(); e.hasMoreElements();) { |
| 188 | Header header = (Header) e.nextElement(); |
| 189 | message.removeHeader(header.getName()); |
| 190 | } |
| 191 | // add our header |
| 192 | message.addHeaderLine(signatureHeader); |
| 193 | // add the remaining headers using "addHeaderLine" that won't alter the |
| 194 | // insertion order. |
| 195 | for (Iterator i = prevHeader.iterator(); i.hasNext();) { |
| 196 | String header = (String) i.next(); |
| 197 | message.addHeaderLine(header); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | } |