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 | } |