1 /* $OpenBSD: a_time_posix.c,v 1.5 2024/02/18 16:28:38 tb Exp $ */ 2 /* 3 * Copyright (c) 2022, Google Inc. 4 * Copyright (c) 2022, Bob Beck <beck@obtuse.com> 5 * 6 * Permission to use, copy, modify, and/or distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 */ 18 19 /* 20 * Time conversion to/from POSIX time_t and struct tm, with no support 21 * for time zones other than UTC 22 */ 23 24 #include <inttypes.h> 25 #include <limits.h> 26 #include <stdint.h> 27 #include <string.h> 28 #include <time.h> 29 30 #include <openssl/asn1.h> 31 #include <openssl/posix_time.h> 32 33 #include "crypto_internal.h" 34 35 #define SECS_PER_HOUR (int64_t)(60 * 60) 36 #define SECS_PER_DAY (int64_t)(24 * SECS_PER_HOUR) 37 38 /* 39 * Is a year/month/day combination valid, in the range from year 0000 40 * to 9999? 41 */ 42 static int 43 is_valid_date(int64_t year, int64_t month, int64_t day) 44 { 45 int days_in_month; 46 if (day < 1 || month < 1 || year < 0 || year > 9999) 47 return 0; 48 switch (month) { 49 case 1: 50 case 3: 51 case 5: 52 case 7: 53 case 8: 54 case 10: 55 case 12: 56 days_in_month = 31; 57 break; 58 case 4: 59 case 6: 60 case 9: 61 case 11: 62 days_in_month = 30; 63 break; 64 case 2: 65 if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) 66 days_in_month = 29; 67 else 68 days_in_month = 28; 69 break; 70 default: 71 return 0; 72 } 73 return day <= days_in_month; 74 } 75 76 /* 77 * Is a time valid? Leap seconds of 60 are not considered valid, as 78 * the POSIX time in seconds does not include them. 79 */ 80 static int 81 is_valid_time(int hours, int minutes, int seconds) 82 { 83 return hours >= 0 && minutes >= 0 && seconds >= 0 && hours <= 23 && 84 minutes <= 59 && seconds <= 59; 85 } 86 87 /* 0000-01-01 00:00:00 UTC */ 88 #define MIN_POSIX_TIME INT64_C(-62167219200) 89 /* 9999-12-31 23:59:59 UTC */ 90 #define MAX_POSIX_TIME INT64_C(253402300799) 91 92 /* Is a int64 time representing a time within our expected range? */ 93 static int 94 is_valid_posix_time(int64_t time) 95 { 96 return MIN_POSIX_TIME <= time && time <= MAX_POSIX_TIME; 97 } 98 99 /* 100 * Inspired by algorithms presented in 101 * https://howardhinnant.github.io/date_algorithms.html 102 * (Public Domain) 103 */ 104 static int 105 posix_time_from_utc(int64_t year, int64_t month, int64_t day, int64_t hours, 106 int64_t minutes, int64_t seconds, int64_t *out_time) 107 { 108 int64_t era, year_of_era, day_of_year, day_of_era, posix_days; 109 110 if (!is_valid_date(year, month, day) || 111 !is_valid_time(hours, minutes, seconds)) 112 return 0; 113 if (month <= 2) 114 year--; /* Start years on Mar 1, so leap days end a year. */ 115 116 /* At this point year will be in the range -1 and 9999.*/ 117 era = (year >= 0 ? year : year - 399) / 400; 118 year_of_era = year - era * 400; 119 day_of_year = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 120 5 + day - 1; 121 day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 122 100 + day_of_year; 123 posix_days = era * 146097 + day_of_era - 719468; 124 *out_time = posix_days * SECS_PER_DAY + hours * SECS_PER_HOUR + 125 minutes * 60 + seconds; 126 127 return 1; 128 } 129 130 /* 131 * Inspired by algorithms presented in 132 * https://howardhinnant.github.io/date_algorithms.html 133 * (Public Domain) 134 */ 135 static int 136 utc_from_posix_time(int64_t time, int *out_year, int *out_month, int *out_day, 137 int *out_hours, int *out_minutes, int *out_seconds) 138 { 139 int64_t days, leftover_seconds, era, day_of_era, year_of_era, 140 day_of_year, month_of_year; 141 142 if (!is_valid_posix_time(time)) 143 return 0; 144 145 days = time / SECS_PER_DAY; 146 leftover_seconds = time % SECS_PER_DAY; 147 if (leftover_seconds < 0) { 148 days--; 149 leftover_seconds += SECS_PER_DAY; 150 } 151 days += 719468; /* Shift to starting epoch of Mar 1 0000. */ 152 153 /* At this point, days will be in the range -61 and 3652364. */ 154 era = (days > 0 ? days : days - 146096) / 146097; 155 day_of_era = days - era * 146097; 156 year_of_era = (day_of_era - day_of_era / 1460 + day_of_era / 36524 - 157 day_of_era / 146096) / 158 365; 159 *out_year = year_of_era + era * 400; /* Year starts on Mar 1 */ 160 day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - 161 year_of_era / 100); 162 month_of_year = (5 * day_of_year + 2) / 153; 163 *out_month = (month_of_year < 10 ? month_of_year + 3 : 164 month_of_year - 9); 165 if (*out_month <= 2) 166 (*out_year)++; /* Adjust year back to Jan 1 start of year. */ 167 168 *out_day = day_of_year - (153 * month_of_year + 2) / 5 + 1; 169 *out_hours = leftover_seconds / SECS_PER_HOUR; 170 leftover_seconds %= SECS_PER_HOUR; 171 *out_minutes = leftover_seconds / 60; 172 *out_seconds = leftover_seconds % 60; 173 174 return 1; 175 } 176 177 int 178 OPENSSL_tm_to_posix(const struct tm *tm, int64_t *out) 179 { 180 return posix_time_from_utc(tm->tm_year + (int64_t)1900, 181 tm->tm_mon + (int64_t)1, tm->tm_mday, tm->tm_hour, tm->tm_min, 182 tm->tm_sec, out); 183 } 184 LCRYPTO_ALIAS(OPENSSL_tm_to_posix); 185 186 int 187 OPENSSL_posix_to_tm(int64_t time, struct tm *out_tm) 188 { 189 struct tm tmp_tm = {0}; 190 191 memset(out_tm, 0, sizeof(*out_tm)); 192 193 if (!utc_from_posix_time(time, &tmp_tm.tm_year, &tmp_tm.tm_mon, 194 &tmp_tm.tm_mday, &tmp_tm.tm_hour, &tmp_tm.tm_min, &tmp_tm.tm_sec)) 195 return 0; 196 197 tmp_tm.tm_year -= 1900; 198 tmp_tm.tm_mon -= 1; 199 200 *out_tm = tmp_tm; 201 202 return 1; 203 } 204 LCRYPTO_ALIAS(OPENSSL_posix_to_tm); 205 206 int 207 asn1_time_tm_to_time_t(const struct tm *tm, time_t *out) 208 { 209 int64_t posix_time; 210 211 if (!OPENSSL_tm_to_posix(tm, &posix_time)) 212 return 0; 213 214 #ifdef SMALL_TIME_T 215 /* For portable. */ 216 if (sizeof(time_t) == sizeof(int32_t) && 217 (posix_time > INT32_MAX || posix_time < INT32_MIN)) 218 return 0; 219 #endif 220 221 *out = posix_time; 222 return 1; 223 } 224 225 int 226 asn1_time_time_t_to_tm(const time_t *time, struct tm *out_tm) 227 { 228 int64_t posix_time = *time; 229 230 return OPENSSL_posix_to_tm(posix_time, out_tm); 231 } 232 233 int 234 OPENSSL_timegm(const struct tm *tm, time_t *out) { 235 return asn1_time_tm_to_time_t(tm, out); 236 } 237 LCRYPTO_ALIAS(OPENSSL_timegm); 238 239 struct tm * 240 OPENSSL_gmtime(const time_t *time, struct tm *out_tm) { 241 if (!asn1_time_time_t_to_tm(time, out_tm)) 242 return NULL; 243 return out_tm; 244 } 245 LCRYPTO_ALIAS(OPENSSL_gmtime); 246 247 /* Public API in OpenSSL. BoringSSL uses int64_t instead of long. */ 248 int 249 OPENSSL_gmtime_adj(struct tm *tm, int offset_day, int64_t offset_sec) 250 { 251 int64_t posix_time; 252 253 if (!OPENSSL_tm_to_posix(tm, &posix_time)) 254 return 0; 255 256 CTASSERT(INT_MAX <= INT64_MAX / SECS_PER_DAY); 257 CTASSERT(MAX_POSIX_TIME <= INT64_MAX - INT_MAX * SECS_PER_DAY); 258 CTASSERT(MIN_POSIX_TIME >= INT64_MIN - INT_MIN * SECS_PER_DAY); 259 260 posix_time += offset_day * SECS_PER_DAY; 261 262 if (posix_time > 0 && offset_sec > INT64_MAX - posix_time) 263 return 0; 264 if (posix_time < 0 && offset_sec < INT64_MIN - posix_time) 265 return 0; 266 posix_time += offset_sec; 267 268 if (!OPENSSL_posix_to_tm(posix_time, tm)) 269 return 0; 270 271 return 1; 272 } 273 274 int 275 OPENSSL_gmtime_diff(int *out_days, int *out_secs, const struct tm *from, 276 const struct tm *to) 277 { 278 int64_t time_to, time_from, timediff, daydiff; 279 280 if (!OPENSSL_tm_to_posix(to, &time_to) || 281 !OPENSSL_tm_to_posix(from, &time_from)) 282 return 0; 283 284 /* Times are in range, so these calculations cannot overflow. */ 285 CTASSERT(SECS_PER_DAY <= INT_MAX); 286 CTASSERT((MAX_POSIX_TIME - MIN_POSIX_TIME) / SECS_PER_DAY <= INT_MAX); 287 288 timediff = time_to - time_from; 289 daydiff = timediff / SECS_PER_DAY; 290 timediff %= SECS_PER_DAY; 291 292 *out_secs = timediff; 293 *out_days = daydiff; 294 295 return 1; 296 } 297