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