/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* lib/krb5/ccache/cc_file.c - File-based credential cache */ /* * Copyright 1990,1991,1992,1993,1994,2000,2004,2007 Massachusetts Institute of Technology. * All Rights Reserved. * * Original stdio support copyright 1995 by Cygnus Support. * * Export of this software from the United States of America may * require a specific license from the United States Government. * It is the responsibility of any person or organization contemplating * export to obtain such a license before exporting. * * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and * distribute this software and its documentation for any purpose and * without fee is hereby granted, provided that the above copyright * notice appear in all copies and that both that copyright notice and * this permission notice appear in supporting documentation, and that * the name of M.I.T. not be used in advertising or publicity pertaining * to distribution of the software without specific, written prior * permission. Furthermore if you modify this software you must label * your software as modified software and not distribute it in such a * fashion that it might be confused with the original M.I.T. software. * M.I.T. makes no representations about the suitability of * this software for any purpose. It is provided "as is" without express * or implied warranty. */ /* * A psuedo-BNF grammar for the FILE credential cache format is: * * file ::= * version (2 bytes; 05 01 for version 1 through 05 04 for version 4) * header [not present before version 4] * principal * credential1 * credential2 * ... * * header ::= * headerlen (16 bits) * header1tag (16 bits) * header1len (16 bits) * header1val (header1len bytes) * * See ccmarshal.c for the principal and credential formats. Although versions * 1 and 2 of the FILE format use native byte order for integer representations * within principals and credentials, the integer fields in the grammar above * are always in big-endian byte order. * * Only one header tag is currently defined. The tag value is 1 * (FCC_TAG_DELTATIME), and its contents are two 32-bit integers giving the * seconds and microseconds of the time offset of the KDC relative to the * client. * * Each of the file ccache functions opens and closes the file whenever it * needs to access it. * * This module depends on UNIX-like file descriptors, and UNIX-like behavior * from the functions: open, close, read, write, lseek. */ #include "k5-int.h" #include "cc-int.h" #include #include #if HAVE_UNISTD_H #include #endif #ifndef O_CLOEXEC #define O_CLOEXEC 0 #endif extern const krb5_cc_ops krb5_cc_file_ops; krb5_error_code krb5_change_cache(void); static krb5_error_code interpret_errno(krb5_context, int); /* The cache format version is a positive integer, represented in the cache * file as a two-byte big endian number with 0x0500 added to it. */ #define FVNO_BASE 0x0500 #define FCC_TAG_DELTATIME 1 #ifndef TKT_ROOT #ifdef MSDOS_FILESYSTEM #define TKT_ROOT "\\tkt" #else #define TKT_ROOT "/tmp/tkt" #endif #endif typedef struct fcc_data_st { k5_cc_mutex lock; char *filename; } fcc_data; /* Iterator over file caches. */ struct krb5_fcc_ptcursor_data { krb5_boolean first; }; /* Iterator over a cache. */ typedef struct _krb5_fcc_cursor { FILE *fp; int version; } krb5_fcc_cursor; k5_cc_mutex krb5int_cc_file_mutex = K5_CC_MUTEX_PARTIAL_INITIALIZER; /* Add fname to the standard error message for ret. */ static krb5_error_code set_errmsg_filename(krb5_context context, krb5_error_code ret, const char *fname) { if (!ret) return 0; k5_setmsg(context, ret, "%s (filename: %s)", error_message(ret), fname); return ret; } /* Get the size of the cache file as a size_t, or SIZE_MAX if it is too * large to be represented as a size_t. */ static krb5_error_code get_size(krb5_context context, FILE *fp, size_t *size_out) { struct stat sb; *size_out = 0; if (fstat(fileno(fp), &sb) == -1) return interpret_errno(context, errno); if (sizeof(off_t) > sizeof(size_t) && sb.st_size > (off_t)SIZE_MAX) *size_out = SIZE_MAX; else *size_out = sb.st_size; return 0; } /* Read len bytes from fp, storing them in buf. Return KRB5_CC_END * if not enough bytes are present. */ static krb5_error_code read_bytes(krb5_context context, FILE *fp, void *buf, size_t len) { size_t nread; nread = fread(buf, 1, len, fp); if (nread < len) return ferror(fp) ? errno : KRB5_CC_END; return 0; } /* Load four bytes from the cache file. Add them to buf (if set) and return * their value as a 32-bit unsigned integer according to the file format. */ static krb5_error_code read32(krb5_context context, FILE *fp, int version, struct k5buf *buf, uint32_t *out) { krb5_error_code ret; char bytes[4]; ret = read_bytes(context, fp, bytes, 4); if (ret) return ret; if (buf != NULL) k5_buf_add_len(buf, bytes, 4); *out = (version < 3) ? load_32_n(bytes) : load_32_be(bytes); return 0; } /* Load two bytes from the cache file and return their value as a 16-bit * unsigned integer according to the file format. */ static krb5_error_code read16(krb5_context context, FILE *fp, int version, uint16_t *out) { krb5_error_code ret; char bytes[2]; ret = read_bytes(context, fp, bytes, 2); if (ret) return ret; *out = (version < 3) ? load_16_n(bytes) : load_16_be(bytes); return 0; } /* Read len bytes from the cache file and add them to buf. */ static krb5_error_code load_bytes(krb5_context context, FILE *fp, size_t len, struct k5buf *buf) { void *ptr; ptr = k5_buf_get_space(buf, len); return (ptr == NULL) ? KRB5_CC_NOMEM : read_bytes(context, fp, ptr, len); } /* Load a 32-bit length and data from the cache file into buf, but not more * than maxsize bytes. */ static krb5_error_code load_data(krb5_context context, FILE *fp, int version, size_t maxsize, struct k5buf *buf) { krb5_error_code ret; uint32_t count; ret = read32(context, fp, version, buf, &count); if (ret) return ret; if (count > maxsize) return KRB5_CC_FORMAT; return load_bytes(context, fp, count, buf); } /* Load a marshalled principal from the cache file into buf, without * unmarshalling it. */ static krb5_error_code load_principal(krb5_context context, FILE *fp, int version, size_t maxsize, struct k5buf *buf) { krb5_error_code ret; uint32_t count; if (version > 1) { ret = load_bytes(context, fp, 4, buf); if (ret) return ret; } ret = read32(context, fp, version, buf, &count); if (ret) return ret; /* Add one for the realm (except in version 1 which already counts it). */ if (version != 1) count++; while (count-- > 0) { ret = load_data(context, fp, version, maxsize, buf); if (ret) return ret; } return 0; } /* Load a marshalled credential from the cache file into buf, without * unmarshalling it. */ static krb5_error_code load_cred(krb5_context context, FILE *fp, int version, size_t maxsize, struct k5buf *buf) { krb5_error_code ret; uint32_t count, i; /* client and server */ ret = load_principal(context, fp, version, maxsize, buf); if (ret) return ret; ret = load_principal(context, fp, version, maxsize, buf); if (ret) return ret; /* keyblock (enctype, enctype again for version 3, length, value) */ ret = load_bytes(context, fp, (version == 3) ? 4 : 2, buf); if (ret) return ret; ret = load_data(context, fp, version, maxsize, buf); if (ret) return ret; /* times (4*4 bytes), is_skey (1 byte), ticket flags (4 bytes) */ ret = load_bytes(context, fp, 4 * 4 + 1 + 4, buf); if (ret) return ret; /* addresses and authdata, both lists of {type, length, data} */ for (i = 0; i < 2; i++) { ret = read32(context, fp, version, buf, &count); if (ret) return ret; while (count-- > 0) { ret = load_bytes(context, fp, 2, buf); if (ret) return ret; ret = load_data(context, fp, version, maxsize, buf); if (ret) return ret; } } /* ticket and second_ticket */ ret = load_data(context, fp, version, maxsize, buf); if (ret) return ret; return load_data(context, fp, version, maxsize, buf); } static krb5_error_code read_principal(krb5_context context, FILE *fp, int version, krb5_principal *princ) { krb5_error_code ret; struct k5buf buf; size_t maxsize; *princ = NULL; k5_buf_init_dynamic(&buf); /* Read the principal representation into memory. */ ret = get_size(context, fp, &maxsize); if (ret) goto cleanup; ret = load_principal(context, fp, version, maxsize, &buf); if (ret) goto cleanup; ret = k5_buf_status(&buf); if (ret) goto cleanup; /* Unmarshal it from buf into princ. */ ret = k5_unmarshal_princ(buf.data, buf.len, version, princ); cleanup: k5_buf_free(&buf); return ret; } /* * Open and lock an existing cache file. If writable is true, open it for * writing (with O_APPEND) and get an exclusive lock; otherwise open it for * reading and get a shared lock. */ static krb5_error_code open_cache_file(krb5_context context, const char *filename, krb5_boolean writable, FILE **fp_out) { krb5_error_code ret; int fd, flags, lockmode; FILE *fp; *fp_out = NULL; flags = writable ? (O_RDWR | O_APPEND) : O_RDONLY; fd = open(filename, flags | O_BINARY | O_CLOEXEC, 0600); if (fd == -1) return interpret_errno(context, errno); set_cloexec_fd(fd); lockmode = writable ? KRB5_LOCKMODE_EXCLUSIVE : KRB5_LOCKMODE_SHARED; ret = krb5_lock_file(context, fd, lockmode); if (ret) { (void)close(fd); return ret; } fp = fdopen(fd, writable ? "r+b" : "rb"); if (fp == NULL) { (void)krb5_unlock_file(context, fd); (void)close(fd); return KRB5_CC_NOMEM; } *fp_out = fp; return 0; } /* Unlock and close the cache file. Do nothing if fp is NULL. */ static krb5_error_code close_cache_file(krb5_context context, FILE *fp) { int st; krb5_error_code ret; if (fp == NULL) return 0; ret = krb5_unlock_file(context, fileno(fp)); st = fclose(fp); if (ret) return ret; return st ? interpret_errno(context, errno) : 0; } /* Read the cache file header. Set time offsets in context from the header if * appropriate. Set *version_out to the cache file format version. */ static krb5_error_code read_header(krb5_context context, FILE *fp, int *version_out) { krb5_error_code ret; krb5_os_context os_ctx = &context->os_context; uint16_t fields_len, tag, flen; uint32_t time_offset, usec_offset; char i16buf[2]; int version; *version_out = 0; /* Get the file format version. */ ret = read_bytes(context, fp, i16buf, 2); if (ret) return KRB5_CC_FORMAT; version = load_16_be(i16buf) - FVNO_BASE; if (version < 1 || version > 4) return KRB5_CCACHE_BADVNO; *version_out = version; /* Tagged header fields begin with version 4. */ if (version < 4) return 0; if (read16(context, fp, version, &fields_len)) return KRB5_CC_FORMAT; while (fields_len) { if (fields_len < 4 || read16(context, fp, version, &tag) || read16(context, fp, version, &flen) || flen > fields_len - 4) return KRB5_CC_FORMAT; switch (tag) { case FCC_TAG_DELTATIME: if (flen != 8 || read32(context, fp, version, NULL, &time_offset) || read32(context, fp, version, NULL, &usec_offset)) return KRB5_CC_FORMAT; if (!(context->library_options & KRB5_LIBOPT_SYNC_KDCTIME) || (os_ctx->os_flags & KRB5_OS_TOFFSET_VALID)) break; os_ctx->time_offset = time_offset; os_ctx->usec_offset = usec_offset; os_ctx->os_flags = ((os_ctx->os_flags & ~KRB5_OS_TOFFSET_TIME) | KRB5_OS_TOFFSET_VALID); break; default: if (flen && fseek(fp, flen, SEEK_CUR) != 0) return KRB5_CC_FORMAT; break; } fields_len -= (4 + flen); } return 0; } static void marshal_header(krb5_context context, struct k5buf *buf, krb5_principal princ) { krb5_os_context os_ctx = &context->os_context; int version = context->fcc_default_format - FVNO_BASE; uint16_t fields_len; version = context->fcc_default_format - FVNO_BASE; k5_buf_add_uint16_be(buf, FVNO_BASE + version); if (version >= 4) { /* Add tagged header fields. */ fields_len = 0; if (os_ctx->os_flags & KRB5_OS_TOFFSET_VALID) fields_len += 12; k5_buf_add_uint16_be(buf, fields_len); if (os_ctx->os_flags & KRB5_OS_TOFFSET_VALID) { /* Add time offset tag. */ k5_buf_add_uint16_be(buf, FCC_TAG_DELTATIME); k5_buf_add_uint16_be(buf, 8); k5_buf_add_uint32_be(buf, os_ctx->time_offset); k5_buf_add_uint32_be(buf, os_ctx->usec_offset); } } k5_marshal_princ(buf, version, princ); } /* Create or overwrite the cache file with a header and default principal. */ static krb5_error_code KRB5_CALLCONV fcc_initialize(krb5_context context, krb5_ccache id, krb5_principal princ) { krb5_error_code ret; fcc_data *data = id->data; ssize_t nwritten; int st, flags, fd = -1; struct k5buf buf = EMPTY_K5BUF; krb5_boolean file_locked = FALSE; k5_cc_mutex_lock(context, &data->lock); unlink(data->filename); flags = O_CREAT | O_EXCL | O_RDWR | O_BINARY | O_CLOEXEC; fd = open(data->filename, flags, 0600); if (fd == -1) { ret = interpret_errno(context, errno); goto cleanup; } set_cloexec_fd(fd); #if defined(HAVE_FCHMOD) || defined(HAVE_CHMOD) #ifdef HAVE_FCHMOD st = fchmod(fd, S_IRUSR | S_IWUSR); #else st = chmod(data->filename, S_IRUSR | S_IWUSR); #endif if (st == -1) { ret = interpret_errno(context, errno); goto cleanup; } #endif ret = krb5_lock_file(context, fd, KRB5_LOCKMODE_EXCLUSIVE); if (ret) goto cleanup; file_locked = TRUE; /* Prepare the header and principal in buf. */ k5_buf_init_dynamic(&buf); marshal_header(context, &buf, princ); ret = k5_buf_status(&buf); if (ret) goto cleanup; /* Write the header and principal. */ nwritten = write(fd, buf.data, buf.len); if (nwritten == -1) ret = interpret_errno(context, errno); if ((size_t)nwritten != buf.len) ret = KRB5_CC_IO; cleanup: k5_buf_free(&buf); if (file_locked) krb5_unlock_file(context, fd); if (fd != -1) close(fd); k5_cc_mutex_unlock(context, &data->lock); krb5_change_cache(); return set_errmsg_filename(context, ret, data->filename); } /* Release an fcc_data object. */ static void free_fccdata(krb5_context context, fcc_data *data) { k5_cc_mutex_assert_unlocked(context, &data->lock); free(data->filename); k5_cc_mutex_destroy(&data->lock); free(data); } /* Release the ccache handle. */ static krb5_error_code KRB5_CALLCONV fcc_close(krb5_context context, krb5_ccache id) { free_fccdata(context, id->data); free(id); return 0; } /* Destroy the cache file and release the handle. */ static krb5_error_code KRB5_CALLCONV fcc_destroy(krb5_context context, krb5_ccache id) { krb5_error_code ret = 0; fcc_data *data = id->data; int st, fd; struct stat buf; unsigned long i, size; unsigned int wlen; char zeros[BUFSIZ]; k5_cc_mutex_lock(context, &data->lock); fd = open(data->filename, O_RDWR | O_BINARY | O_CLOEXEC, 0); if (fd < 0) { ret = interpret_errno(context, errno); goto cleanup; } set_cloexec_fd(fd); #ifdef MSDOS_FILESYSTEM /* * "Disgusting bit of UNIX trivia" - that's how the writers of NFS describe * the ability of UNIX to still write to a file which has been unlinked. * Naturally, the PC can't do this. As a result, we have to delete the * file after we wipe it clean, but that throws off all the error handling * code. So we have do the work ourselves. */ st = fstat(fd, &buf); if (st == -1) { ret = interpret_errno(context, errno); size = 0; /* Nothing to wipe clean */ } else { size = (unsigned long)buf.st_size; } memset(zeros, 0, BUFSIZ); while (size > 0) { wlen = (int)((size > BUFSIZ) ? BUFSIZ : size); /* How much to write */ i = write(fd, zeros, wlen); if (i < 0) { ret = interpret_errno(context, errno); /* Don't jump to cleanup--we still want to delete the file. */ break; } size -= i; } (void)close(fd); st = unlink(data->filename); if (st < 0) { ret = interpret_errno(context, errno); goto cleanup; } #else /* MSDOS_FILESYSTEM */ st = unlink(data->filename); if (st < 0) { ret = interpret_errno(context, errno); (void)close(fd); goto cleanup; } st = fstat(fd, &buf); if (st < 0) { ret = interpret_errno(context, errno); (void)close(fd); goto cleanup; } /* XXX This may not be legal XXX */ size = (unsigned long)buf.st_size; memset(zeros, 0, BUFSIZ); for (i = 0; i < size / BUFSIZ; i++) { if (write(fd, zeros, BUFSIZ) < 0) { ret = interpret_errno(context, errno); (void)close(fd); goto cleanup; } } wlen = size % BUFSIZ; if (write(fd, zeros, wlen) < 0) { ret = interpret_errno(context, errno); (void)close(fd); goto cleanup; } st = close(fd); if (st) ret = interpret_errno(context, errno); #endif /* MSDOS_FILESYSTEM */ cleanup: (void)set_errmsg_filename(context, ret, data->filename); k5_cc_mutex_unlock(context, &data->lock); free_fccdata(context, data); free(id); krb5_change_cache(); return ret; } extern const krb5_cc_ops krb5_fcc_ops; /* Create a file ccache handle for the pathname given by residual. */ static krb5_error_code KRB5_CALLCONV fcc_resolve(krb5_context context, krb5_ccache *id, const char *residual) { krb5_ccache lid; krb5_error_code ret; fcc_data *data; data = malloc(sizeof(fcc_data)); if (data == NULL) return KRB5_CC_NOMEM; data->filename = strdup(residual); if (data->filename == NULL) { free(data); return KRB5_CC_NOMEM; } ret = k5_cc_mutex_init(&data->lock); if (ret) { free(data->filename); free(data); return ret; } lid = malloc(sizeof(struct _krb5_ccache)); if (lid == NULL) { free_fccdata(context, data); return KRB5_CC_NOMEM; } lid->ops = &krb5_fcc_ops; lid->data = data; lid->magic = KV5M_CCACHE; /* Other routines will get errors on open, and callers must expect them, if * cache is non-existent/unusable. */ *id = lid; return 0; } /* Prepare for a sequential iteration over the cache file. */ static krb5_error_code KRB5_CALLCONV fcc_start_seq_get(krb5_context context, krb5_ccache id, krb5_cc_cursor *cursor) { krb5_fcc_cursor *fcursor = NULL; krb5_error_code ret; krb5_principal princ = NULL; fcc_data *data = id->data; FILE *fp = NULL; int version; k5_cc_mutex_lock(context, &data->lock); fcursor = malloc(sizeof(krb5_fcc_cursor)); if (fcursor == NULL) { ret = KRB5_CC_NOMEM; goto cleanup; } /* Open the cache file and read the header. */ ret = open_cache_file(context, data->filename, FALSE, &fp); if (ret) goto cleanup; ret = read_header(context, fp, &version); if (ret) goto cleanup; /* Read past the default client principal name. */ ret = read_principal(context, fp, version, &princ); if (ret) goto cleanup; /* Drop the shared file lock but retain the file handle. */ (void)krb5_unlock_file(context, fileno(fp)); fcursor->fp = fp; fp = NULL; fcursor->version = version; *cursor = (krb5_cc_cursor)fcursor; fcursor = NULL; cleanup: (void)close_cache_file(context, fp); free(fcursor); krb5_free_principal(context, princ); k5_cc_mutex_unlock(context, &data->lock); return set_errmsg_filename(context, ret, data->filename); } /* Return true if cred is a removed entry (assuming that no legitimate cred * entries will have authtime=-1 and endtime=0). */ static inline krb5_boolean cred_removed(krb5_creds *c) { return c->times.endtime == 0 && c->times.authtime == -1; } /* Get the next credential from the cache file. */ static krb5_error_code KRB5_CALLCONV fcc_next_cred(krb5_context context, krb5_ccache id, krb5_cc_cursor *cursor, krb5_creds *creds) { krb5_error_code ret; krb5_fcc_cursor *fcursor = *cursor; fcc_data *data = id->data; struct k5buf buf; size_t maxsize; krb5_boolean file_locked = FALSE; memset(creds, 0, sizeof(*creds)); k5_cc_mutex_lock(context, &data->lock); k5_buf_init_dynamic_zap(&buf); ret = krb5_lock_file(context, fileno(fcursor->fp), KRB5_LOCKMODE_SHARED); if (ret) goto cleanup; file_locked = TRUE; for (;;) { /* Load a marshalled cred into memory. */ ret = get_size(context, fcursor->fp, &maxsize); if (ret) goto cleanup; ret = load_cred(context, fcursor->fp, fcursor->version, maxsize, &buf); if (ret) goto cleanup; ret = k5_buf_status(&buf); if (ret) goto cleanup; /* Unmarshal it from buf into creds. */ ret = k5_unmarshal_cred(buf.data, buf.len, fcursor->version, creds); if (ret) goto cleanup; /* Keep going if this entry has been removed; otherwise stop. */ if (!cred_removed(creds)) break; k5_buf_truncate(&buf, 0); krb5_free_cred_contents(context, creds); } cleanup: if (file_locked) (void)krb5_unlock_file(context, fileno(fcursor->fp)); k5_cc_mutex_unlock(context, &data->lock); k5_buf_free(&buf); return set_errmsg_filename(context, ret, data->filename); } /* Release an iteration cursor. */ static krb5_error_code KRB5_CALLCONV fcc_end_seq_get(krb5_context context, krb5_ccache id, krb5_cc_cursor *cursor) { krb5_fcc_cursor *fcursor = *cursor; (void)fclose(fcursor->fp); free(fcursor); *cursor = NULL; return 0; } /* Generate a unique file ccache using the given template (which will be * modified to contain the actual name of the file). */ krb5_error_code krb5int_fcc_new_unique(krb5_context context, char *template, krb5_ccache *id) { krb5_ccache lid; int fd; krb5_error_code ret; fcc_data *data; char fcc_fvno[2]; int16_t fcc_flen = 0; int errsave, cnt; fd = mkstemp(template); if (fd == -1) return interpret_errno(context, errno); set_cloexec_fd(fd); /* Allocate memory */ data = malloc(sizeof(fcc_data)); if (data == NULL) { close(fd); unlink(template); return KRB5_CC_NOMEM; } data->filename = strdup(template); if (data->filename == NULL) { free(data); close(fd); unlink(template); return KRB5_CC_NOMEM; } ret = k5_cc_mutex_init(&data->lock); if (ret) { free(data->filename); free(data); close(fd); unlink(template); return ret; } k5_cc_mutex_lock(context, &data->lock); /* Ignore user's umask, set mode = 0600 */ #ifndef HAVE_FCHMOD #ifdef HAVE_CHMOD chmod(data->filename, S_IRUSR | S_IWUSR); #endif #else fchmod(fd, S_IRUSR | S_IWUSR); #endif store_16_be(context->fcc_default_format, fcc_fvno); cnt = write(fd, &fcc_fvno, 2); if (cnt != 2) { errsave = errno; (void)close(fd); (void)unlink(data->filename); ret = (cnt == -1) ? interpret_errno(context, errsave) : KRB5_CC_IO; goto err_out; } /* For version 4 we save a length for the rest of the header */ if (context->fcc_default_format == FVNO_BASE + 4) { cnt = write(fd, &fcc_flen, sizeof(fcc_flen)); if (cnt != sizeof(fcc_flen)) { errsave = errno; (void)close(fd); (void)unlink(data->filename); ret = (cnt == -1) ? interpret_errno(context, errsave) : KRB5_CC_IO; goto err_out; } } if (close(fd) == -1) { errsave = errno; (void)unlink(data->filename); ret = interpret_errno(context, errsave); goto err_out; } k5_cc_mutex_assert_locked(context, &data->lock); k5_cc_mutex_unlock(context, &data->lock); lid = malloc(sizeof(*lid)); if (lid == NULL) { free_fccdata(context, data); return KRB5_CC_NOMEM; } lid->ops = &krb5_fcc_ops; lid->data = data; lid->magic = KV5M_CCACHE; *id = lid; krb5_change_cache(); return 0; err_out: (void)set_errmsg_filename(context, ret, data->filename); k5_cc_mutex_unlock(context, &data->lock); k5_cc_mutex_destroy(&data->lock); free(data->filename); free(data); return ret; } /* * Create a new file cred cache whose name is guaranteed to be unique. The * name begins with the string TKT_ROOT (from fcc.h). The cache file is not * opened, but the new filename is reserved. */ static krb5_error_code KRB5_CALLCONV fcc_generate_new(krb5_context context, krb5_ccache *id) { char scratch[sizeof(TKT_ROOT) + 7]; /* Room for XXXXXX and terminator */ (void)snprintf(scratch, sizeof(scratch), "%sXXXXXX", TKT_ROOT); return krb5int_fcc_new_unique(context, scratch, id); } /* Return an alias to the pathname of the cache file. */ static const char * KRB5_CALLCONV fcc_get_name(krb5_context context, krb5_ccache id) { return ((fcc_data *)id->data)->filename; } /* Retrieve a copy of the default principal, if the cache is initialized. */ static krb5_error_code KRB5_CALLCONV fcc_get_principal(krb5_context context, krb5_ccache id, krb5_principal *princ) { krb5_error_code ret; fcc_data *data = id->data; FILE *fp = NULL; int version; k5_cc_mutex_lock(context, &data->lock); ret = open_cache_file(context, data->filename, FALSE, &fp); if (ret) goto cleanup; ret = read_header(context, fp, &version); if (ret) goto cleanup; ret = read_principal(context, fp, version, princ); cleanup: (void)close_cache_file(context, fp); k5_cc_mutex_unlock(context, &data->lock); return set_errmsg_filename(context, ret, data->filename); } /* Search for a credential within the cache file. */ static krb5_error_code KRB5_CALLCONV fcc_retrieve(krb5_context context, krb5_ccache id, krb5_flags whichfields, krb5_creds *mcreds, krb5_creds *creds) { krb5_error_code ret; ret = k5_cc_retrieve_cred_default(context, id, whichfields, mcreds, creds); return set_errmsg_filename(context, ret, ((fcc_data *)id->data)->filename); } /* Store a credential in the cache file. */ static krb5_error_code KRB5_CALLCONV fcc_store(krb5_context context, krb5_ccache id, krb5_creds *creds) { krb5_error_code ret, ret2; fcc_data *data = id->data; FILE *fp = NULL; int version; struct k5buf buf = EMPTY_K5BUF; ssize_t nwritten; k5_cc_mutex_lock(context, &data->lock); /* Open the cache file for O_APPEND writing. */ ret = open_cache_file(context, data->filename, TRUE, &fp); if (ret) goto cleanup; ret = read_header(context, fp, &version); if (ret) goto cleanup; /* Marshal the cred and write it to the file with a single append write. */ k5_buf_init_dynamic_zap(&buf); k5_marshal_cred(&buf, version, creds); ret = k5_buf_status(&buf); if (ret) goto cleanup; nwritten = write(fileno(fp), buf.data, buf.len); if (nwritten == -1) ret = interpret_errno(context, errno); if ((size_t)nwritten != buf.len) ret = KRB5_CC_IO; krb5_change_cache(); cleanup: k5_buf_free(&buf); ret2 = close_cache_file(context, fp); k5_cc_mutex_unlock(context, &data->lock); return set_errmsg_filename(context, ret ? ret : ret2, data->filename); } /* * Overwrite cred in the ccache file with an entry that should not match any * reasonable search. Deletion is not guaranteed. This method is originally * from Heimdal, with the addition of setting authtime to -1. */ static krb5_error_code delete_cred(krb5_context context, krb5_ccache cache, krb5_cc_cursor *cursor, krb5_creds *cred) { krb5_error_code ret; krb5_fcc_cursor *fcursor = *cursor; fcc_data *data = cache->data; struct k5buf expected = EMPTY_K5BUF, overwrite = EMPTY_K5BUF; int fd = -1; uint8_t *on_disk = NULL; ssize_t rwret; off_t start_offset; k5_buf_init_dynamic_zap(&expected); k5_buf_init_dynamic_zap(&overwrite); /* Re-marshal cred to get its byte representation in the file. */ k5_marshal_cred(&expected, fcursor->version, cred); ret = k5_buf_status(&expected); if (ret) goto cleanup; /* * Mark the cred expired so that it will be skipped over by any future * match checks. Heimdal only sets endtime, but we also set authtime to * distinguish from gssproxy's creds. */ cred->times.endtime = 0; cred->times.authtime = -1; /* For config entries, also change the realm so that other implementations * won't match them. */ if (data_eq_string(cred->server->realm, "X-CACHECONF:")) memcpy(cred->server->realm.data, "X-RMED-CONF:", 12); k5_marshal_cred(&overwrite, fcursor->version, cred); ret = k5_buf_status(&overwrite); if (ret) goto cleanup; if (expected.len != overwrite.len) { ret = KRB5_CC_FORMAT; goto cleanup; } /* Get a non-O_APPEND handle to the raw file. */ fd = open(data->filename, O_RDWR | O_BINARY | O_CLOEXEC); if (fd == -1) { ret = interpret_errno(context, errno); goto cleanup; } start_offset = ftell(fcursor->fp); if (start_offset == -1) { ret = interpret_errno(context, errno); goto cleanup; } start_offset -= expected.len; /* Read the bytes at the entry to be overwritten. */ if (lseek(fd, start_offset, SEEK_SET) == -1) { ret = interpret_errno(context, errno); goto cleanup; } on_disk = k5alloc(expected.len, &ret); if (ret != 0) goto cleanup; rwret = read(fd, on_disk, expected.len); if (rwret < 0) { ret = interpret_errno(context, errno); goto cleanup; } else if ((size_t)rwret != expected.len) { ret = KRB5_CC_FORMAT; goto cleanup; } /* * If the bytes have changed, either someone else removed the same cred or * the cache was reinitialized. Either way the cred is no longer present, * so return successfully. */ if (memcmp(on_disk, expected.data, expected.len) != 0) goto cleanup; /* Write out the altered entry. */ if (lseek(fd, start_offset, SEEK_SET) == -1) { ret = interpret_errno(context, errno); goto cleanup; } rwret = write(fd, overwrite.data, overwrite.len); if (rwret < 0) { ret = interpret_errno(context, errno); goto cleanup; } cleanup: if (fd >= 0) close(fd); zapfree(on_disk, expected.len); k5_buf_free(&expected); k5_buf_free(&overwrite); return ret; } /* Remove the given creds from the ccache file. */ static krb5_error_code KRB5_CALLCONV fcc_remove_cred(krb5_context context, krb5_ccache cache, krb5_flags flags, krb5_creds *creds) { krb5_error_code ret; krb5_cc_cursor cursor; krb5_creds cur; ret = krb5_cc_start_seq_get(context, cache, &cursor); if (ret) return ret; for (;;) { ret = krb5_cc_next_cred(context, cache, &cursor, &cur); if (ret) break; if (krb5int_cc_creds_match_request(context, flags, creds, &cur)) ret = delete_cred(context, cache, &cursor, &cur); krb5_free_cred_contents(context, &cur); if (ret) break; } krb5_cc_end_seq_get(context, cache, &cursor); return (ret == KRB5_CC_END) ? 0 : ret; } static krb5_error_code KRB5_CALLCONV fcc_set_flags(krb5_context context, krb5_ccache id, krb5_flags flags) { return 0; } static krb5_error_code KRB5_CALLCONV fcc_get_flags(krb5_context context, krb5_ccache id, krb5_flags *flags) { *flags = 0; return 0; } /* Prepare to iterate over the caches in the per-type collection. */ static krb5_error_code KRB5_CALLCONV fcc_ptcursor_new(krb5_context context, krb5_cc_ptcursor *cursor) { krb5_cc_ptcursor n = NULL; struct krb5_fcc_ptcursor_data *cdata = NULL; *cursor = NULL; n = malloc(sizeof(*n)); if (n == NULL) return ENOMEM; n->ops = &krb5_fcc_ops; cdata = malloc(sizeof(*cdata)); if (cdata == NULL) { free(n); return ENOMEM; } cdata->first = TRUE; n->data = cdata; *cursor = n; return 0; } /* Get the next cache in the per-type collection. The FILE per-type collection * contains only the context's default cache if it is a file cache. */ static krb5_error_code KRB5_CALLCONV fcc_ptcursor_next(krb5_context context, krb5_cc_ptcursor cursor, krb5_ccache *cache_out) { krb5_error_code ret; struct krb5_fcc_ptcursor_data *cdata = cursor->data; const char *defname, *residual; krb5_ccache cache; struct stat sb; *cache_out = NULL; if (!cdata->first) return 0; cdata->first = FALSE; defname = krb5_cc_default_name(context); if (!defname) return 0; /* Check if the default has type FILE or no type; find the residual. */ if (strncmp(defname, "FILE:", 5) == 0) residual = defname + 5; else if (strchr(defname + 2, ':') == NULL) /* Skip drive prefix if any. */ residual = defname; else return 0; /* Don't yield a nonexistent default file cache. */ if (stat(residual, &sb) != 0) return 0; ret = krb5_cc_resolve(context, defname, &cache); if (ret) return set_errmsg_filename(context, ret, defname); *cache_out = cache; return 0; } /* Release a per-type collection iteration cursor. */ static krb5_error_code KRB5_CALLCONV fcc_ptcursor_free(krb5_context context, krb5_cc_ptcursor *cursor) { if (*cursor == NULL) return 0; free((*cursor)->data); free(*cursor); *cursor = NULL; return 0; } /* Lock the cache handle against other threads. (This does not lock the cache * file against other processes.) */ static krb5_error_code KRB5_CALLCONV fcc_lock(krb5_context context, krb5_ccache id) { fcc_data *data = id->data; k5_cc_mutex_lock(context, &data->lock); return 0; } /* Unlock the cache handle. */ static krb5_error_code KRB5_CALLCONV fcc_unlock(krb5_context context, krb5_ccache id) { fcc_data *data = id->data; k5_cc_mutex_unlock(context, &data->lock); return 0; } static krb5_error_code KRB5_CALLCONV fcc_replace(krb5_context context, krb5_ccache id, krb5_principal princ, krb5_creds **creds) { krb5_error_code ret; fcc_data *data = id->data; char *tmpname = NULL; int i, st, fd = -1, version = context->fcc_default_format - FVNO_BASE; ssize_t nwritten; struct k5buf buf = EMPTY_K5BUF; krb5_boolean tmpfile_exists = FALSE; if (asprintf(&tmpname, "%s.XXXXXX", data->filename) < 0) return ENOMEM; fd = mkstemp(tmpname); if (fd < 0) goto errno_cleanup; tmpfile_exists = TRUE; k5_buf_init_dynamic_zap(&buf); marshal_header(context, &buf, princ); for (i = 0; creds[i] != NULL; i++) k5_marshal_cred(&buf, version, creds[i]); ret = k5_buf_status(&buf); if (ret) goto cleanup; nwritten = write(fd, buf.data, buf.len); if (nwritten == -1) goto errno_cleanup; if ((size_t)nwritten != buf.len) { ret = KRB5_CC_IO; goto cleanup; } st = close(fd); fd = -1; if (st != 0) goto errno_cleanup; st = rename(tmpname, data->filename); if (st != 0) goto errno_cleanup; tmpfile_exists = FALSE; cleanup: k5_buf_free(&buf); if (fd != -1) close(fd); if (tmpfile_exists) unlink(tmpname); free(tmpname); return ret; errno_cleanup: ret = interpret_errno(context, errno); goto cleanup; } /* Translate a system errno value to a Kerberos com_err code. */ static krb5_error_code interpret_errno(krb5_context context, int errnum) { krb5_error_code ret; switch (errnum) { case ENOENT: case ENOTDIR: #ifdef ELOOP case ELOOP: #endif #ifdef ENAMETOOLONG case ENAMETOOLONG: #endif ret = KRB5_FCC_NOFILE; break; case EPERM: case EACCES: #ifdef EISDIR case EISDIR: /* Mac doesn't have EISDIR */ #endif case EROFS: ret = KRB5_FCC_PERM; break; case EINVAL: case EEXIST: case EFAULT: case EBADF: #ifdef EWOULDBLOCK case EWOULDBLOCK: #endif ret = KRB5_FCC_INTERNAL; break; /* * The rest all map to KRB5_CC_IO. These errnos are listed to * document that they've been considered explicitly: * * - EDQUOT * - ENOSPC * - EIO * - ENFILE * - EMFILE * - ENXIO * - EBUSY * - ETXTBSY */ default: ret = KRB5_CC_IO; break; } return ret; } const krb5_cc_ops krb5_fcc_ops = { 0, "FILE", fcc_get_name, fcc_resolve, fcc_generate_new, fcc_initialize, fcc_destroy, fcc_close, fcc_store, fcc_retrieve, fcc_get_principal, fcc_start_seq_get, fcc_next_cred, fcc_end_seq_get, fcc_remove_cred, fcc_set_flags, fcc_get_flags, fcc_ptcursor_new, fcc_ptcursor_next, fcc_ptcursor_free, fcc_replace, NULL, /* wasdefault */ fcc_lock, fcc_unlock, NULL, /* switch_to */ }; #if defined(_WIN32) /* * krb5_change_cache should be called after the cache changes. * A notification message is is posted out to all top level * windows so that they may recheck the cache based on the * changes made. We register a unique message type with which * we'll communicate to all other processes. */ krb5_error_code krb5_change_cache(void) { PostMessage(HWND_BROADCAST, krb5_get_notification_message(), 0, 0); return 0; } unsigned int KRB5_CALLCONV krb5_get_notification_message(void) { static unsigned int message = 0; if (message == 0) message = RegisterWindowMessage(WM_KERBEROS5_CHANGED); return message; } #else /* _WIN32 */ krb5_error_code krb5_change_cache(void) { return 0; } unsigned int krb5_get_notification_message(void) { return 0; } #endif /* _WIN32 */ const krb5_cc_ops krb5_cc_file_ops = { 0, "FILE", fcc_get_name, fcc_resolve, fcc_generate_new, fcc_initialize, fcc_destroy, fcc_close, fcc_store, fcc_retrieve, fcc_get_principal, fcc_start_seq_get, fcc_next_cred, fcc_end_seq_get, fcc_remove_cred, fcc_set_flags, fcc_get_flags, fcc_ptcursor_new, fcc_ptcursor_next, fcc_ptcursor_free, fcc_replace, NULL, /* wasdefault */ fcc_lock, fcc_unlock, NULL, /* switch_to */ };