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