blob: 1cee2b969e180c60462c93cf908cb488657a5fe7 [file] [log] [blame]
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you 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. *
****************************************************************/
package org.apache.james.jdkim.tagvalue;
import org.apache.commons.codec.binary.Base64;
import org.apache.james.jdkim.api.SignatureRecord;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
public class SignatureRecordImpl extends TagValue implements SignatureRecord {
// TODO ftext is defined as a sequence of at least one in %d33-57 or
// %d59-126
private static final Pattern hdrNamePattern = Pattern.compile("^[^: \r\n\t]+$");
public SignatureRecordImpl(String data) {
super(data);
}
protected void init() {
mandatoryTags.add("v");
mandatoryTags.add("a");
mandatoryTags.add("b");
mandatoryTags.add("bh");
mandatoryTags.add("d");
mandatoryTags.add("h");
mandatoryTags.add("s");
defaults.put("c", SIMPLE + "/" + SIMPLE);
defaults.put("l", ALL);
defaults.put("q", "dns/txt");
}
/**
* @see org.apache.james.jdkim.api.SignatureRecord#validate()
*/
public void validate() throws IllegalStateException {
super.validate();
// TODO: what about v=0.5 and no v= at all?
// do specs allow parsing? what should we check?
if (!"1".equals(getValue("v")))
throw new IllegalStateException(
"Invalid DKIM-Signature version (expected '1'): "
+ getValue("v"));
if (getValue("h").length() == 0)
throw new IllegalStateException("Tag h= cannot be empty.");
CharSequence identity;
try {
identity = getIdentity();
} catch (IllegalArgumentException e) {
throw new IllegalStateException("Identity (i=) declaration cannot be parsed. Probably due to missing quoted printable encoding", e);
}
if (!identity.toString().toLowerCase().endsWith(
("@" + getValue("d")).toLowerCase())
&& !getIdentity().toString().toLowerCase().endsWith(
("." + getValue("d")).toLowerCase()))
throw new IllegalStateException("Identity (i=) domain mismatch: expected [optional]@[optional.]domain-from-d-attribute");
// when "x=" exists and signature expired then return PERMFAIL
// (signature expired)
if (getValue("x") != null) {
long expiration = Long.parseLong(getValue("x").toString());
long lifetime = (expiration - System.currentTimeMillis() / 1000);
if (lifetime < 0) {
throw new IllegalStateException("Signature is expired since "
+ getTimeMeasure(lifetime) + ".");
}
}
// when "h=" does not contain "from" return PERMFAIL (From field not
// signed).
if (!isInListCaseInsensitive("from", getHeaders()))
throw new IllegalStateException("From field not signed");
// TODO support ignoring signature for certain d values (externally to
// this class).
}
private String getTimeMeasure(long lifetime) {
String measure = "s";
lifetime = -lifetime;
if (lifetime > 600) {
lifetime = lifetime / 60;
measure = "m";
if (lifetime > 600) {
lifetime = lifetime / 60;
measure = "h";
if (lifetime > 120) {
lifetime = lifetime / 24;
measure = "d";
if (lifetime > 90) {
lifetime = lifetime / 30;
measure = " months";
if (lifetime > 24) {
lifetime = lifetime / 12;
measure = " years";
}
}
}
}
}
return lifetime + measure;
}
/**
* @see org.apache.james.jdkim.api.SignatureRecord#getHeaders()
*/
public List<CharSequence> getHeaders() {
return stringToColonSeparatedList(getValue("h").toString(),
hdrNamePattern);
}
// If i= is unspecified the default is @d
protected CharSequence getDefault(String tag) {
if ("i".equals(tag)) {
return "@" + getValue("d");
} else
return super.getDefault(tag);
}
/**
* @see org.apache.james.jdkim.api.SignatureRecord#getIdentityLocalPart()
*/
public CharSequence getIdentityLocalPart() {
String identity = getIdentity().toString();
int pAt = identity.indexOf('@');
return identity.subSequence(0, pAt);
}
/**
* This may throws IllegalArgumentException on invalid "i" content,
* but should always happen during validation!
*
* @see org.apache.james.jdkim.api.SignatureRecord#getIdentity()
*/
public CharSequence getIdentity() {
return dkimQuotedPrintableDecode(getValue("i"));
}
public static String dkimQuotedPrintableDecode(CharSequence input)
throws IllegalArgumentException {
StringBuilder sb = new StringBuilder(input.length());
// TODO should we fail on WSP that is not part of FWS?
// the specification in 2.6 DKIM-Quoted-Printable is not
// clear
int state = 0;
int start = 0;
int d = 0;
boolean lastWasNL = false;
for (int i = 0; i < input.length(); i++) {
if (lastWasNL && input.charAt(i) != ' ' && input.charAt(i) != '\t') {
throw new IllegalArgumentException(
"Unexpected LF not part of an FWS");
}
lastWasNL = false;
switch (state) {
case 0:
switch (input.charAt(i)) {
case ' ':
case '\t':
case '\r':
case '\n':
if ('\n' == input.charAt(i))
lastWasNL = true;
sb.append(input.subSequence(start, i));
start = i + 1;
// ignoring whitespace by now.
break;
case '=':
sb.append(input.subSequence(start, i));
state = 1;
break;
}
break;
case 1:
case 2:
if (input.charAt(i) >= '0' && input.charAt(i) <= '9'
|| input.charAt(i) >= 'A' && input.charAt(i) <= 'F') {
int v = Arrays.binarySearch("0123456789ABCDEF".getBytes(),
(byte) input.charAt(i));
if (state == 1) {
state = 2;
d = v;
} else {
d = d * 16 + v;
sb.append((char) d);
state = 0;
start = i + 1;
}
} else {
throw new IllegalArgumentException(
"Invalid input sequence at " + i);
}
}
}
if (state != 0) {
throw new IllegalArgumentException(
"Invalid quoted printable termination");
}
sb.append(input.subSequence(start, input.length()));
return sb.toString();
}
/**
* @see org.apache.james.jdkim.api.SignatureRecord#getHashKeyType()
*/
public CharSequence getHashKeyType() {
String a = getValue("a").toString();
int pHyphen = a.indexOf('-');
// TODO x-sig-a-tag-h = ALPHA *(ALPHA / DIGIT)
if (pHyphen == -1)
throw new IllegalStateException(
"Invalid hash algorythm (key type): " + a);
return a.subSequence(0, pHyphen);
}
/**
* @see org.apache.james.jdkim.api.SignatureRecord#getHashMethod()
*/
public CharSequence getHashMethod() {
String a = getValue("a").toString();
int pHyphen = a.indexOf('-');
// TODO x-sig-a-tag-h = ALPHA *(ALPHA / DIGIT)
if (pHyphen == -1)
throw new IllegalStateException("Invalid hash method: " + a);
return a.subSequence(pHyphen + 1, a.length());
}
/**
* @see org.apache.james.jdkim.api.SignatureRecord#getHashAlgo()
*/
public CharSequence getHashAlgo() {
String a = getValue("a").toString();
int pHyphen = a.indexOf('-');
if (pHyphen == -1)
throw new IllegalStateException("Invalid hash method: " + a);
if (a.length() > pHyphen + 3 && a.charAt(pHyphen + 1) == 's'
&& a.charAt(pHyphen + 2) == 'h' && a.charAt(pHyphen + 3) == 'a') {
return "sha-" + a.subSequence(pHyphen + 4, a.length());
} else
return a.subSequence(pHyphen + 1, a.length());
}
/**
* @see org.apache.james.jdkim.api.SignatureRecord#getSelector()
*/
public CharSequence getSelector() {
return getValue("s");
}
/**
* @see org.apache.james.jdkim.api.SignatureRecord#getDToken()
*/
public CharSequence getDToken() {
return getValue("d");
}
public byte[] getBodyHash() {
return Base64.decodeBase64(getValue("bh").toString().getBytes());
}
public byte[] getSignature() {
return Base64.decodeBase64(getValue("b").toString().getBytes());
}
public int getBodyHashLimit() {
String limit = getValue("l").toString();
if (ALL.equals(limit))
return -1;
else
return Integer.parseInt(limit);
}
public Long getSignatureTimestamp() {
CharSequence cs = getValue("t");
if (cs == null) return null;
return Long.parseLong(cs.toString());
}
public String getBodyCanonicalisationMethod() {
String c = getValue("c").toString();
int pSlash = c.indexOf("/");
if (pSlash != -1) {
return c.substring(pSlash + 1);
} else {
return SIMPLE;
}
}
public String getHeaderCanonicalisationMethod() {
String c = getValue("c").toString();
int pSlash = c.indexOf("/");
if (pSlash != -1) {
return c.substring(0, pSlash);
} else {
return c;
}
}
public List<CharSequence> getRecordLookupMethods() {
String flags = getValue("q").toString();
String[] flagsStrings = flags.split(":");
List<CharSequence> res = new LinkedList<CharSequence>();
for (String flagsString : flagsStrings) {
// TODO add validation method[/option]
// if (VALIDATION)
res.add(trimFWS(flagsString, 0, flagsString.length() - 1,
true));
}
return res;
}
public void setSignature(byte[] newSignature) {
String signature = new String(Base64.encodeBase64(newSignature));
setValue("b", signature);
}
public void setBodyHash(byte[] newBodyHash) {
String bodyHash = new String(Base64.encodeBase64(newBodyHash));
setValue("bh", bodyHash);
// If a t=; parameter is present in the signature, make sure to
// fill it with the current timestamp
if (getValue("t") != null && getValue("t").toString().trim().length() == 0) {
setValue("t", "" + (System.currentTimeMillis() / 1000));
}
}
public String toUnsignedString() {
return toString().replaceFirst("b=[^;]*", "b=");
}
}