xref: /freebsd/crypto/krb5/src/lib/krb5/rcache/rc_file2.c (revision 7f2fe78b9dd5f51c821d771b63d2e096f6fd49e9)
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