001/* 002 * Copyright 2017-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2017-2018 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.asn1; 022 023 024 025import java.text.SimpleDateFormat; 026import java.util.Date; 027import java.util.Calendar; 028import java.util.GregorianCalendar; 029import java.util.TimeZone; 030 031import com.unboundid.util.Debug; 032import com.unboundid.util.NotMutable; 033import com.unboundid.util.ThreadSafety; 034import com.unboundid.util.ThreadSafetyLevel; 035import com.unboundid.util.StaticUtils; 036 037import static com.unboundid.asn1.ASN1Messages.*; 038 039 040 041/** 042 * This class provides an ASN.1 UTC time element, which represents a timestamp 043 * with a string representation in the format "YYMMDDhhmmssZ". Although the 044 * general UTC time format considers the seconds element to be optional, the 045 * ASN.1 specification requires the element to be present. 046 * <BR><BR> 047 * Note that the UTC time format only allows two digits for the year, which is 048 * obviously prone to causing problems when deciding which century is implied 049 * by the timestamp. The official specification does not indicate which 050 * behavior should be used, so this implementation will use the same logic as 051 * Java's {@code SimpleDateFormat} class, which infers the century using a 052 * sliding window that assumes that the year is somewhere between 80 years 053 * before and 20 years after the current time. For example, if the current year 054 * is 2017, the following values would be inferred: 055 * <UL> 056 * <LI>A year of "40" would be interpreted as 1940.</LI> 057 * <LI>A year of "50" would be interpreted as 1950.</LI> 058 * <LI>A year of "60" would be interpreted as 1960.</LI> 059 * <LI>A year of "70" would be interpreted as 1970.</LI> 060 * <LI>A year of "80" would be interpreted as 1980.</LI> 061 * <LI>A year of "90" would be interpreted as 1990.</LI> 062 * <LI>A year of "00" would be interpreted as 2000.</LI> 063 * <LI>A year of "10" would be interpreted as 2010.</LI> 064 * <LI>A year of "20" would be interpreted as 2020.</LI> 065 * <LI>A year of "30" would be interpreted as 2030.</LI> 066 * </UL> 067 * <BR><BR> 068 * UTC time elements should generally only be used for historical purposes in 069 * encodings that require them. For new cases in which a timestamp may be 070 * required, you should use some other format to represent the timestamp. The 071 * {@link ASN1GeneralizedTime} element type does use a four-digit year (and also 072 * allows for the possibility of sub-second values), so it may be a good fit. 073 * You may also want to use a general-purpose string format like 074 * {@link ASN1OctetString} that is flexible enough to support whatever encoding 075 * you want. 076 */ 077@NotMutable() 078@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 079public final class ASN1UTCTime 080 extends ASN1Element 081{ 082 /** 083 * The thread-local date formatter used to encode and decode UTC time values. 084 */ 085 private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTERS = 086 new ThreadLocal<>(); 087 088 089 090 /** 091 * The serial version UID for this serializable class. 092 */ 093 private static final long serialVersionUID = -3107099228691194285L; 094 095 096 097 // The timestamp represented by this UTC time value. 098 private final long time; 099 100 // The string representation of the UTC time value. 101 private final String stringRepresentation; 102 103 104 105 /** 106 * Creates a new UTC time element with the default BER type that represents 107 * the current time. 108 */ 109 public ASN1UTCTime() 110 { 111 this(ASN1Constants.UNIVERSAL_UTC_TIME_TYPE); 112 } 113 114 115 116 /** 117 * Creates a new UTC time element with the specified BER type that represents 118 * the current time. 119 * 120 * @param type The BER type to use for this element. 121 */ 122 public ASN1UTCTime(final byte type) 123 { 124 this(type, System.currentTimeMillis()); 125 } 126 127 128 129 /** 130 * Creates a new UTC time element with the default BER type that represents 131 * the indicated time. 132 * 133 * @param date The date value that specifies the time to represent. This 134 * must not be {@code null}. Note that the time that is 135 * actually represented by the element will have its 136 * milliseconds component set to zero. 137 */ 138 public ASN1UTCTime(final Date date) 139 { 140 this(ASN1Constants.UNIVERSAL_UTC_TIME_TYPE, date.getTime()); 141 } 142 143 144 145 /** 146 * Creates a new UTC time element with the specified BER type that represents 147 * the indicated time. 148 * 149 * @param type The BER type to use for this element. 150 * @param date The date value that specifies the time to represent. This 151 * must not be {@code null}. Note that the time that is 152 * actually represented by the element will have its 153 * milliseconds component set to zero. 154 */ 155 public ASN1UTCTime(final byte type, final Date date) 156 { 157 this(type, date.getTime()); 158 } 159 160 161 162 /** 163 * Creates a new UTC time element with the default BER type that represents 164 * the indicated time. 165 * 166 * @param time The time to represent. This must be expressed in 167 * milliseconds since the epoch (the same format used by 168 * {@code System.currentTimeMillis()} and 169 * {@code Date.getTime()}). Note that the time that is actually 170 * represented by the element will have its milliseconds 171 * component set to zero. 172 */ 173 public ASN1UTCTime(final long time) 174 { 175 this(ASN1Constants.UNIVERSAL_UTC_TIME_TYPE, time); 176 } 177 178 179 180 /** 181 * Creates a new UTC time element with the specified BER type that represents 182 * the indicated time. 183 * 184 * @param type The BER type to use for this element. 185 * @param time The time to represent. This must be expressed in 186 * milliseconds since the epoch (the same format used by 187 * {@code System.currentTimeMillis()} and 188 * {@code Date.getTime()}). Note that the time that is actually 189 * represented by the element will have its milliseconds 190 * component set to zero. 191 */ 192 public ASN1UTCTime(final byte type, final long time) 193 { 194 super(type, StaticUtils.getBytes(encodeTimestamp(time))); 195 196 final GregorianCalendar calendar = 197 new GregorianCalendar(StaticUtils.getUTCTimeZone()); 198 calendar.setTimeInMillis(time); 199 calendar.set(Calendar.MILLISECOND, 0); 200 201 this.time = calendar.getTimeInMillis(); 202 stringRepresentation = encodeTimestamp(time); 203 } 204 205 206 207 /** 208 * Creates a new UTC time element with the default BER type and a time decoded 209 * from the provided string representation. 210 * 211 * @param timestamp The string representation of the timestamp to represent. 212 * This must not be {@code null}. 213 * 214 * @throws ASN1Exception If the provided timestamp does not represent a 215 * valid ASN.1 UTC time string representation. 216 */ 217 public ASN1UTCTime(final String timestamp) 218 throws ASN1Exception 219 { 220 this(ASN1Constants.UNIVERSAL_UTC_TIME_TYPE, timestamp); 221 } 222 223 224 225 /** 226 * Creates a new UTC time element with the specified BER type and a time 227 * decoded from the provided string representation. 228 * 229 * @param type The BER type to use for this element. 230 * @param timestamp The string representation of the timestamp to represent. 231 * This must not be {@code null}. 232 * 233 * @throws ASN1Exception If the provided timestamp does not represent a 234 * valid ASN.1 UTC time string representation. 235 */ 236 public ASN1UTCTime(final byte type, final String timestamp) 237 throws ASN1Exception 238 { 239 super(type, StaticUtils.getBytes(timestamp)); 240 241 time = decodeTimestamp(timestamp); 242 stringRepresentation = timestamp; 243 } 244 245 246 247 /** 248 * Encodes the time represented by the provided date into the appropriate 249 * ASN.1 UTC time format. 250 * 251 * @param date The date value that specifies the time to represent. This 252 * must not be {@code null}. 253 * 254 * @return The encoded timestamp. 255 */ 256 public static String encodeTimestamp(final Date date) 257 { 258 return getDateFormatter().format(date); 259 } 260 261 262 263 /** 264 * Gets a date formatter instance, using a thread-local instance if one 265 * exists, or creating a new one if not. 266 * 267 * @return A date formatter instance. 268 */ 269 private static SimpleDateFormat getDateFormatter() 270 { 271 final SimpleDateFormat existingFormatter = DATE_FORMATTERS.get(); 272 if (existingFormatter != null) 273 { 274 return existingFormatter; 275 } 276 277 final SimpleDateFormat newFormatter 278 = new SimpleDateFormat("yyMMddHHmmss'Z'"); 279 newFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); 280 newFormatter.setLenient(false); 281 DATE_FORMATTERS.set(newFormatter); 282 return newFormatter; 283 } 284 285 286 287 /** 288 * Encodes the specified time into the appropriate ASN.1 UTC time format. 289 * 290 * @param time The time to represent. This must be expressed in 291 * milliseconds since the epoch (the same format used by 292 * {@code System.currentTimeMillis()} and 293 * {@code Date.getTime()}). 294 * 295 * @return The encoded timestamp. 296 */ 297 public static String encodeTimestamp(final long time) 298 { 299 return encodeTimestamp(new Date(time)); 300 } 301 302 303 304 /** 305 * Decodes the provided string as a timestamp in the UTC time format. 306 * 307 * @param timestamp The string representation of a UTC time to be parsed as 308 * a timestamp. It must not be {@code null}. 309 * 310 * @return The decoded time, expressed in milliseconds since the epoch (the 311 * same format used by {@code System.currentTimeMillis()} and 312 * {@code Date.getTime()}). 313 * 314 * @throws ASN1Exception If the provided timestamp cannot be parsed as a 315 * valid string representation of an ASN.1 UTC time 316 * value. 317 */ 318 public static long decodeTimestamp(final String timestamp) 319 throws ASN1Exception 320 { 321 if (timestamp.length() != 13) 322 { 323 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_LENGTH.get()); 324 } 325 326 if (! (timestamp.endsWith("Z") || timestamp.endsWith("z"))) 327 { 328 throw new ASN1Exception(ERR_UTC_TIME_STRING_DOES_NOT_END_WITH_Z.get()); 329 } 330 331 for (int i=0; i < (timestamp.length() - 1); i++) 332 { 333 final char c = timestamp.charAt(i); 334 if ((c < '0') || (c > '9')) 335 { 336 throw new ASN1Exception(ERR_UTC_TIME_STRING_CHAR_NOT_DIGIT.get(i + 1)); 337 } 338 } 339 340 final int month = Integer.parseInt(timestamp.substring(2, 4)); 341 if ((month < 1) || (month > 12)) 342 { 343 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_MONTH.get()); 344 } 345 346 final int day = Integer.parseInt(timestamp.substring(4, 6)); 347 if ((day < 1) || (day > 31)) 348 { 349 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_DAY.get()); 350 } 351 352 final int hour = Integer.parseInt(timestamp.substring(6, 8)); 353 if (hour > 23) 354 { 355 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_HOUR.get()); 356 } 357 358 final int minute = Integer.parseInt(timestamp.substring(8, 10)); 359 if (minute > 59) 360 { 361 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_MINUTE.get()); 362 } 363 364 final int second = Integer.parseInt(timestamp.substring(10, 12)); 365 if (second > 60) 366 { 367 // In the case of a leap second, there can be 61 seconds in a minute. 368 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_SECOND.get()); 369 } 370 371 try 372 { 373 return getDateFormatter().parse(timestamp).getTime(); 374 } 375 catch (final Exception e) 376 { 377 // Even though we've already done a lot of validation, this could still 378 // happen if the timestamp isn't valid as a whole because one of the 379 // components is out of a range implied by another component. In the case 380 // of UTC time values, this should only happen when trying to use a day 381 // of the month that is not valid for the desired month (for example, 382 // trying to use a date of September 31, when September only has 30 days). 383 Debug.debugException(e); 384 throw new ASN1Exception( 385 ERR_UTC_TIME_STRING_CANNOT_PARSE.get( 386 StaticUtils.getExceptionMessage(e)), 387 e); 388 } 389 } 390 391 392 393 /** 394 * Retrieves the time represented by this UTC time element, expressed as the 395 * number of milliseconds since the epoch (the same format used by 396 * {@code System.currentTimeMillis()} and {@code Date.getTime()}). 397 398 * @return The time represented by this UTC time element. 399 */ 400 public long getTime() 401 { 402 return time; 403 } 404 405 406 407 /** 408 * Retrieves a {@code Date} object that is set to the time represented by this 409 * UTC time element. 410 * 411 * @return A {@code Date} object that is set ot the time represented by this 412 * UTC time element. 413 */ 414 public Date getDate() 415 { 416 return new Date(time); 417 } 418 419 420 421 /** 422 * Retrieves the string representation of the UTC time value contained in this 423 * element. 424 * 425 * @return The string representation of the UTC time value contained in this 426 * element. 427 */ 428 public String getStringRepresentation() 429 { 430 return stringRepresentation; 431 } 432 433 434 435 /** 436 * Decodes the contents of the provided byte array as a UTC time element. 437 * 438 * @param elementBytes The byte array to decode as an ASN.1 UTC time 439 * element. 440 * 441 * @return The decoded ASN.1 UTC time element. 442 * 443 * @throws ASN1Exception If the provided array cannot be decoded as a UTC 444 * time element. 445 */ 446 public static ASN1UTCTime decodeAsUTCTime(final byte[] elementBytes) 447 throws ASN1Exception 448 { 449 try 450 { 451 int valueStartPos = 2; 452 int length = (elementBytes[1] & 0x7F); 453 if (length != elementBytes[1]) 454 { 455 final int numLengthBytes = length; 456 457 length = 0; 458 for (int i=0; i < numLengthBytes; i++) 459 { 460 length <<= 8; 461 length |= (elementBytes[valueStartPos++] & 0xFF); 462 } 463 } 464 465 if ((elementBytes.length - valueStartPos) != length) 466 { 467 throw new ASN1Exception(ERR_ELEMENT_LENGTH_MISMATCH.get(length, 468 (elementBytes.length - valueStartPos))); 469 } 470 471 final byte[] elementValue = new byte[length]; 472 System.arraycopy(elementBytes, valueStartPos, elementValue, 0, length); 473 474 return new ASN1UTCTime(elementBytes[0], 475 StaticUtils.toUTF8String(elementValue)); 476 } 477 catch (final ASN1Exception ae) 478 { 479 Debug.debugException(ae); 480 throw ae; 481 } 482 catch (final Exception e) 483 { 484 Debug.debugException(e); 485 throw new ASN1Exception(ERR_ELEMENT_DECODE_EXCEPTION.get(e), e); 486 } 487 } 488 489 490 491 /** 492 * Decodes the provided ASN.1 element as a UTC time element. 493 * 494 * @param element The ASN.1 element to be decoded. 495 * 496 * @return The decoded ASN.1 UTC time element. 497 * 498 * @throws ASN1Exception If the provided element cannot be decoded as a UTC 499 * time element. 500 */ 501 public static ASN1UTCTime decodeAsUTCTime(final ASN1Element element) 502 throws ASN1Exception 503 { 504 return new ASN1UTCTime(element.getType(), 505 StaticUtils.toUTF8String(element.getValue())); 506 } 507 508 509 510 /** 511 * {@inheritDoc} 512 */ 513 @Override() 514 public void toString(final StringBuilder buffer) 515 { 516 buffer.append(stringRepresentation); 517 } 518}