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