View Javadoc

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.tagvalue;
21  
22  import java.util.Arrays;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.regex.Pattern;
26  
27  import org.apache.commons.codec.binary.Base64;
28  import org.apache.james.jdkim.api.SignatureRecord;
29  
30  public class SignatureRecordImpl extends TagValue implements SignatureRecord {
31  
32      // TODO ftext is defined as a sequence of at least one in %d33-57 or
33      // %d59-126
34      private static Pattern hdrNamePattern = Pattern.compile("^[^: \r\n\t]+$");
35  
36      public SignatureRecordImpl(String data) {
37          super(data);
38      }
39  
40      protected void init() {
41          mandatoryTags.add("v");
42          mandatoryTags.add("a");
43          mandatoryTags.add("b");
44          mandatoryTags.add("bh");
45          mandatoryTags.add("d");
46          mandatoryTags.add("h");
47          mandatoryTags.add("s");
48  
49          defaults.put("c", SIMPLE+"/"+SIMPLE);
50          defaults.put("l", ALL);
51          defaults.put("q", "dns/txt");
52      }
53  
54      /**
55       * @see org.apache.james.jdkim.api.SignatureRecord#validate()
56       */
57      public void validate() throws IllegalStateException {
58          super.validate();
59          // TODO: what about v=0.5 and no v= at all?
60          // do specs allow parsing? what should we check?
61          if (!"1".equals(getValue("v")))
62              throw new IllegalStateException(
63                      "Invalid DKIM-Signature version (expected '1'): "
64                              + getValue("v"));
65          if (getValue("h").length() == 0)
66              throw new IllegalStateException("Tag h= cannot be empty.");
67          if (!getIdentity().toString().toLowerCase().endsWith(
68                  ("@" + getValue("d")).toLowerCase())
69                  && !getIdentity().toString().toLowerCase().endsWith(
70                          ("." + getValue("d")).toLowerCase()))
71              throw new IllegalStateException("Domain mismatch");
72  
73          // when "x=" exists and signature expired then return PERMFAIL
74          // (signature expired)
75          if (getValue("x") != null) {
76              long expiration = Long.parseLong(getValue("x").toString());
77              long lifetime = (expiration - System.currentTimeMillis() / 1000);
78              String measure = "s";
79              if (lifetime < 0) {
80                  lifetime = -lifetime;
81                  if (lifetime > 600) {
82                      lifetime = lifetime / 60;
83                      measure = "m";
84                      if (lifetime > 600) {
85                          lifetime = lifetime / 60;
86                          measure = "h";
87                          if (lifetime > 120) {
88                              lifetime = lifetime / 24;
89                              measure = "d";
90                              if (lifetime > 90) {
91                                  lifetime = lifetime / 30;
92                                  measure = " months";
93                                  if (lifetime > 24) {
94                                      lifetime = lifetime / 12;
95                                      measure = " years";
96                                  }
97                              }
98                          }
99                      }
100                 }
101                 throw new IllegalStateException("Signature is expired since "
102                         + lifetime + measure + ".");
103             }
104         }
105 
106         // when "h=" does not contain "from" return PERMFAIL (From field not
107         // signed).
108         if (!isInListCaseInsensitive("from", getHeaders()))
109             throw new IllegalStateException("From field not signed");
110         // TODO support ignoring signature for certain d values (externally to
111         // this class).
112     }
113 
114     /**
115      * @see org.apache.james.jdkim.api.SignatureRecord#getHeaders()
116      */
117     public List/* CharSequence */getHeaders() {
118         return stringToColonSeparatedList(getValue("h").toString(),
119                 hdrNamePattern);
120     }
121 
122     // If i= is unspecified the default is @d
123     protected CharSequence getDefault(String tag) {
124         if ("i".equals(tag)) {
125             return "@" + getValue("d");
126         } else
127             return super.getDefault(tag);
128     }
129 
130     /**
131      * @see org.apache.james.jdkim.api.SignatureRecord#getIdentityLocalPart()
132      */
133     public CharSequence getIdentityLocalPart() {
134         String identity = getIdentity().toString();
135         int pAt = identity.indexOf('@');
136         return identity.subSequence(0, pAt);
137     }
138 
139     public CharSequence getIdentity() {
140         return dkimQuotedPrintableDecode(getValue("i"));
141     }
142 
143     public static String dkimQuotedPrintableDecode(CharSequence input)
144             throws IllegalArgumentException {
145         StringBuffer sb = new StringBuffer(input.length());
146         // TODO should we fail on WSP that is not part of FWS?
147         // the specification in 2.6 DKIM-Quoted-Printable is not
148         // clear
149         int state = 0;
150         int start = 0;
151         int d = 0;
152         boolean lastWasNL = false;
153         for (int i = 0; i < input.length(); i++) {
154             if (lastWasNL && input.charAt(i) != ' ' && input.charAt(i) != '\t') {
155                 throw new IllegalArgumentException(
156                         "Unexpected LF not part of an FWS");
157             }
158             lastWasNL = false;
159             switch (state) {
160             case 0:
161                 switch (input.charAt(i)) {
162                 case ' ':
163                 case '\t':
164                 case '\r':
165                 case '\n':
166                     if ('\n' == input.charAt(i))
167                         lastWasNL = true;
168                     sb.append(input.subSequence(start, i));
169                     start = i + 1;
170                     // ignoring whitespace by now.
171                     break;
172                 case '=':
173                     sb.append(input.subSequence(start, i));
174                     state = 1;
175                     break;
176                 }
177                 break;
178             case 1:
179             case 2:
180                 if (input.charAt(i) >= '0' && input.charAt(i) <= '9'
181                         || input.charAt(i) >= 'A' && input.charAt(i) <= 'F') {
182                     int v = Arrays.binarySearch("0123456789ABCDEF".getBytes(),
183                             (byte) input.charAt(i));
184                     if (state == 1) {
185                         state = 2;
186                         d = v;
187                     } else {
188                         d = d * 16 + v;
189                         sb.append((char) d);
190                         state = 0;
191                         start = i + 1;
192                     }
193                 } else {
194                     throw new IllegalArgumentException(
195                             "Invalid input sequence at " + i);
196                 }
197             }
198         }
199         if (state != 0) {
200             throw new IllegalArgumentException(
201                     "Invalid quoted printable termination");
202         }
203         sb.append(input.subSequence(start, input.length()));
204         return sb.toString();
205     }
206 
207     /**
208      * @see org.apache.james.jdkim.api.SignatureRecord#getHashKeyType()
209      */
210     public CharSequence getHashKeyType() {
211         String a = getValue("a").toString();
212         int pHyphen = a.indexOf('-');
213         // TODO x-sig-a-tag-h = ALPHA *(ALPHA / DIGIT)
214         if (pHyphen == -1)
215             throw new IllegalStateException(
216                     "Invalid hash algorythm (key type): " + a);
217         return a.subSequence(0, pHyphen);
218     }
219 
220     /**
221      * @see org.apache.james.jdkim.api.SignatureRecord#getHashMethod()
222      */
223     public CharSequence getHashMethod() {
224         String a = getValue("a").toString();
225         int pHyphen = a.indexOf('-');
226         // TODO x-sig-a-tag-h = ALPHA *(ALPHA / DIGIT)
227         if (pHyphen == -1)
228             throw new IllegalStateException("Invalid hash method: " + a);
229         return a.subSequence(pHyphen + 1, a.length());
230     }
231 
232     /**
233      * @see org.apache.james.jdkim.api.SignatureRecord#getHashAlgo()
234      */
235     public CharSequence getHashAlgo() {
236         String a = getValue("a").toString();
237         int pHyphen = a.indexOf('-');
238         if (pHyphen == -1)
239             throw new IllegalStateException("Invalid hash method: " + a);
240         if (a.length() > pHyphen + 3 && a.charAt(pHyphen + 1) == 's'
241                 && a.charAt(pHyphen + 2) == 'h' && a.charAt(pHyphen + 3) == 'a') {
242             return "sha-" + a.subSequence(pHyphen + 4, a.length());
243         } else
244             return a.subSequence(pHyphen + 1, a.length());
245     }
246 
247     /**
248      * @see org.apache.james.jdkim.api.SignatureRecord#getSelector()
249      */
250     public CharSequence getSelector() {
251         return getValue("s");
252     }
253 
254     /**
255      * @see org.apache.james.jdkim.api.SignatureRecord#getDToken()
256      */
257     public CharSequence getDToken() {
258         return getValue("d");
259     }
260 
261     public byte[] getBodyHash() {
262         return Base64.decodeBase64(getValue("bh").toString().getBytes());
263     }
264 
265     public byte[] getSignature() {
266         return Base64.decodeBase64(getValue("b").toString().getBytes());
267     }
268 
269     public int getBodyHashLimit() {
270         String limit = getValue("l").toString();
271         if (ALL.equals(limit))
272             return -1;
273         else
274             return Integer.parseInt(limit);
275     }
276 
277     public String getBodyCanonicalisationMethod() {
278         String c = getValue("c").toString();
279         int pSlash = c.toString().indexOf("/");
280         if (pSlash != -1) {
281             return c.substring(pSlash + 1);
282         } else {
283             return SIMPLE;
284         }
285     }
286 
287     public String getHeaderCanonicalisationMethod() {
288         String c = getValue("c").toString();
289         int pSlash = c.toString().indexOf("/");
290         if (pSlash != -1) {
291             return c.substring(0, pSlash);
292         } else {
293             return c;
294         }
295     }
296 
297     public List getRecordLookupMethods() {
298         String flags = getValue("q").toString();
299         String[] flagsStrings = flags.split(":");
300         List res = new LinkedList();
301         for (int i = 0; i < flagsStrings.length; i++) {
302             // TODO add validation method[/option]
303             // if (VALIDATION)
304             res.add(trimFWS(flagsStrings[i], 0, flagsStrings[i].length() - 1,
305                     true).toString());
306         }
307         return res;
308     }
309 
310     public void setSignature(byte[] newSignature) {
311         String signature = new String(Base64.encodeBase64(newSignature));
312         setValue("b", signature);
313     }
314 
315     public void setBodyHash(byte[] newBodyHash) {
316         String bodyHash = new String(Base64.encodeBase64(newBodyHash));
317         setValue("bh", bodyHash);
318     }
319 
320     public String toUnsignedString() {
321         return toString().replaceFirst("b=[^;]*", "b=");
322     }
323 
324 
325 }