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