1 /*
2  *  Licensed to the Apache Software Foundation (ASF) under one or more
3  *  contributor license agreements.  See the NOTICE file distributed with
4  *  this work for additional information regarding copyright ownership.
5  *  The ASF licenses this file to You under the Apache License, Version 2.0
6  *  (the "License"); you may not use this file except in compliance with
7  *  the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */
17 
18 package org.apache.http.conn.ssl;
19 
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.List;
23 import javax.security.auth.x500.X500Principal;
24 
25 /**
26  * A distinguished name (DN) parser. This parser only supports extracting a
27  * string value from a DN. It doesn't support values in the hex-string style.
28  *
29  * @hide
30  */
31 @Deprecated
32 final class AndroidDistinguishedNameParser {
33     private final String dn;
34     private final int length;
35     private int pos;
36     private int beg;
37     private int end;
38 
39     /** tmp vars to store positions of the currently parsed item */
40     private int cur;
41 
42     /** distinguished name chars */
43     private char[] chars;
44 
AndroidDistinguishedNameParser(X500Principal principal)45     public AndroidDistinguishedNameParser(X500Principal principal) {
46         // RFC2253 is used to ensure we get attributes in the reverse
47         // order of the underlying ASN.1 encoding, so that the most
48         // significant values of repeated attributes occur first.
49         this.dn = principal.getName(X500Principal.RFC2253);
50         this.length = this.dn.length();
51     }
52 
53     // gets next attribute type: (ALPHA 1*keychar) / oid
nextAT()54     private String nextAT() {
55         // skip preceding space chars, they can present after
56         // comma or semicolon (compatibility with RFC 1779)
57         for (; pos < length && chars[pos] == ' '; pos++) {
58         }
59         if (pos == length) {
60             return null; // reached the end of DN
61         }
62 
63         // mark the beginning of attribute type
64         beg = pos;
65 
66         // attribute type chars
67         pos++;
68         for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
69             // we don't follow exact BNF syntax here:
70             // accept any char except space and '='
71         }
72         if (pos >= length) {
73             throw new IllegalStateException("Unexpected end of DN: " + dn);
74         }
75 
76         // mark the end of attribute type
77         end = pos;
78 
79         // skip trailing space chars between attribute type and '='
80         // (compatibility with RFC 1779)
81         if (chars[pos] == ' ') {
82             for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
83             }
84 
85             if (chars[pos] != '=' || pos == length) {
86                 throw new IllegalStateException("Unexpected end of DN: " + dn);
87             }
88         }
89 
90         pos++; //skip '=' char
91 
92         // skip space chars between '=' and attribute value
93         // (compatibility with RFC 1779)
94         for (; pos < length && chars[pos] == ' '; pos++) {
95         }
96 
97         // in case of oid attribute type skip its prefix: "oid." or "OID."
98         // (compatibility with RFC 1779)
99         if ((end - beg > 4) && (chars[beg + 3] == '.')
100                 && (chars[beg] == 'O' || chars[beg] == 'o')
101                 && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
102                 && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
103             beg += 4;
104         }
105 
106         return new String(chars, beg, end - beg);
107     }
108 
109     // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
quotedAV()110     private String quotedAV() {
111         pos++;
112         beg = pos;
113         end = beg;
114         while (true) {
115 
116             if (pos == length) {
117                 throw new IllegalStateException("Unexpected end of DN: " + dn);
118             }
119 
120             if (chars[pos] == '"') {
121                 // enclosing quotation was found
122                 pos++;
123                 break;
124             } else if (chars[pos] == '\\') {
125                 chars[end] = getEscaped();
126             } else {
127                 // shift char: required for string with escaped chars
128                 chars[end] = chars[pos];
129             }
130             pos++;
131             end++;
132         }
133 
134         // skip trailing space chars before comma or semicolon.
135         // (compatibility with RFC 1779)
136         for (; pos < length && chars[pos] == ' '; pos++) {
137         }
138 
139         return new String(chars, beg, end - beg);
140     }
141 
142     // gets hex string attribute value: "#" hexstring
hexAV()143     private String hexAV() {
144         if (pos + 4 >= length) {
145             // encoded byte array  must be not less then 4 c
146             throw new IllegalStateException("Unexpected end of DN: " + dn);
147         }
148 
149         beg = pos; // store '#' position
150         pos++;
151         while (true) {
152 
153             // check for end of attribute value
154             // looks for space and component separators
155             if (pos == length || chars[pos] == '+' || chars[pos] == ','
156                     || chars[pos] == ';') {
157                 end = pos;
158                 break;
159             }
160 
161             if (chars[pos] == ' ') {
162                 end = pos;
163                 pos++;
164                 // skip trailing space chars before comma or semicolon.
165                 // (compatibility with RFC 1779)
166                 for (; pos < length && chars[pos] == ' '; pos++) {
167                 }
168                 break;
169             } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
170                 chars[pos] += 32; //to low case
171             }
172 
173             pos++;
174         }
175 
176         // verify length of hex string
177         // encoded byte array  must be not less then 4 and must be even number
178         int hexLen = end - beg; // skip first '#' char
179         if (hexLen < 5 || (hexLen & 1) == 0) {
180             throw new IllegalStateException("Unexpected end of DN: " + dn);
181         }
182 
183         // get byte encoding from string representation
184         byte[] encoded = new byte[hexLen / 2];
185         for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
186             encoded[i] = (byte) getByte(p);
187         }
188 
189         return new String(chars, beg, hexLen);
190     }
191 
192     // gets string attribute value: *( stringchar / pair )
escapedAV()193     private String escapedAV() {
194         beg = pos;
195         end = pos;
196         while (true) {
197             if (pos >= length) {
198                 // the end of DN has been found
199                 return new String(chars, beg, end - beg);
200             }
201 
202             switch (chars[pos]) {
203             case '+':
204             case ',':
205             case ';':
206                 // separator char has been found
207                 return new String(chars, beg, end - beg);
208             case '\\':
209                 // escaped char
210                 chars[end++] = getEscaped();
211                 pos++;
212                 break;
213             case ' ':
214                 // need to figure out whether space defines
215                 // the end of attribute value or not
216                 cur = end;
217 
218                 pos++;
219                 chars[end++] = ' ';
220 
221                 for (; pos < length && chars[pos] == ' '; pos++) {
222                     chars[end++] = ' ';
223                 }
224                 if (pos == length || chars[pos] == ',' || chars[pos] == '+'
225                         || chars[pos] == ';') {
226                     // separator char or the end of DN has been found
227                     return new String(chars, beg, cur - beg);
228                 }
229                 break;
230             default:
231                 chars[end++] = chars[pos];
232                 pos++;
233             }
234         }
235     }
236 
237     // returns escaped char
getEscaped()238     private char getEscaped() {
239         pos++;
240         if (pos == length) {
241             throw new IllegalStateException("Unexpected end of DN: " + dn);
242         }
243 
244         switch (chars[pos]) {
245         case '"':
246         case '\\':
247         case ',':
248         case '=':
249         case '+':
250         case '<':
251         case '>':
252         case '#':
253         case ';':
254         case ' ':
255         case '*':
256         case '%':
257         case '_':
258             //FIXME: escaping is allowed only for leading or trailing space char
259             return chars[pos];
260         default:
261             // RFC doesn't explicitly say that escaped hex pair is
262             // interpreted as UTF-8 char. It only contains an example of such DN.
263             return getUTF8();
264         }
265     }
266 
267     // decodes UTF-8 char
268     // see http://www.unicode.org for UTF-8 bit distribution table
getUTF8()269     private char getUTF8() {
270         int res = getByte(pos);
271         pos++; //FIXME tmp
272 
273         if (res < 128) { // one byte: 0-7F
274             return (char) res;
275         } else if (res >= 192 && res <= 247) {
276 
277             int count;
278             if (res <= 223) { // two bytes: C0-DF
279                 count = 1;
280                 res = res & 0x1F;
281             } else if (res <= 239) { // three bytes: E0-EF
282                 count = 2;
283                 res = res & 0x0F;
284             } else { // four bytes: F0-F7
285                 count = 3;
286                 res = res & 0x07;
287             }
288 
289             int b;
290             for (int i = 0; i < count; i++) {
291                 pos++;
292                 if (pos == length || chars[pos] != '\\') {
293                     return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
294                 }
295                 pos++;
296 
297                 b = getByte(pos);
298                 pos++; //FIXME tmp
299                 if ((b & 0xC0) != 0x80) {
300                     return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
301                 }
302 
303                 res = (res << 6) + (b & 0x3F);
304             }
305             return (char) res;
306         } else {
307             return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
308         }
309     }
310 
311     // Returns byte representation of a char pair
312     // The char pair is composed of DN char in
313     // specified 'position' and the next char
314     // According to BNF syntax:
315     // hexchar    = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
316     //                    / "a" / "b" / "c" / "d" / "e" / "f"
getByte(int position)317     private int getByte(int position) {
318         if (position + 1 >= length) {
319             throw new IllegalStateException("Malformed DN: " + dn);
320         }
321 
322         int b1, b2;
323 
324         b1 = chars[position];
325         if (b1 >= '0' && b1 <= '9') {
326             b1 = b1 - '0';
327         } else if (b1 >= 'a' && b1 <= 'f') {
328             b1 = b1 - 87; // 87 = 'a' - 10
329         } else if (b1 >= 'A' && b1 <= 'F') {
330             b1 = b1 - 55; // 55 = 'A' - 10
331         } else {
332             throw new IllegalStateException("Malformed DN: " + dn);
333         }
334 
335         b2 = chars[position + 1];
336         if (b2 >= '0' && b2 <= '9') {
337             b2 = b2 - '0';
338         } else if (b2 >= 'a' && b2 <= 'f') {
339             b2 = b2 - 87; // 87 = 'a' - 10
340         } else if (b2 >= 'A' && b2 <= 'F') {
341             b2 = b2 - 55; // 55 = 'A' - 10
342         } else {
343             throw new IllegalStateException("Malformed DN: " + dn);
344         }
345 
346         return (b1 << 4) + b2;
347     }
348 
349     /**
350      * Parses the DN and returns the most significant attribute value
351      * for an attribute type, or null if none found.
352      *
353      * @param attributeType attribute type to look for (e.g. "ca")
354      */
findMostSpecific(String attributeType)355     public String findMostSpecific(String attributeType) {
356         // Initialize internal state.
357         pos = 0;
358         beg = 0;
359         end = 0;
360         cur = 0;
361         chars = dn.toCharArray();
362 
363         String attType = nextAT();
364         if (attType == null) {
365             return null;
366         }
367         while (true) {
368             String attValue = "";
369 
370             if (pos == length) {
371                 return null;
372             }
373 
374             switch (chars[pos]) {
375             case '"':
376                 attValue = quotedAV();
377                 break;
378             case '#':
379                 attValue = hexAV();
380                 break;
381             case '+':
382             case ',':
383             case ';': // compatibility with RFC 1779: semicolon can separate RDNs
384                 //empty attribute value
385                 break;
386             default:
387                 attValue = escapedAV();
388             }
389 
390             // Values are ordered from most specific to least specific
391             // due to the RFC2253 formatting. So take the first match
392             // we see.
393             if (attributeType.equalsIgnoreCase(attType)) {
394                 return attValue;
395             }
396 
397             if (pos >= length) {
398                 return null;
399             }
400 
401             if (chars[pos] == ',' || chars[pos] == ';') {
402             } else if (chars[pos] != '+') {
403                 throw new IllegalStateException("Malformed DN: " + dn);
404             }
405 
406             pos++;
407             attType = nextAT();
408             if (attType == null) {
409                 throw new IllegalStateException("Malformed DN: " + dn);
410             }
411         }
412     }
413 
414     /**
415      * Parses the DN and returns all values for an attribute type, in
416      * the order of decreasing significance (most significant first).
417      *
418      * @param attributeType attribute type to look for (e.g. "ca")
419      */
getAllMostSpecificFirst(String attributeType)420     public List<String> getAllMostSpecificFirst(String attributeType) {
421         // Initialize internal state.
422         pos = 0;
423         beg = 0;
424         end = 0;
425         cur = 0;
426         chars = dn.toCharArray();
427         List<String> result = Collections.emptyList();
428 
429         String attType = nextAT();
430         if (attType == null) {
431             return result;
432         }
433         while (pos < length) {
434             String attValue = "";
435 
436             switch (chars[pos]) {
437             case '"':
438                 attValue = quotedAV();
439                 break;
440             case '#':
441                 attValue = hexAV();
442                 break;
443             case '+':
444             case ',':
445             case ';': // compatibility with RFC 1779: semicolon can separate RDNs
446                 //empty attribute value
447                 break;
448             default:
449                 attValue = escapedAV();
450             }
451 
452             // Values are ordered from most specific to least specific
453             // due to the RFC2253 formatting. So take the first match
454             // we see.
455             if (attributeType.equalsIgnoreCase(attType)) {
456                 if (result.isEmpty()) {
457                     result = new ArrayList<String>();
458                 }
459                 result.add(attValue);
460             }
461 
462             if (pos >= length) {
463                 break;
464             }
465 
466             if (chars[pos] == ',' || chars[pos] == ';') {
467             } else if (chars[pos] != '+') {
468                 throw new IllegalStateException("Malformed DN: " + dn);
469             }
470 
471             pos++;
472             attType = nextAT();
473             if (attType == null) {
474                 throw new IllegalStateException("Malformed DN: " + dn);
475             }
476         }
477 
478         return result;
479     }
480 }
481