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
18package com.android.internal.net;
19
20
21import android.util.Log;
22
23import java.io.IOException;
24
25import javax.security.auth.x500.X500Principal;
26
27/**
28 * A simple distinguished name(DN) parser.
29 *
30 * <p>This class is based on org.apache.harmony.security.x509.DNParser.  It's customized to remove
31 * external references which are unnecessary for our requirements.
32 *
33 * <p>This class is only meant for extracting a string value from a DN.  e.g. it doesn't support
34 * values in the hex-string style.
35 *
36 * <p>This class is used by {@link DomainNameValidator} only.  However, in order to make this
37 * class visible from unit tests, it's made public.
38 *
39 * @hide
40 */
41public final class DNParser {
42    private static final String TAG = "DNParser";
43
44    /** DN to be parsed. */
45    private final String dn;
46
47    // length of distinguished name string
48    private final int length;
49
50    private int pos, beg, end;
51
52    // tmp vars to store positions of the currently parsed item
53    private int cur;
54
55    // distinguished name chars
56    private char[] chars;
57
58    /**
59     * Exception message thrown when we failed to parse DN, which shouldn't happen because we
60     * only handle DNs that {@link X500Principal#getName} returns, which shouldn't be malformed.
61     */
62    private static final String ERROR_PARSE_ERROR = "Failed to parse DN";
63
64    /**
65     * Constructor.
66     *
67     * @param principal - {@link X500Principal} to be parsed
68     */
69    public DNParser(X500Principal principal) {
70        this.dn = principal.getName(X500Principal.RFC2253);
71        this.length = dn.length();
72    }
73
74    // gets next attribute type: (ALPHA 1*keychar) / oid
75    private String nextAT() throws IOException {
76
77        // skip preceding space chars, they can present after
78        // comma or semicolon (compatibility with RFC 1779)
79        for (; pos < length && chars[pos] == ' '; pos++) {
80        }
81        if (pos == length) {
82            return null; // reached the end of DN
83        }
84
85        // mark the beginning of attribute type
86        beg = pos;
87
88        // attribute type chars
89        pos++;
90        for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
91            // we don't follow exact BNF syntax here:
92            // accept any char except space and '='
93        }
94        if (pos >= length) {
95            // unexpected end of DN
96            throw new IOException(ERROR_PARSE_ERROR);
97        }
98
99        // mark the end of attribute type
100        end = pos;
101
102        // skip trailing space chars between attribute type and '='
103        // (compatibility with RFC 1779)
104        if (chars[pos] == ' ') {
105            for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
106            }
107
108            if (chars[pos] != '=' || pos == length) {
109                // unexpected end of DN
110                throw new IOException(ERROR_PARSE_ERROR);
111            }
112        }
113
114        pos++; //skip '=' char
115
116        // skip space chars between '=' and attribute value
117        // (compatibility with RFC 1779)
118        for (; pos < length && chars[pos] == ' '; pos++) {
119        }
120
121        // in case of oid attribute type skip its prefix: "oid." or "OID."
122        // (compatibility with RFC 1779)
123        if ((end - beg > 4) && (chars[beg + 3] == '.')
124                && (chars[beg] == 'O' || chars[beg] == 'o')
125                && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
126                && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
127            beg += 4;
128        }
129
130        return new String(chars, beg, end - beg);
131    }
132
133    // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
134    private String quotedAV() throws IOException {
135
136        pos++;
137        beg = pos;
138        end = beg;
139        while (true) {
140
141            if (pos == length) {
142                // unexpected end of DN
143                throw new IOException(ERROR_PARSE_ERROR);
144            }
145
146            if (chars[pos] == '"') {
147                // enclosing quotation was found
148                pos++;
149                break;
150            } else if (chars[pos] == '\\') {
151                chars[end] = getEscaped();
152            } else {
153                // shift char: required for string with escaped chars
154                chars[end] = chars[pos];
155            }
156            pos++;
157            end++;
158        }
159
160        // skip trailing space chars before comma or semicolon.
161        // (compatibility with RFC 1779)
162        for (; pos < length && chars[pos] == ' '; pos++) {
163        }
164
165        return new String(chars, beg, end - beg);
166    }
167
168    // gets hex string attribute value: "#" hexstring
169    private String hexAV() throws IOException {
170
171        if (pos + 4 >= length) {
172            // encoded byte array  must be not less then 4 c
173            throw new IOException(ERROR_PARSE_ERROR);
174        }
175
176        beg = pos; // store '#' position
177        pos++;
178        while (true) {
179
180            // check for end of attribute value
181            // looks for space and component separators
182            if (pos == length || chars[pos] == '+' || chars[pos] == ','
183                    || chars[pos] == ';') {
184                end = pos;
185                break;
186            }
187
188            if (chars[pos] == ' ') {
189                end = pos;
190                pos++;
191                // skip trailing space chars before comma or semicolon.
192                // (compatibility with RFC 1779)
193                for (; pos < length && chars[pos] == ' '; pos++) {
194                }
195                break;
196            } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
197                chars[pos] += 32; //to low case
198            }
199
200            pos++;
201        }
202
203        // verify length of hex string
204        // encoded byte array  must be not less then 4 and must be even number
205        int hexLen = end - beg; // skip first '#' char
206        if (hexLen < 5 || (hexLen & 1) == 0) {
207            throw new IOException(ERROR_PARSE_ERROR);
208        }
209
210        // get byte encoding from string representation
211        byte[] encoded = new byte[hexLen / 2];
212        for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
213            encoded[i] = (byte) getByte(p);
214        }
215
216        return new String(chars, beg, hexLen);
217    }
218
219    // gets string attribute value: *( stringchar / pair )
220    private String escapedAV() throws IOException {
221
222        beg = pos;
223        end = pos;
224        while (true) {
225
226            if (pos >= length) {
227                // the end of DN has been found
228                return new String(chars, beg, end - beg);
229            }
230
231            switch (chars[pos]) {
232            case '+':
233            case ',':
234            case ';':
235                // separator char has beed found
236                return new String(chars, beg, end - beg);
237            case '\\':
238                // escaped char
239                chars[end++] = getEscaped();
240                pos++;
241                break;
242            case ' ':
243                // need to figure out whether space defines
244                // the end of attribute value or not
245                cur = end;
246
247                pos++;
248                chars[end++] = ' ';
249
250                for (; pos < length && chars[pos] == ' '; pos++) {
251                    chars[end++] = ' ';
252                }
253                if (pos == length || chars[pos] == ',' || chars[pos] == '+'
254                        || chars[pos] == ';') {
255                    // separator char or the end of DN has beed found
256                    return new String(chars, beg, cur - beg);
257                }
258                break;
259            default:
260                chars[end++] = chars[pos];
261                pos++;
262            }
263        }
264    }
265
266    // returns escaped char
267    private char getEscaped() throws IOException {
268
269        pos++;
270        if (pos == length) {
271            throw new IOException(ERROR_PARSE_ERROR);
272        }
273
274        switch (chars[pos]) {
275        case '"':
276        case '\\':
277        case ',':
278        case '=':
279        case '+':
280        case '<':
281        case '>':
282        case '#':
283        case ';':
284        case ' ':
285        case '*':
286        case '%':
287        case '_':
288            //FIXME: escaping is allowed only for leading or trailing space char
289            return chars[pos];
290        default:
291            // RFC doesn't explicitly say that escaped hex pair is
292            // interpreted as UTF-8 char. It only contains an example of such DN.
293            return getUTF8();
294        }
295    }
296
297    // decodes UTF-8 char
298    // see http://www.unicode.org for UTF-8 bit distribution table
299    private char getUTF8() throws IOException {
300
301        int res = getByte(pos);
302        pos++; //FIXME tmp
303
304        if (res < 128) { // one byte: 0-7F
305            return (char) res;
306        } else if (res >= 192 && res <= 247) {
307
308            int count;
309            if (res <= 223) { // two bytes: C0-DF
310                count = 1;
311                res = res & 0x1F;
312            } else if (res <= 239) { // three bytes: E0-EF
313                count = 2;
314                res = res & 0x0F;
315            } else { // four bytes: F0-F7
316                count = 3;
317                res = res & 0x07;
318            }
319
320            int b;
321            for (int i = 0; i < count; i++) {
322                pos++;
323                if (pos == length || chars[pos] != '\\') {
324                    return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
325                }
326                pos++;
327
328                b = getByte(pos);
329                pos++; //FIXME tmp
330                if ((b & 0xC0) != 0x80) {
331                    return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
332                }
333
334                res = (res << 6) + (b & 0x3F);
335            }
336            return (char) res;
337        } else {
338            return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
339        }
340    }
341
342    // Returns byte representation of a char pair
343    // The char pair is composed of DN char in
344    // specified 'position' and the next char
345    // According to BNF syntax:
346    // hexchar    = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
347    //                    / "a" / "b" / "c" / "d" / "e" / "f"
348    private int getByte(int position) throws IOException {
349
350        if ((position + 1) >= length) {
351            // to avoid ArrayIndexOutOfBoundsException
352            throw new IOException(ERROR_PARSE_ERROR);
353        }
354
355        int b1, b2;
356
357        b1 = chars[position];
358        if (b1 >= '0' && b1 <= '9') {
359            b1 = b1 - '0';
360        } else if (b1 >= 'a' && b1 <= 'f') {
361            b1 = b1 - 87; // 87 = 'a' - 10
362        } else if (b1 >= 'A' && b1 <= 'F') {
363            b1 = b1 - 55; // 55 = 'A' - 10
364        } else {
365            throw new IOException(ERROR_PARSE_ERROR);
366        }
367
368        b2 = chars[position + 1];
369        if (b2 >= '0' && b2 <= '9') {
370            b2 = b2 - '0';
371        } else if (b2 >= 'a' && b2 <= 'f') {
372            b2 = b2 - 87; // 87 = 'a' - 10
373        } else if (b2 >= 'A' && b2 <= 'F') {
374            b2 = b2 - 55; // 55 = 'A' - 10
375        } else {
376            throw new IOException(ERROR_PARSE_ERROR);
377        }
378
379        return (b1 << 4) + b2;
380    }
381
382    /**
383     * Parses the DN and returns the attribute value for an attribute type.
384     *
385     * @param attributeType attribute type to look for (e.g. "ca")
386     * @return value of the attribute that first found, or null if none found
387     */
388    public String find(String attributeType) {
389        try {
390            // Initialize internal state.
391            pos = 0;
392            beg = 0;
393            end = 0;
394            cur = 0;
395            chars = dn.toCharArray();
396
397            String attType = nextAT();
398            if (attType == null) {
399                return null;
400            }
401            while (true) {
402                String attValue = "";
403
404                if (pos == length) {
405                    return null;
406                }
407
408                switch (chars[pos]) {
409                case '"':
410                    attValue = quotedAV();
411                    break;
412                case '#':
413                    attValue = hexAV();
414                    break;
415                case '+':
416                case ',':
417                case ';': // compatibility with RFC 1779: semicolon can separate RDNs
418                    //empty attribute value
419                    break;
420                default:
421                    attValue = escapedAV();
422                }
423
424                if (attributeType.equalsIgnoreCase(attType)) {
425                    return attValue;
426                }
427
428                if (pos >= length) {
429                    return null;
430                }
431
432                if (chars[pos] == ',' || chars[pos] == ';') {
433                } else if (chars[pos] != '+') {
434                    throw new IOException(ERROR_PARSE_ERROR);
435                }
436
437                pos++;
438                attType = nextAT();
439                if (attType == null) {
440                    throw new IOException(ERROR_PARSE_ERROR);
441                }
442            }
443        } catch (IOException e) {
444            // Parse error shouldn't happen, because we only handle DNs that
445            // X500Principal.getName() returns, which shouldn't be malformed.
446            Log.e(TAG, "Failed to parse DN: " + dn);
447            return null;
448        }
449    }
450}
451