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
next_table(off_t * offset,off_t * nrecords)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
read_records(int fd,off_t offset,uint8_t tag1_out[TAG_LEN],uint32_t * timestamp1_out,uint8_t tag2_out[TAG_LEN],uint32_t * timestamp2_out,int * nread)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
write_record(int fd,off_t offset,const uint8_t tag[TAG_LEN],uint32_t timestamp)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
expired(uint32_t timestamp,uint32_t now,uint32_t skew)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
store(krb5_context context,int fd,const uint8_t tag[TAG_LEN],uint32_t now,uint32_t skew)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
k5_rcfile2_store(krb5_context context,int fd,const krb5_data * tag_data)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
file2_resolve(krb5_context context,const char * residual,void ** rcdata_out)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
file2_close(krb5_context context,void * rcdata)236 file2_close(krb5_context context, void *rcdata)
237 {
238 free(rcdata);
239 }
240
241 static krb5_error_code
file2_store(krb5_context context,void * rcdata,const krb5_data * tag)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