1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 /* lib/krb5/rcache/rc_file2.c - file-based replay cache, version 2 */ 3 /* 4 * Copyright (C) 2019 by the Massachusetts Institute of Technology. 5 * All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * * Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 14 * * Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in 16 * the documentation and/or other materials provided with the 17 * distribution. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 22 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 23 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 27 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 28 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 30 * OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 #include "k5-int.h" 34 #include "k5-hashtab.h" 35 #include "rc-int.h" 36 #ifndef _WIN32 37 #include <sys/types.h> 38 #include <sys/stat.h> 39 #endif 40 41 #define MAX_SIZE INT32_MAX 42 #define TAG_LEN 12 43 #define RECORD_LEN (TAG_LEN + 4) 44 #define FIRST_TABLE_RECORDS 1023 45 46 /* Return the offset and number of records in the next table. *offset should 47 * initially be -1. */ 48 static inline krb5_error_code 49 next_table(off_t *offset, off_t *nrecords) 50 { 51 if (*offset == -1) { 52 *offset = K5_HASH_SEED_LEN; 53 *nrecords = FIRST_TABLE_RECORDS; 54 } else if (*offset == K5_HASH_SEED_LEN) { 55 *offset += *nrecords * RECORD_LEN; 56 *nrecords = (FIRST_TABLE_RECORDS + 1) * 2; 57 } else { 58 *offset += *nrecords * RECORD_LEN; 59 *nrecords *= 2; 60 } 61 62 /* Make sure the next table fits within the maximum file size. */ 63 if (*nrecords > MAX_SIZE / RECORD_LEN) 64 return EOVERFLOW; 65 if (*offset > MAX_SIZE - (*nrecords * RECORD_LEN)) 66 return EOVERFLOW; 67 68 return 0; 69 } 70 71 /* Read up to two records from fd at offset, and parse them out into tags and 72 * timestamps. Place the number of records read in *nread. */ 73 static krb5_error_code 74 read_records(int fd, off_t offset, uint8_t tag1_out[TAG_LEN], 75 uint32_t *timestamp1_out, uint8_t tag2_out[TAG_LEN], 76 uint32_t *timestamp2_out, int *nread) 77 { 78 uint8_t buf[RECORD_LEN * 2]; 79 ssize_t st; 80 81 *nread = 0; 82 83 st = lseek(fd, offset, SEEK_SET); 84 if (st == -1) 85 return errno; 86 st = read(fd, buf, RECORD_LEN * 2); 87 if (st == -1) 88 return errno; 89 90 if (st >= RECORD_LEN) { 91 memcpy(tag1_out, buf, TAG_LEN); 92 *timestamp1_out = load_32_be(buf + TAG_LEN); 93 *nread = 1; 94 } 95 if (st == RECORD_LEN * 2) { 96 memcpy(tag2_out, buf + RECORD_LEN, TAG_LEN); 97 *timestamp2_out = load_32_be(buf + RECORD_LEN + TAG_LEN); 98 *nread = 2; 99 } 100 return 0; 101 } 102 103 /* Write one record to fd at offset, marshalling the tag and timestamp. */ 104 static krb5_error_code 105 write_record(int fd, off_t offset, const uint8_t tag[TAG_LEN], 106 uint32_t timestamp) 107 { 108 uint8_t record[RECORD_LEN]; 109 ssize_t st; 110 111 memcpy(record, tag, TAG_LEN); 112 store_32_be(timestamp, record + TAG_LEN); 113 114 st = lseek(fd, offset, SEEK_SET); 115 if (st == -1) 116 return errno; 117 st = write(fd, record, RECORD_LEN); 118 if (st == -1) 119 return errno; 120 if (st != RECORD_LEN) /* Unexpected for a regular file */ 121 return EIO; 122 123 return 0; 124 } 125 126 /* Return true if timestamp is expired, for the current timestamp (now) and 127 * allowable clock skew. */ 128 static inline krb5_boolean 129 expired(uint32_t timestamp, uint32_t now, uint32_t skew) 130 { 131 return ts_after(now, ts_incr(timestamp, skew)); 132 } 133 134 /* Check and store a record into an open and locked file. fd is assumed to be 135 * at offset 0. */ 136 static krb5_error_code 137 store(krb5_context context, int fd, const uint8_t tag[TAG_LEN], uint32_t now, 138 uint32_t skew) 139 { 140 krb5_error_code ret; 141 krb5_data d; 142 off_t table_offset = -1, nrecords = 0, avail_offset = -1, record_offset; 143 ssize_t st; 144 int ind, nread; 145 uint8_t seed[K5_HASH_SEED_LEN], r1tag[TAG_LEN], r2tag[TAG_LEN]; 146 uint32_t r1stamp, r2stamp; 147 148 /* Read or generate the hash seed. */ 149 st = read(fd, seed, sizeof(seed)); 150 if (st < 0) 151 return errno; 152 if ((size_t)st < sizeof(seed)) { 153 d = make_data(seed, sizeof(seed)); 154 ret = krb5_c_random_make_octets(context, &d); 155 if (ret) 156 return ret; 157 st = write(fd, seed, sizeof(seed)); 158 if (st < 0) 159 return errno; 160 if ((size_t)st != sizeof(seed)) 161 return EIO; 162 } 163 164 for (;;) { 165 ret = next_table(&table_offset, &nrecords); 166 if (ret) 167 return ret; 168 169 ind = k5_siphash24(tag, TAG_LEN, seed) % nrecords; 170 record_offset = table_offset + ind * RECORD_LEN; 171 172 ret = read_records(fd, record_offset, r1tag, &r1stamp, r2tag, &r2stamp, 173 &nread); 174 if (ret) 175 return ret; 176 177 if ((nread >= 1 && r1stamp && memcmp(r1tag, tag, TAG_LEN) == 0) || 178 (nread == 2 && r2stamp && memcmp(r2tag, tag, TAG_LEN) == 0)) 179 return KRB5KRB_AP_ERR_REPEAT; 180 181 /* Make note of the first record available for writing (empty, beyond 182 * the end of the file, or expired). */ 183 if (avail_offset == -1) { 184 if (nread == 0 || !r1stamp || expired(r1stamp, now, skew)) 185 avail_offset = record_offset; 186 else if (nread == 1 || !r2stamp || expired(r2stamp, now, skew)) 187 avail_offset = record_offset + RECORD_LEN; 188 } 189 190 /* Stop searching if we encountered an empty record or one beyond the 191 * end of the file, as tag would have been written there previously. */ 192 if (nread < 2 || !r1stamp || !r2stamp) 193 return write_record(fd, avail_offset, tag, now); 194 195 /* Use a different hash seed for the next table we search. */ 196 seed[0]++; 197 } 198 } 199 200 krb5_error_code 201 k5_rcfile2_store(krb5_context context, int fd, const krb5_data *tag_data) 202 { 203 krb5_error_code ret; 204 krb5_timestamp now; 205 uint8_t tagbuf[TAG_LEN], *tag; 206 207 ret = krb5_timeofday(context, &now); 208 if (ret) 209 return ret; 210 211 /* Extract a tag from the authenticator checksum. */ 212 if (tag_data->length >= TAG_LEN) { 213 tag = (uint8_t *)tag_data->data; 214 } else { 215 memcpy(tagbuf, tag_data->data, tag_data->length); 216 memset(tagbuf + tag_data->length, 0, TAG_LEN - tag_data->length); 217 tag = tagbuf; 218 } 219 220 ret = krb5_lock_file(context, fd, KRB5_LOCKMODE_EXCLUSIVE); 221 if (ret) 222 return ret; 223 ret = store(context, fd, tag, now, context->clockskew); 224 (void)krb5_unlock_file(NULL, fd); 225 return ret; 226 } 227 228 static krb5_error_code 229 file2_resolve(krb5_context context, const char *residual, void **rcdata_out) 230 { 231 *rcdata_out = strdup(residual); 232 return (*rcdata_out == NULL) ? ENOMEM : 0; 233 } 234 235 static void 236 file2_close(krb5_context context, void *rcdata) 237 { 238 free(rcdata); 239 } 240 241 static krb5_error_code 242 file2_store(krb5_context context, void *rcdata, const krb5_data *tag) 243 { 244 krb5_error_code ret; 245 const char *filename = rcdata; 246 int fd; 247 248 fd = open(filename, O_CREAT | O_RDWR | O_BINARY, 0600); 249 if (fd < 0) { 250 ret = errno; 251 k5_setmsg(context, ret, "%s (filename: %s)", error_message(ret), 252 filename); 253 return ret; 254 } 255 ret = k5_rcfile2_store(context, fd, tag); 256 close(fd); 257 return ret; 258 } 259 260 const krb5_rc_ops k5_rc_file2_ops = 261 { 262 "file2", 263 file2_resolve, 264 file2_close, 265 file2_store 266 }; 267