1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
33
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
56
57 public void validate() throws IllegalStateException {
58 super.validate();
59
60
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
74
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
107
108 if (!isInListCaseInsensitive("from", getHeaders()))
109 throw new IllegalStateException("From field not signed");
110
111
112 }
113
114
115
116
117 public List
118 return stringToColonSeparatedList(getValue("h").toString(),
119 hdrNamePattern);
120 }
121
122
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
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
147
148
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
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
209
210 public CharSequence getHashKeyType() {
211 String a = getValue("a").toString();
212 int pHyphen = a.indexOf('-');
213
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
222
223 public CharSequence getHashMethod() {
224 String a = getValue("a").toString();
225 int pHyphen = a.indexOf('-');
226
227 if (pHyphen == -1)
228 throw new IllegalStateException("Invalid hash method: " + a);
229 return a.subSequence(pHyphen + 1, a.length());
230 }
231
232
233
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
249
250 public CharSequence getSelector() {
251 return getValue("s");
252 }
253
254
255
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
303
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 }