summaryrefslogtreecommitdiffstats
path: root/core/java/com/android/internal/net/DNParser.java
blob: 5254207cd78e4f745e4c6d01ac70c05a83e14adb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.android.internal.net;


import android.util.Log;

import java.io.IOException;

import javax.security.auth.x500.X500Principal;

/**
 * A simple distinguished name(DN) parser.
 *
 * <p>This class is based on org.apache.harmony.security.x509.DNParser.  It's customized to remove
 * external references which are unnecessary for our requirements.
 *
 * <p>This class is only meant for extracting a string value from a DN.  e.g. it doesn't support
 * values in the hex-string style.
 *
 * <p>This class is used by {@link DomainNameValidator} only.  However, in order to make this
 * class visible from unit tests, it's made public.
 * 
 * @hide
 */
public final class DNParser {
    private static final String TAG = "DNParser";

    /** DN to be parsed. */
    private final String dn;

    // length of distinguished name string
    private final int length;

    private int pos, beg, end;

    // tmp vars to store positions of the currently parsed item
    private int cur;

    // distinguished name chars
    private char[] chars;

    /**
     * Exception message thrown when we failed to parse DN, which shouldn't happen because we
     * only handle DNs that {@link X500Principal#getName} returns, which shouldn't be malformed.
     */
    private static final String ERROR_PARSE_ERROR = "Failed to parse DN";

    /**
     * Constructor.
     *
     * @param principal - {@link X500Principal} to be parsed
     */
    public DNParser(X500Principal principal) {
        this.dn = principal.getName(X500Principal.RFC2253);
        this.length = dn.length();
    }

    // gets next attribute type: (ALPHA 1*keychar) / oid
    private String nextAT() throws IOException {

        // skip preceding space chars, they can present after
        // comma or semicolon (compatibility with RFC 1779)
        for (; pos < length && chars[pos] == ' '; pos++) {
        }
        if (pos == length) {
            return null; // reached the end of DN
        }

        // mark the beginning of attribute type
        beg = pos;

        // attribute type chars
        pos++;
        for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
            // we don't follow exact BNF syntax here:
            // accept any char except space and '='
        }
        if (pos >= length) {
            // unexpected end of DN
            throw new IOException(ERROR_PARSE_ERROR);
        }

        // mark the end of attribute type
        end = pos;

        // skip trailing space chars between attribute type and '='
        // (compatibility with RFC 1779)
        if (chars[pos] == ' ') {
            for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
            }

            if (chars[pos] != '=' || pos == length) {
                // unexpected end of DN
                throw new IOException(ERROR_PARSE_ERROR);
            }
        }

        pos++; //skip '=' char

        // skip space chars between '=' and attribute value
        // (compatibility with RFC 1779)
        for (; pos < length && chars[pos] == ' '; pos++) {
        }

        // in case of oid attribute type skip its prefix: "oid." or "OID."
        // (compatibility with RFC 1779)
        if ((end - beg > 4) && (chars[beg + 3] == '.')
                && (chars[beg] == 'O' || chars[beg] == 'o')
                && (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
                && (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
            beg += 4;
        }

        return new String(chars, beg, end - beg);
    }

    // gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
    private String quotedAV() throws IOException {

        pos++;
        beg = pos;
        end = beg;
        while (true) {

            if (pos == length) {
                // unexpected end of DN
                throw new IOException(ERROR_PARSE_ERROR);
            }

            if (chars[pos] == '"') {
                // enclosing quotation was found
                pos++;
                break;
            } else if (chars[pos] == '\\') {
                chars[end] = getEscaped();
            } else {
                // shift char: required for string with escaped chars
                chars[end] = chars[pos];
            }
            pos++;
            end++;
        }

        // skip trailing space chars before comma or semicolon.
        // (compatibility with RFC 1779)
        for (; pos < length && chars[pos] == ' '; pos++) {
        }

        return new String(chars, beg, end - beg);
    }

    // gets hex string attribute value: "#" hexstring
    private String hexAV() throws IOException {

        if (pos + 4 >= length) {
            // encoded byte array  must be not less then 4 c
            throw new IOException(ERROR_PARSE_ERROR);
        }

        beg = pos; // store '#' position
        pos++;
        while (true) {

            // check for end of attribute value
            // looks for space and component separators
            if (pos == length || chars[pos] == '+' || chars[pos] == ','
                    || chars[pos] == ';') {
                end = pos;
                break;
            }

            if (chars[pos] == ' ') {
                end = pos;
                pos++;
                // skip trailing space chars before comma or semicolon.
                // (compatibility with RFC 1779)
                for (; pos < length && chars[pos] == ' '; pos++) {
                }
                break;
            } else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
                chars[pos] += 32; //to low case
            }

            pos++;
        }

        // verify length of hex string
        // encoded byte array  must be not less then 4 and must be even number
        int hexLen = end - beg; // skip first '#' char
        if (hexLen < 5 || (hexLen & 1) == 0) {
            throw new IOException(ERROR_PARSE_ERROR);
        }

        // get byte encoding from string representation
        byte[] encoded = new byte[hexLen / 2];
        for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
            encoded[i] = (byte) getByte(p);
        }

        return new String(chars, beg, hexLen);
    }

    // gets string attribute value: *( stringchar / pair )
    private String escapedAV() throws IOException {

        beg = pos;
        end = pos;
        while (true) {

            if (pos >= length) {
                // the end of DN has been found
                return new String(chars, beg, end - beg);
            }

            switch (chars[pos]) {
            case '+':
            case ',':
            case ';':
                // separator char has beed found
                return new String(chars, beg, end - beg);
            case '\\':
                // escaped char
                chars[end++] = getEscaped();
                pos++;
                break;
            case ' ':
                // need to figure out whether space defines
                // the end of attribute value or not
                cur = end;

                pos++;
                chars[end++] = ' ';

                for (; pos < length && chars[pos] == ' '; pos++) {
                    chars[end++] = ' ';
                }
                if (pos == length || chars[pos] == ',' || chars[pos] == '+'
                        || chars[pos] == ';') {
                    // separator char or the end of DN has beed found
                    return new String(chars, beg, cur - beg);
                }
                break;
            default:
                chars[end++] = chars[pos];
                pos++;
            }
        }
    }

    // returns escaped char
    private char getEscaped() throws IOException {

        pos++;
        if (pos == length) {
            throw new IOException(ERROR_PARSE_ERROR);
        }

        switch (chars[pos]) {
        case '"':
        case '\\':
        case ',':
        case '=':
        case '+':
        case '<':
        case '>':
        case '#':
        case ';':
        case ' ':
        case '*':
        case '%':
        case '_':
            //FIXME: escaping is allowed only for leading or trailing space char
            return chars[pos];
        default:
            // RFC doesn't explicitly say that escaped hex pair is
            // interpreted as UTF-8 char. It only contains an example of such DN.
            return getUTF8();
        }
    }

    // decodes UTF-8 char
    // see http://www.unicode.org for UTF-8 bit distribution table
    private char getUTF8() throws IOException {

        int res = getByte(pos);
        pos++; //FIXME tmp

        if (res < 128) { // one byte: 0-7F
            return (char) res;
        } else if (res >= 192 && res <= 247) {

            int count;
            if (res <= 223) { // two bytes: C0-DF
                count = 1;
                res = res & 0x1F;
            } else if (res <= 239) { // three bytes: E0-EF
                count = 2;
                res = res & 0x0F;
            } else { // four bytes: F0-F7
                count = 3;
                res = res & 0x07;
            }

            int b;
            for (int i = 0; i < count; i++) {
                pos++;
                if (pos == length || chars[pos] != '\\') {
                    return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
                }
                pos++;

                b = getByte(pos);
                pos++; //FIXME tmp
                if ((b & 0xC0) != 0x80) {
                    return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
                }

                res = (res << 6) + (b & 0x3F);
            }
            return (char) res;
        } else {
            return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
        }
    }

    // Returns byte representation of a char pair
    // The char pair is composed of DN char in
    // specified 'position' and the next char
    // According to BNF syntax:
    // hexchar    = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
    //                    / "a" / "b" / "c" / "d" / "e" / "f"
    private int getByte(int position) throws IOException {

        if ((position + 1) >= length) {
            // to avoid ArrayIndexOutOfBoundsException
            throw new IOException(ERROR_PARSE_ERROR);
        }

        int b1, b2;

        b1 = chars[position];
        if (b1 >= '0' && b1 <= '9') {
            b1 = b1 - '0';
        } else if (b1 >= 'a' && b1 <= 'f') {
            b1 = b1 - 87; // 87 = 'a' - 10
        } else if (b1 >= 'A' && b1 <= 'F') {
            b1 = b1 - 55; // 55 = 'A' - 10
        } else {
            throw new IOException(ERROR_PARSE_ERROR);
        }

        b2 = chars[position + 1];
        if (b2 >= '0' && b2 <= '9') {
            b2 = b2 - '0';
        } else if (b2 >= 'a' && b2 <= 'f') {
            b2 = b2 - 87; // 87 = 'a' - 10
        } else if (b2 >= 'A' && b2 <= 'F') {
            b2 = b2 - 55; // 55 = 'A' - 10
        } else {
            throw new IOException(ERROR_PARSE_ERROR);
        }

        return (b1 << 4) + b2;
    }

    /**
     * Parses the DN and returns the attribute value for an attribute type.
     *
     * @param attributeType attribute type to look for (e.g. "ca")
     * @return value of the attribute that first found, or null if none found
     */
    public String find(String attributeType) {
        try {
            // Initialize internal state.
            pos = 0;
            beg = 0;
            end = 0;
            cur = 0;
            chars = dn.toCharArray();

            String attType = nextAT();
            if (attType == null) {
                return null;
            }
            while (true) {
                String attValue = "";

                if (pos == length) {
                    return null;
                }

                switch (chars[pos]) {
                case '"':
                    attValue = quotedAV();
                    break;
                case '#':
                    attValue = hexAV();
                    break;
                case '+':
                case ',':
                case ';': // compatibility with RFC 1779: semicolon can separate RDNs
                    //empty attribute value
                    break;
                default:
                    attValue = escapedAV();
                }

                if (attributeType.equalsIgnoreCase(attType)) {
                    return attValue;
                }

                if (pos >= length) {
                    return null;
                }

                if (chars[pos] == ',' || chars[pos] == ';') {
                } else if (chars[pos] != '+') {
                    throw new IOException(ERROR_PARSE_ERROR);
                }

                pos++;
                attType = nextAT();
                if (attType == null) {
                    throw new IOException(ERROR_PARSE_ERROR);
                }
            }
        } catch (IOException e) {
            // Parse error shouldn't happen, because we only handle DNs that
            // X500Principal.getName() returns, which shouldn't be malformed.
            Log.e(TAG, "Failed to parse DN: " + dn);
            return null;
        }
    }
}