001/* 002 * Copyright 2017-2025 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2017-2025 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-2025 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; 044 045import com.unboundid.util.Debug; 046import com.unboundid.util.NotMutable; 047import com.unboundid.util.NotNull; 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 @NotNull 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 @NotNull 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(@NotNull 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, @NotNull 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(@NotNull 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, @NotNull 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 @NotNull() 272 public static String encodeTimestamp(@NotNull final Date date) 273 { 274 return getDateFormatter().format(date); 275 } 276 277 278 279 /** 280 * Gets a date formatter instance, using a thread-local instance if one 281 * exists, or creating a new one if not. 282 * 283 * @return A date formatter instance. 284 */ 285 @NotNull() 286 private static SimpleDateFormat getDateFormatter() 287 { 288 final SimpleDateFormat existingFormatter = DATE_FORMATTERS.get(); 289 if (existingFormatter != null) 290 { 291 return existingFormatter; 292 } 293 294 final SimpleDateFormat newFormatter 295 = new SimpleDateFormat("yyMMddHHmmss'Z'"); 296 newFormatter.setTimeZone(StaticUtils.getUTCTimeZone()); 297 newFormatter.setLenient(false); 298 DATE_FORMATTERS.set(newFormatter); 299 return newFormatter; 300 } 301 302 303 304 /** 305 * Encodes the specified time into the appropriate ASN.1 UTC time format. 306 * 307 * @param time The time to represent. This must be expressed in 308 * milliseconds since the epoch (the same format used by 309 * {@code System.currentTimeMillis()} and 310 * {@code Date.getTime()}). 311 * 312 * @return The encoded timestamp. 313 */ 314 @NotNull() 315 public static String encodeTimestamp(final long time) 316 { 317 return encodeTimestamp(new Date(time)); 318 } 319 320 321 322 /** 323 * Decodes the provided string as a timestamp in the UTC time format. 324 * 325 * @param timestamp The string representation of a UTC time to be parsed as 326 * a timestamp. It must not be {@code null}. 327 * 328 * @return The decoded time, expressed in milliseconds since the epoch (the 329 * same format used by {@code System.currentTimeMillis()} and 330 * {@code Date.getTime()}). 331 * 332 * @throws ASN1Exception If the provided timestamp cannot be parsed as a 333 * valid string representation of an ASN.1 UTC time 334 * value. 335 */ 336 public static long decodeTimestamp(@NotNull final String timestamp) 337 throws ASN1Exception 338 { 339 if (timestamp.length() != 13) 340 { 341 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_LENGTH.get()); 342 } 343 344 if (! (timestamp.endsWith("Z") || timestamp.endsWith("z"))) 345 { 346 throw new ASN1Exception(ERR_UTC_TIME_STRING_DOES_NOT_END_WITH_Z.get()); 347 } 348 349 for (int i=0; i < (timestamp.length() - 1); i++) 350 { 351 final char c = timestamp.charAt(i); 352 if ((c < '0') || (c > '9')) 353 { 354 throw new ASN1Exception(ERR_UTC_TIME_STRING_CHAR_NOT_DIGIT.get(i + 1)); 355 } 356 } 357 358 final int month = Integer.parseInt(timestamp.substring(2, 4)); 359 if ((month < 1) || (month > 12)) 360 { 361 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_MONTH.get()); 362 } 363 364 final int day = Integer.parseInt(timestamp.substring(4, 6)); 365 if ((day < 1) || (day > 31)) 366 { 367 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_DAY.get()); 368 } 369 370 final int hour = Integer.parseInt(timestamp.substring(6, 8)); 371 if (hour > 23) 372 { 373 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_HOUR.get()); 374 } 375 376 final int minute = Integer.parseInt(timestamp.substring(8, 10)); 377 if (minute > 59) 378 { 379 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_MINUTE.get()); 380 } 381 382 final int second = Integer.parseInt(timestamp.substring(10, 12)); 383 if (second > 60) 384 { 385 // In the case of a leap second, there can be 61 seconds in a minute. 386 throw new ASN1Exception(ERR_UTC_TIME_STRING_INVALID_SECOND.get()); 387 } 388 389 try 390 { 391 return getDateFormatter().parse(timestamp).getTime(); 392 } 393 catch (final Exception e) 394 { 395 // Even though we've already done a lot of validation, this could still 396 // happen if the timestamp isn't valid as a whole because one of the 397 // components is out of a range implied by another component. In the case 398 // of UTC time values, this should only happen when trying to use a day 399 // of the month that is not valid for the desired month (for example, 400 // trying to use a date of September 31, when September only has 30 days). 401 Debug.debugException(e); 402 throw new ASN1Exception( 403 ERR_UTC_TIME_STRING_CANNOT_PARSE.get( 404 StaticUtils.getExceptionMessage(e)), 405 e); 406 } 407 } 408 409 410 411 /** 412 * Retrieves the time represented by this UTC time element, expressed as the 413 * number of milliseconds since the epoch (the same format used by 414 * {@code System.currentTimeMillis()} and {@code Date.getTime()}). 415 416 * @return The time represented by this UTC time element. 417 */ 418 public long getTime() 419 { 420 return time; 421 } 422 423 424 425 /** 426 * Retrieves a {@code Date} object that is set to the time represented by this 427 * UTC time element. 428 * 429 * @return A {@code Date} object that is set ot the time represented by this 430 * UTC time element. 431 */ 432 @NotNull() 433 public Date getDate() 434 { 435 return new Date(time); 436 } 437 438 439 440 /** 441 * Retrieves the string representation of the UTC time value contained in this 442 * element. 443 * 444 * @return The string representation of the UTC time value contained in this 445 * element. 446 */ 447 @NotNull() 448 public String getStringRepresentation() 449 { 450 return stringRepresentation; 451 } 452 453 454 455 /** 456 * Decodes the contents of the provided byte array as a UTC time element. 457 * 458 * @param elementBytes The byte array to decode as an ASN.1 UTC time 459 * element. 460 * 461 * @return The decoded ASN.1 UTC time element. 462 * 463 * @throws ASN1Exception If the provided array cannot be decoded as a UTC 464 * time element. 465 */ 466 @NotNull() 467 public static ASN1UTCTime decodeAsUTCTime(@NotNull final byte[] elementBytes) 468 throws ASN1Exception 469 { 470 try 471 { 472 int valueStartPos = 2; 473 int length = (elementBytes[1] & 0x7F); 474 if (length != elementBytes[1]) 475 { 476 final int numLengthBytes = length; 477 478 length = 0; 479 for (int i=0; i < numLengthBytes; i++) 480 { 481 length <<= 8; 482 length |= (elementBytes[valueStartPos++] & 0xFF); 483 } 484 } 485 486 if ((elementBytes.length - valueStartPos) != length) 487 { 488 throw new ASN1Exception(ERR_ELEMENT_LENGTH_MISMATCH.get(length, 489 (elementBytes.length - valueStartPos))); 490 } 491 492 final byte[] elementValue = new byte[length]; 493 System.arraycopy(elementBytes, valueStartPos, elementValue, 0, length); 494 495 return new ASN1UTCTime(elementBytes[0], 496 StaticUtils.toUTF8String(elementValue)); 497 } 498 catch (final ASN1Exception ae) 499 { 500 Debug.debugException(ae); 501 throw ae; 502 } 503 catch (final Exception e) 504 { 505 Debug.debugException(e); 506 throw new ASN1Exception(ERR_ELEMENT_DECODE_EXCEPTION.get(e), e); 507 } 508 } 509 510 511 512 /** 513 * Decodes the provided ASN.1 element as a UTC time element. 514 * 515 * @param element The ASN.1 element to be decoded. 516 * 517 * @return The decoded ASN.1 UTC time element. 518 * 519 * @throws ASN1Exception If the provided element cannot be decoded as a UTC 520 * time element. 521 */ 522 @NotNull() 523 public static ASN1UTCTime decodeAsUTCTime(@NotNull final ASN1Element element) 524 throws ASN1Exception 525 { 526 return new ASN1UTCTime(element.getType(), 527 StaticUtils.toUTF8String(element.getValue())); 528 } 529 530 531 532 /** 533 * {@inheritDoc} 534 */ 535 @Override() 536 public void toString(@NotNull final StringBuilder buffer) 537 { 538 buffer.append(stringRepresentation); 539 } 540}