All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.markenwerk.utils.mail.dkim.DkimSigner Maven / Gradle / Ivy

There is a newer version: 2.0.1
Show newest version
/*
 * Copyright (c) 2015 Torsten Krause, Markenwerk GmbH.
 * 
 * This file is part of 'A DKIM library for JavaMail', hereafter
 * called 'this library', identified by the following coordinates:
 * 
 *    groupID: net.markenwerk
 *    artifactId: utils-mail-dkim
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 * 
 * See the LICENSE and NOTICE files in the root directory for further
 * information.
 * 
 * This file incorporates work covered by the following copyright and  
 * permission notice:
 *  
 *    Copyright 2008 The Apache Software Foundation or its licensors, as
 *    applicable.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 *
 *    A licence was granted to the ASF by Florian Sager on 30 November 2008
 */
package net.markenwerk.utils.mail.dkim;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.Header;
import javax.mail.MessagingException;

import com.sun.mail.util.CRLFOutputStream;
import com.sun.mail.util.QPEncoderStream;
import net.iharder.Base64;

import net.markenwerk.utils.data.fetcher.Fetcher;

/**
 * Main class providing a signature according to DKIM RFC 4871.
 * 
 * @author Torsten Krause (tk at markenwerk dot net)
 * @author Florian Sager
 * @since 1.0.0
 */
public class DkimSigner {

	private static final String DKIM_SIGNATUR_HEADER = "DKIM-Signature";
	private static final int MAX_HEADER_LENGTH = 67;

	private static final List MIMIMUM_HEADERS_TO_SIGN = new ArrayList(3);
	private static final List DEFAULT_HEADERS_TO_SIGN = new ArrayList(28);

	static {
		MIMIMUM_HEADERS_TO_SIGN.add("From");
		MIMIMUM_HEADERS_TO_SIGN.add("To");
		MIMIMUM_HEADERS_TO_SIGN.add("Subject");

		DEFAULT_HEADERS_TO_SIGN.addAll(MIMIMUM_HEADERS_TO_SIGN);
		DEFAULT_HEADERS_TO_SIGN.add("Content-Description");
		DEFAULT_HEADERS_TO_SIGN.add("Content-ID");
		DEFAULT_HEADERS_TO_SIGN.add("Content-Type");
		DEFAULT_HEADERS_TO_SIGN.add("Content-Transfer-Encoding");
		DEFAULT_HEADERS_TO_SIGN.add("Cc");
		DEFAULT_HEADERS_TO_SIGN.add("Date");
		DEFAULT_HEADERS_TO_SIGN.add("In-Reply-To");
		DEFAULT_HEADERS_TO_SIGN.add("List-Subscribe");
		DEFAULT_HEADERS_TO_SIGN.add("List-Post");
		DEFAULT_HEADERS_TO_SIGN.add("List-Owner");
		DEFAULT_HEADERS_TO_SIGN.add("List-Id");
		DEFAULT_HEADERS_TO_SIGN.add("List-Archive");
		DEFAULT_HEADERS_TO_SIGN.add("List-Help");
		DEFAULT_HEADERS_TO_SIGN.add("List-Unsubscribe");
		DEFAULT_HEADERS_TO_SIGN.add("MIME-Version");
		DEFAULT_HEADERS_TO_SIGN.add("Message-ID");
		DEFAULT_HEADERS_TO_SIGN.add("Resent-Sender");
		DEFAULT_HEADERS_TO_SIGN.add("Resent-Cc");
		DEFAULT_HEADERS_TO_SIGN.add("Resent-Date");
		DEFAULT_HEADERS_TO_SIGN.add("Resent-To");
		DEFAULT_HEADERS_TO_SIGN.add("Reply-To");
		DEFAULT_HEADERS_TO_SIGN.add("References");
		DEFAULT_HEADERS_TO_SIGN.add("Resent-Message-ID");
		DEFAULT_HEADERS_TO_SIGN.add("Resent-From");
		DEFAULT_HEADERS_TO_SIGN.add("Sender");
	}

	private final Set headersToSign = new HashSet(DEFAULT_HEADERS_TO_SIGN);

	private SigningAlgorithm signingAlgorithm = SigningAlgorithm.SHA256_WITH_RSA;
	private Signature signature;
	private MessageDigest messageDigest;
	private String signingDomain;
	private String selector;
	private String identity;
	private boolean lengthParam;
	private boolean zParam;
	private Canonicalization headerCanonicalization = Canonicalization.RELAXED;
	private Canonicalization bodyCanonicalization = Canonicalization.SIMPLE;
	private boolean checkDomainKey = true;
	private RSAPrivateKey privateKey;

	/**
	 * Created a new {@code DkimSigner} for the given signing domain and
	 * selector with the given {@link RSAPrivateKey}.
	 * 
	 * @param signingDomain
	 *            The signing domain to be used.
	 * @param selector
	 *            The selector to be used.
	 * @param privateKey
	 *            The {@link RSAPrivateKey} to be used to sign
	 *            {@link DkimMessage DkimMessage}s.
	 * @throws DkimException
	 *             If the given signing domain is invalid.
	 */
	public DkimSigner(String signingDomain, String selector, RSAPrivateKey privateKey) throws DkimException {
		initDkimSigner(signingDomain, selector, privateKey);
	}

	/**
	 * Created a new {@code DkimSigner} for the given signing domain and
	 * selector with the given DER encoded RSA private Key.
	 * 
	 * @param signingDomain
	 *            The signing domain to be used.
	 * @param selector
	 *            The selector to be used.
	 * @param derFile
	 *            A {@link File} that contains the DER encoded RSA private key
	 *            to be used.
	 * 
	 * @throws IOException
	 *             If reading the content of the given {@link File} failed.
	 * @throws NoSuchAlgorithmException
	 *             If the RSA algorithm is not supported by the current JVM.
	 * @throws InvalidKeySpecException
	 *             If the content of the given {@link File} couldn't be
	 *             interpreted as an RSA private key.
	 * @throws DkimException
	 *             If the given signing domain is invalid.
	 */
	public DkimSigner(String signingDomain, String selector, File derFile) throws IOException,
			NoSuchAlgorithmException, InvalidKeySpecException, DkimException {
		this(signingDomain, selector, new FileInputStream(derFile));
	}

	/**
	 * Created a new {@code DkimSigner} for the given signing domain and
	 * selector with the given DER encoded RSA private Key.
	 * 
	 * @param signingDomain
	 *            The signing domain to be used.
	 * @param selector
	 *            The selector to be used.
	 * @param derStream
	 *            A {@link InputStream} that yields the DER encoded RSA private
	 *            key to be used.
	 * 
	 * @throws IOException
	 *             If reading the content of the given {@link InputStream}
	 *             failed.
	 * @throws NoSuchAlgorithmException
	 *             If the RSA algorithm is not supported by the current JVM.
	 * @throws InvalidKeySpecException
	 *             If the content of the given {@link InputStream} couldn't be
	 *             interpreted as an RSA private key.
	 * @throws DkimException
	 *             If the given signing domain is invalid.
	 */
	public DkimSigner(String signingDomain, String selector, InputStream derStream) throws IOException,
			NoSuchAlgorithmException, InvalidKeySpecException {
		byte[] privKeyBytes = Fetcher.fetch(derStream);
		KeyFactory keyFactory = KeyFactory.getInstance("RSA");
		PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(privKeyBytes);
		RSAPrivateKey privKey = (RSAPrivateKey) keyFactory.generatePrivate(privSpec);
		initDkimSigner(signingDomain, selector, privKey);
	}

	private void initDkimSigner(String signingDomain, String selector, RSAPrivateKey privkey) throws DkimException {

		if (!isValidDomain(signingDomain)) {
			throw new DkimException(signingDomain + " is an invalid signing domain");
		}

		this.signingDomain = signingDomain;
		this.selector = selector.trim();
		this.privateKey = privkey;
		this.setSigningAlgorithm(this.signingAlgorithm);
	}

	/**
	 * Returns the configured identity parameter.
	 * 
	 * @return The configured identity parameter.
	 */
	public String getIdentity() {
		return identity;
	}

	/**
	 * Sets the identity parameter to be used.
	 * 
	 * @param identity
	 *            The identity to be used.
	 * @throws DkimException
	 *             If the given identity parameter doesn't belong to the signing
	 *             domain of this {@code DkimSigner} or an subdomain thereof.
	 */
	public void setIdentity(String identity) throws DkimException {
		if (null != identity) {
			identity = identity.trim();
			if (!(identity.endsWith("@" + signingDomain) || identity.endsWith("." + signingDomain))) {
				throw new DkimException("The domain part of " + identity + " has to be " + signingDomain
						+ " or a subdomain thereof");
			}
		}
		this.identity = identity;
	}

	/**
	 * Returns the configured {@link Canonicalization} to be used for the body.
	 * 
	 * @return The configured {@link Canonicalization} to be used for the body.
	 */
	public Canonicalization getBodyCanonicalization() {
		return bodyCanonicalization;
	}

	/**
	 * Sets the {@link Canonicalization} to be used for the body.
	 * 
	 * @param canonicalization
	 *            The {@link Canonicalization} to be used for the body.
	 */
	public void setBodyCanonicalization(Canonicalization canonicalization) {
		this.bodyCanonicalization = canonicalization;
	}

	/**
	 * Returns the configured {@link Canonicalization} to be used for the
	 * headers.
	 * 
	 * @return The configured {@link Canonicalization} to be used for the
	 *         headers.
	 */
	public Canonicalization getHeaderCanonicalization() {
		return headerCanonicalization;
	}

	/**
	 * Sets the {@link Canonicalization} to be used for the headers.
	 * 
	 * @param canonicalization
	 *            The {@link Canonicalization} to be used for the headers.
	 */
	public void setHeaderCanonicalization(Canonicalization canonicalization) {
		this.headerCanonicalization = canonicalization;
	}

	/**
	 * Adds a header to the set of headers that will be included in the
	 * signature, if present.
	 * 
	 * @param header
	 *            The name of the header.
	 */
	public void addHeaderToSign(String header) {
		if (null != header && 0 != header.length()) {
			headersToSign.add(header);
		}
	}

	/**
	 * Removes a header from the set of headers that will be included in the
	 * signature, unless it is one of the required headers ('From', 'To',
	 * 'Subject').
	 * 
	 * @param header
	 *            The name of the header.
	 */
	public void removeHeaderToSign(String header) {
		if (null != header && 0 != header.length() && !MIMIMUM_HEADERS_TO_SIGN.contains(header)) {
			headersToSign.remove(header);
		}
	}

	/**
	 * Returns the configured length parameter.
	 * 
	 * @return The configured length parameter.
	 */
	public boolean getLengthParam() {
		return lengthParam;
	}

	/**
	 * Sets the length parameter to be used.
	 * 
	 * @param lengthParam
	 *            The length parameter to be used.
	 */
	public void setLengthParam(boolean lengthParam) {
		this.lengthParam = lengthParam;
	}

	/**
	 * Returns the configured z parameter.
	 * 
	 * @return The configured z parameter.
	 */
	public boolean isZParam() {
		return zParam;
	}

	/**
	 * Sets the z parameter to be used.
	 * 
	 * @param zParam
	 *            The z parameter to be used.
	 */
	public void setZParam(boolean zParam) {
		this.zParam = zParam;
	}

	/**
	 * Returns the configured {@link SigningAlgorithm}.
	 * 
	 * @return The configured {@link SigningAlgorithm}.
	 */
	public SigningAlgorithm getSigningAlgorithm() {
		return signingAlgorithm;
	}

	/**
	 * Sets the {@link SigningAlgorithm} to be used.
	 * 
	 * @param signingAlgorithm
	 *            The {@link SigningAlgorithm} to be used.
	 * 
	 * @throws DkimException
	 *             If either the signing algorithm or the hashing algorithm is
	 *             not supported by the current JVM or the {@link Signature}
	 *             couldn't be initialized.
	 */
	public void setSigningAlgorithm(SigningAlgorithm signingAlgorithm) throws DkimException {

		try {
			messageDigest = MessageDigest.getInstance(signingAlgorithm.getHashNotation());
		} catch (NoSuchAlgorithmException e) {
			throw new DkimException("The hashing algorithm " + signingAlgorithm.getHashNotation()
					+ " is not known by the JVM", e);
		}

		try {
			signature = Signature.getInstance(signingAlgorithm.getJavaNotation());
		} catch (NoSuchAlgorithmException e) {
			throw new DkimException("The signing algorithm " + signingAlgorithm.getJavaNotation()
					+ " is not known by the JVM", e);
		}

		try {
			signature.initSign(privateKey);
		} catch (InvalidKeyException e) {
			throw new DkimException("The provided private key is invalid", e);
		}

		this.signingAlgorithm = signingAlgorithm;
	}

	/**
	 * Returns whether the domain key should be retrieved and checked.
	 * 
	 * @return Whether the domain key should be retrieved and checked.
	 * @see DomainKey#check(String, RSAPrivateKey)
	 */
	public boolean isCheckDomainKey() {
		return checkDomainKey;
	}

	/**
	 * Sets, whether the domain key should be retrieved and checked.
	 * 
	 * @param checkDomainKey
	 *            Whether the domain key should be retrieved and checked.
	 */
	public void setCheckDomainKey(boolean checkDomainKey) {
		this.checkDomainKey = checkDomainKey;
	}

	protected String sign(DkimMessage message) throws DkimAcceptanceException, DkimSigningException {

		if (checkDomainKey) {
			try {
				DomainKeyUtil.getDomainKey(signingDomain, selector).check(identity, privateKey);
			} catch (DkimException e) {
				throw new DkimSigningException("Obtaining the domain key for " + signingDomain + "." + selector
						+ " failed", e);
			}
		}

		Map dkimSignature = new LinkedHashMap();
		dkimSignature.put("v", "1");
		dkimSignature.put("a", this.signingAlgorithm.getRfc4871Notation());
		dkimSignature.put("q", "dns/txt");
		dkimSignature.put("c", getHeaderCanonicalization().getType() + "/" + getBodyCanonicalization().getType());
		dkimSignature.put("t", ((long) new Date().getTime() / 1000) + "");
		dkimSignature.put("s", this.selector);
		dkimSignature.put("d", this.signingDomain);

		// set identity inside signature
		if (identity != null) {
			dkimSignature.put("i", quotedPrintable(identity));
		}

		// process header
		List assureHeaders = new ArrayList(MIMIMUM_HEADERS_TO_SIGN);

		// intersect defaultHeadersToSign with available headers
		StringBuffer headerList = new StringBuffer();
		StringBuffer headerContent = new StringBuffer();
		StringBuffer zParamString = new StringBuffer();

		try {
			@SuppressWarnings("unchecked")
			Enumeration
headerLines = message.getAllHeaders(); while (headerLines.hasMoreElements()) { Header header = (Header) headerLines.nextElement(); String headerName = header.getName(); if (headersToSign.contains(headerName)) { String headerValue = header.getValue(); headerList.append(headerName).append(":"); headerContent.append(headerCanonicalization.canonicalizeHeader(headerName, headerValue)); headerContent.append("\r\n"); assureHeaders.remove(headerName); if (zParam) { zParamString.append(headerName); zParamString.append(":"); zParamString.append(quotedPrintable(headerValue.trim()).replace("|", "=7C")); zParamString.append("|"); } } } if (!assureHeaders.isEmpty()) { throw new DkimSigningException("Could not find the header fields " + concatList(assureHeaders, ", ") + " for signing"); } } catch (MessagingException e) { throw new DkimSigningException("Could not find the header fields " + concatList(assureHeaders, ", ") + " for signing", e); } dkimSignature.put("h", headerList.substring(0, headerList.length() - 1)); if (zParam) { String zParamTemp = zParamString.toString(); dkimSignature.put("z", zParamTemp.substring(0, zParamTemp.length() - 1)); } // process body String body = message.getEncodedBody(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); CRLFOutputStream crlfos = new CRLFOutputStream(baos); try { crlfos.write(body.getBytes()); crlfos.close(); } catch (IOException e) { throw new DkimSigningException("The body conversion to MIME canonical CRLF line terminator failed", e); } body = baos.toString(); body = bodyCanonicalization.canonicalizeBody(body); if (lengthParam) { dkimSignature.put("l", Integer.toString(body.length())); } // calculate and encode body hash dkimSignature.put("bh", base64Encode(messageDigest.digest(body.getBytes()))); // create signature String serializedSignature = serializeDkimSignature(dkimSignature); byte[] signedSignature; try { headerContent.append(headerCanonicalization.canonicalizeHeader(DKIM_SIGNATUR_HEADER, serializedSignature)); signature.update(headerContent.toString().getBytes()); signedSignature = signature.sign(); } catch (SignatureException se) { throw new DkimSigningException("The signing operation by Java security failed", se); } return DKIM_SIGNATUR_HEADER + ": " + serializedSignature + foldSignedSignature(base64Encode(signedSignature), 3); } private String serializeDkimSignature(Map dkimSignature) { Set> entries = dkimSignature.entrySet(); StringBuffer buf = new StringBuffer(), fbuf; int pos = 0; Iterator> iter = entries.iterator(); while (iter.hasNext()) { Entry entry = iter.next(); // buf.append(entry.getKey()).append("=").append(entry.getValue()).append(";\t"); fbuf = new StringBuffer(); fbuf.append(entry.getKey()).append("=").append(entry.getValue()).append(";"); if (pos + fbuf.length() + 1 > MAX_HEADER_LENGTH) { pos = fbuf.length(); // line folding : this doesn't work "sometimes" --> maybe // someone likes to debug this /* * int i = 0; while (iMAXHEADERLENGTH) { * buf.append("\r\n\t").append(fbuf.substring(i, * i+MAXHEADERLENGTH)); i += MAXHEADERLENGTH; } else { * buf.append("\r\n\t").append(fbuf.substring(i)); pos -= i; * break; } } */ buf.append("\r\n\t").append(fbuf); } else { buf.append(" ").append(fbuf); pos += fbuf.length() + 1; } } buf.append("\r\n\tb="); return buf.toString().trim(); } private String foldSignedSignature(String s, int offset) { int i = 0; StringBuffer buf = new StringBuffer(); while (true) { if (offset > 0 && s.substring(i).length() > MAX_HEADER_LENGTH - offset) { buf.append(s.substring(i, i + MAX_HEADER_LENGTH - offset)); i += MAX_HEADER_LENGTH - offset; offset = 0; } else if (s.substring(i).length() > MAX_HEADER_LENGTH) { buf.append("\r\n\t").append(s.substring(i, i + MAX_HEADER_LENGTH)); i += MAX_HEADER_LENGTH; } else { buf.append("\r\n\t").append(s.substring(i)); break; } } return buf.toString(); } private static String concatList(List assureHeaders, String separator) { StringBuffer buffer = new StringBuffer(); for (String string : assureHeaders) { buffer.append(string); buffer.append(separator); } return buffer.substring(0, buffer.length() - separator.length()); } private static boolean isValidDomain(String domainname) { Pattern pattern = Pattern.compile("(.+)\\.(.+)"); Matcher matcher = pattern.matcher(domainname); return matcher.matches(); } // FSTODO: converts to "platforms default encoding" might be wrong ? private static String quotedPrintable(String s) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); QPEncoderStream encodeStream = new QPEncoderStream(out); encodeStream.write(s.getBytes()); encodeStream.close(); String encoded = out.toString(); encoded = encoded.replaceAll(";", "=3B"); encoded = encoded.replaceAll(" ", "=20"); return encoded; } catch (IOException e) { return null; } } private static String base64Encode(byte[] bytes) { String encoded = Base64.encodeBytes(bytes); // remove unnecessary line feeds after 76 characters encoded = encoded.replace("\n", ""); encoded = encoded.replace("\r", ""); return encoded; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy