Changeset 370 for branches/samba-3.3.x/source/client
- Timestamp:
- Jan 15, 2010, 8:23:30 AM (16 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
branches/samba-3.3.x/source/client/cifs.upcall.c
r206 r370 2 2 * CIFS user-space helper. 3 3 * Copyright (C) Igor Mammedov (niallain@gmail.com) 2007 4 * Copyright (C) Jeff Layton (jlayton@redhat.com) 2009 4 5 * 5 6 * Used by /sbin/request-key for handling … … 26 27 27 28 #include "includes.h" 29 #include "smb_krb5.h" 28 30 #include <keyutils.h> 31 #include <getopt.h> 29 32 30 33 #include "cifs_spnego.h" 31 34 32 const char *CIFSSPNEGO_VERSION = "1.2"; 35 #define CIFS_DEFAULT_KRB5_DIR "/tmp" 36 #define CIFS_DEFAULT_KRB5_PREFIX "krb5cc_" 37 38 #define MAX_CCNAME_LEN PATH_MAX + 5 39 40 const char *CIFSSPNEGO_VERSION = "1.3"; 33 41 static const char *prog = "cifs.upcall"; 34 typedef enum _sec Type {42 typedef enum _sectype { 35 43 NONE = 0, 36 44 KRB5, 37 45 MS_KRB5 38 } secType_t; 46 } sectype_t; 47 48 /* does the ccache have a valid TGT? */ 49 static time_t 50 get_tgt_time(const char *ccname) { 51 krb5_context context; 52 krb5_ccache ccache; 53 krb5_cc_cursor cur; 54 krb5_creds creds; 55 krb5_principal principal; 56 time_t credtime = 0; 57 char *realm = NULL; 58 59 if (krb5_init_context(&context)) { 60 syslog(LOG_DEBUG, "%s: unable to init krb5 context", __func__); 61 return 0; 62 } 63 64 if (krb5_cc_resolve(context, ccname, &ccache)) { 65 syslog(LOG_DEBUG, "%s: unable to resolve krb5 cache", __func__); 66 goto err_cache; 67 } 68 69 if (krb5_cc_set_flags(context, ccache, 0)) { 70 syslog(LOG_DEBUG, "%s: unable to set flags", __func__); 71 goto err_cache; 72 } 73 74 if (krb5_cc_get_principal(context, ccache, &principal)) { 75 syslog(LOG_DEBUG, "%s: unable to get principal", __func__); 76 goto err_princ; 77 } 78 79 if (krb5_cc_start_seq_get(context, ccache, &cur)) { 80 syslog(LOG_DEBUG, "%s: unable to seq start", __func__); 81 goto err_ccstart; 82 } 83 84 if ((realm = smb_krb5_principal_get_realm(context, principal)) == NULL) { 85 syslog(LOG_DEBUG, "%s: unable to get realm", __func__); 86 goto err_ccstart; 87 } 88 89 while (!credtime && !krb5_cc_next_cred(context, ccache, &cur, &creds)) { 90 char *name; 91 if (smb_krb5_unparse_name(context, creds.server, &name)) { 92 syslog(LOG_DEBUG, "%s: unable to unparse name", __func__); 93 goto err_endseq; 94 } 95 if (krb5_realm_compare(context, creds.server, principal) && 96 strnequal(name, KRB5_TGS_NAME, KRB5_TGS_NAME_SIZE) && 97 strnequal(name+KRB5_TGS_NAME_SIZE+1, realm, strlen(realm)) && 98 creds.times.endtime > time(NULL)) 99 credtime = creds.times.endtime; 100 krb5_free_cred_contents(context, &creds); 101 SAFE_FREE(name); 102 } 103 err_endseq: 104 krb5_cc_end_seq_get(context, ccache, &cur); 105 err_ccstart: 106 krb5_free_principal(context, principal); 107 err_princ: 108 #if defined(KRB5_TC_OPENCLOSE) 109 krb5_cc_set_flags(context, ccache, KRB5_TC_OPENCLOSE); 110 #endif 111 krb5_cc_close(context, ccache); 112 err_cache: 113 krb5_free_context(context); 114 return credtime; 115 } 116 117 static int 118 krb5cc_filter(const struct dirent *dirent) 119 { 120 if (strstr(dirent->d_name, CIFS_DEFAULT_KRB5_PREFIX)) 121 return 1; 122 else 123 return 0; 124 } 125 126 /* search for a credcache that looks like a likely candidate */ 127 static char * 128 find_krb5_cc(const char *dirname, uid_t uid) 129 { 130 struct dirent **namelist; 131 struct stat sbuf; 132 char ccname[MAX_CCNAME_LEN], *credpath, *best_cache = NULL; 133 int i, n; 134 time_t cred_time, best_time = 0; 135 136 n = scandir(dirname, &namelist, krb5cc_filter, NULL); 137 if (n < 0) { 138 syslog(LOG_DEBUG, "%s: scandir error on directory '%s': %s", 139 __func__, dirname, strerror(errno)); 140 return NULL; 141 } 142 143 for (i = 0; i < n; i++) { 144 snprintf(ccname, sizeof(ccname), "FILE:%s/%s", dirname, 145 namelist[i]->d_name); 146 credpath = ccname + 5; 147 syslog(LOG_DEBUG, "%s: considering %s", __func__, credpath); 148 149 if (lstat(credpath, &sbuf)) { 150 syslog(LOG_DEBUG, "%s: stat error on '%s': %s", 151 __func__, credpath, strerror(errno)); 152 free(namelist[i]); 153 continue; 154 } 155 if (sbuf.st_uid != uid) { 156 syslog(LOG_DEBUG, "%s: %s is owned by %u, not %u", 157 __func__, credpath, sbuf.st_uid, uid); 158 free(namelist[i]); 159 continue; 160 } 161 if (!S_ISREG(sbuf.st_mode)) { 162 syslog(LOG_DEBUG, "%s: %s is not a regular file", 163 __func__, credpath); 164 free(namelist[i]); 165 continue; 166 } 167 if (!(cred_time = get_tgt_time(ccname))) { 168 syslog(LOG_DEBUG, "%s: %s is not a valid credcache.", 169 __func__, ccname); 170 free(namelist[i]); 171 continue; 172 } 173 174 if (cred_time <= best_time) { 175 syslog(LOG_DEBUG, "%s: %s expires sooner than current " 176 "best.", __func__, ccname); 177 free(namelist[i]); 178 continue; 179 } 180 181 syslog(LOG_DEBUG, "%s: %s is valid ccache", __func__, ccname); 182 free(best_cache); 183 best_cache = SMB_STRNDUP(ccname, MAX_CCNAME_LEN); 184 best_time = cred_time; 185 free(namelist[i]); 186 } 187 free(namelist); 188 189 return best_cache; 190 } 39 191 40 192 /* … … 57 209 * 58 210 * ret: 0 - success, others - failure 59 */211 */ 60 212 static int 61 handle_krb5_mech(const char *oid, const char *principal, 62 DATA_BLOB * secblob, DATA_BLOB * sess_key)213 handle_krb5_mech(const char *oid, const char *principal, DATA_BLOB *secblob, 214 DATA_BLOB *sess_key, const char *ccname) 63 215 { 64 216 int retval; 65 217 DATA_BLOB tkt, tkt_wrapped; 66 218 219 syslog(LOG_DEBUG, "%s: getting service ticket for %s", __func__, 220 principal); 221 67 222 /* get a kerberos ticket for the service and extract the session key */ 68 retval = cli_krb5_get_ticket(principal, 0, 69 &tkt, sess_key, 0, NULL, NULL); 70 71 if (retval) 223 retval = cli_krb5_get_ticket(principal, 0, &tkt, sess_key, 0, ccname, 224 NULL); 225 226 if (retval) { 227 syslog(LOG_DEBUG, "%s: failed to obtain service ticket (%d)", 228 __func__, retval); 72 229 return retval; 230 } 231 232 syslog(LOG_DEBUG, "%s: obtained service ticket", __func__); 73 233 74 234 /* wrap that up in a nice GSS-API wrapping */ … … 83 243 } 84 244 85 #define DKD_HAVE_HOSTNAME 186 #define DKD_HAVE_VERSION 287 #define DKD_HAVE_SEC 488 #define DKD_HAVE_IP V4889 #define DKD_HAVE_ IPV6 1690 #define DKD_HAVE_ UID 32245 #define DKD_HAVE_HOSTNAME 0x1 246 #define DKD_HAVE_VERSION 0x2 247 #define DKD_HAVE_SEC 0x4 248 #define DKD_HAVE_IP 0x8 249 #define DKD_HAVE_UID 0x10 250 #define DKD_HAVE_PID 0x20 91 251 #define DKD_MUSTHAVE_SET (DKD_HAVE_HOSTNAME|DKD_HAVE_VERSION|DKD_HAVE_SEC) 92 252 93 static int 94 decode_key_description(const char *desc, int *ver, secType_t * sec, 95 char **hostname, uid_t * uid) 96 { 253 struct decoded_args { 254 int ver; 255 char *hostname; 256 char *ip; 257 uid_t uid; 258 pid_t pid; 259 sectype_t sec; 260 }; 261 262 static unsigned int 263 decode_key_description(const char *desc, struct decoded_args *arg) 264 { 265 int len; 97 266 int retval = 0; 98 267 char *pos; … … 102 271 pos = index(tkn, ';'); 103 272 if (strncmp(tkn, "host=", 5) == 0) { 104 int len; 105 106 if (pos == NULL) { 273 274 if (pos == NULL) 107 275 len = strlen(tkn); 276 else 277 len = pos - tkn; 278 279 len -= 4; 280 SAFE_FREE(arg->hostname); 281 arg->hostname = SMB_XMALLOC_ARRAY(char, len); 282 strlcpy(arg->hostname, tkn + 5, len); 283 retval |= DKD_HAVE_HOSTNAME; 284 } else if (!strncmp(tkn, "ip4=", 4) || 285 !strncmp(tkn, "ip6=", 4)) { 286 if (pos == NULL) 287 len = strlen(tkn); 288 else 289 len = pos - tkn; 290 291 len -= 3; 292 SAFE_FREE(arg->ip); 293 arg->ip = SMB_XMALLOC_ARRAY(char, len); 294 strlcpy(arg->ip, tkn + 4, len); 295 retval |= DKD_HAVE_IP; 296 } else if (strncmp(tkn, "pid=", 4) == 0) { 297 errno = 0; 298 arg->pid = strtol(tkn + 4, NULL, 0); 299 if (errno != 0) { 300 syslog(LOG_ERR, "Invalid pid format: %s", 301 strerror(errno)); 302 return 1; 108 303 } else { 109 len = pos - tkn;304 retval |= DKD_HAVE_PID; 110 305 } 111 len -= 4;112 SAFE_FREE(*hostname);113 *hostname = SMB_XMALLOC_ARRAY(char, len);114 strlcpy(*hostname, tkn + 5, len);115 retval |= DKD_HAVE_HOSTNAME;116 } else if (strncmp(tkn, "ipv4=", 5) == 0) {117 /* BB: do we need it if we have hostname already? */118 } else if (strncmp(tkn, "ipv6=", 5) == 0) {119 /* BB: do we need it if we have hostname already? */120 306 } else if (strncmp(tkn, "sec=", 4) == 0) { 121 307 if (strncmp(tkn + 4, "krb5", 4) == 0) { 122 308 retval |= DKD_HAVE_SEC; 123 *sec = KRB5;309 arg->sec = KRB5; 124 310 } else if (strncmp(tkn + 4, "mskrb5", 6) == 0) { 125 311 retval |= DKD_HAVE_SEC; 126 *sec = MS_KRB5;312 arg->sec = MS_KRB5; 127 313 } 128 314 } else if (strncmp(tkn, "uid=", 4) == 0) { 129 315 errno = 0; 130 *uid = strtol(tkn + 4, NULL, 16);316 arg->uid = strtol(tkn + 4, NULL, 16); 131 317 if (errno != 0) { 132 syslog(LOG_ WARNING, "Invalid uid format: %s",318 syslog(LOG_ERR, "Invalid uid format: %s", 133 319 strerror(errno)); 134 320 return 1; … … 138 324 } else if (strncmp(tkn, "ver=", 4) == 0) { /* if version */ 139 325 errno = 0; 140 *ver = strtol(tkn + 4, NULL, 16);326 arg->ver = strtol(tkn + 4, NULL, 16); 141 327 if (errno != 0) { 142 syslog(LOG_WARNING, 143 "Invalid version format: %s", 328 syslog(LOG_ERR, "Invalid version format: %s", 144 329 strerror(errno)); 145 330 return 1; … … 167 352 keyend = index(keyend+1, ';'); 168 353 if (!keyend) { 169 syslog(LOG_ WARNING, "invalid key description: %s",354 syslog(LOG_ERR, "invalid key description: %s", 170 355 key_descr); 171 356 return 1; … … 177 362 c = getaddrinfo(keyend, NULL, NULL, &addr); 178 363 if (c) { 179 syslog(LOG_ WARNING, "unable to resolve hostname: %s [%s]",364 syslog(LOG_ERR, "unable to resolve hostname: %s [%s]", 180 365 keyend, gai_strerror(c)); 181 366 return 1; … … 183 368 184 369 /* conver ip to string form */ 185 if (addr->ai_family == AF_INET) {370 if (addr->ai_family == AF_INET) 186 371 p = &(((struct sockaddr_in *)addr->ai_addr)->sin_addr); 187 } else {372 else 188 373 p = &(((struct sockaddr_in6 *)addr->ai_addr)->sin6_addr); 189 } 374 190 375 if (!inet_ntop(addr->ai_family, p, ip, sizeof(ip))) { 191 syslog(LOG_WARNING, "%s: inet_ntop: %s", 192 __FUNCTION__, strerror(errno)); 376 syslog(LOG_ERR, "%s: inet_ntop: %s", __func__, strerror(errno)); 193 377 freeaddrinfo(addr); 194 378 return 1; … … 198 382 c = keyctl_instantiate(key, ip, strlen(ip)+1, 0); 199 383 if (c == -1) { 200 syslog(LOG_ WARNING, "%s: keyctl_instantiate: %s",201 __FUNCTION__,strerror(errno));384 syslog(LOG_ERR, "%s: keyctl_instantiate: %s", __func__, 385 strerror(errno)); 202 386 freeaddrinfo(addr); 203 387 return 1; … … 208 392 } 209 393 394 /* 395 * Older kernels sent IPv6 addresses without colons. Well, at least 396 * they're fixed-length strings. Convert these addresses to have colon 397 * delimiters to make getaddrinfo happy. 398 */ 399 static void 400 convert_inet6_addr(const char *from, char *to) 401 { 402 int i = 1; 403 404 while (*from) { 405 *to++ = *from++; 406 if (!(i++ % 4) && *from) 407 *to++ = ':'; 408 } 409 *to = 0; 410 } 411 412 static int 413 ip_to_fqdn(const char *addrstr, char *host, size_t hostlen) 414 { 415 int rc; 416 struct addrinfo hints = { .ai_flags = AI_NUMERICHOST }; 417 struct addrinfo *res; 418 const char *ipaddr = addrstr; 419 char converted[INET6_ADDRSTRLEN + 1]; 420 421 if ((strlen(ipaddr) > INET_ADDRSTRLEN) && !strchr(ipaddr, ':')) { 422 convert_inet6_addr(ipaddr, converted); 423 ipaddr = converted; 424 } 425 426 rc = getaddrinfo(ipaddr, NULL, &hints, &res); 427 if (rc) { 428 syslog(LOG_DEBUG, "%s: failed to resolve %s to " 429 "ipaddr: %s", __func__, ipaddr, 430 rc == EAI_SYSTEM ? strerror(errno) : gai_strerror(rc)); 431 return rc; 432 } 433 434 rc = getnameinfo(res->ai_addr, res->ai_addrlen, host, hostlen, 435 NULL, 0, NI_NAMEREQD); 436 freeaddrinfo(res); 437 if (rc) { 438 syslog(LOG_DEBUG, "%s: failed to resolve %s to fqdn: %s", 439 __func__, ipaddr, 440 rc == EAI_SYSTEM ? strerror(errno) : gai_strerror(rc)); 441 return rc; 442 } 443 444 syslog(LOG_DEBUG, "%s: resolved %s to %s", __func__, ipaddr, host); 445 return 0; 446 } 447 210 448 static void 211 449 usage(void) 212 450 { 213 syslog(LOG_WARNING, "Usage: %s [-c] [-v] key_serial", prog); 214 fprintf(stderr, "Usage: %s [-c] [-v] key_serial\n", prog); 215 } 451 syslog(LOG_INFO, "Usage: %s [-t] [-v] key_serial", prog); 452 fprintf(stderr, "Usage: %s [-t] [-v] key_serial\n", prog); 453 } 454 455 const struct option long_options[] = { 456 { "trust-dns", 0, NULL, 't' }, 457 { "version", 0, NULL, 'v' }, 458 { NULL, 0, NULL, 0 } 459 }; 216 460 217 461 int main(const int argc, char *const argv[]) … … 220 464 DATA_BLOB secblob = data_blob_null; 221 465 DATA_BLOB sess_key = data_blob_null; 222 secType_t sectype = NONE;223 466 key_serial_t key = 0; 224 467 size_t datalen; 468 unsigned int have; 225 469 long rc = 1; 226 uid_t uid= 0;227 int kernel_upcall_version = 0;228 int c, use_cifs_service_prefix = 0;229 char *buf, *hostname = NULL;470 int c, try_dns = 0; 471 char *buf, *princ = NULL, *ccname = NULL; 472 char hostbuf[NI_MAXHOST], *host; 473 struct decoded_args arg = { }; 230 474 const char *oid; 231 475 476 hostbuf[0] = '\0'; 477 232 478 openlog(prog, 0, LOG_DAEMON); 233 479 234 while ((c = getopt (argc, argv, "cv")) != -1) {480 while ((c = getopt_long(argc, argv, "ctv", long_options, NULL)) != -1) { 235 481 switch (c) { 236 case 'c':{ 237 use_cifs_service_prefix = 1; 238 break; 239 } 240 case 'v':{ 482 case 'c': 483 /* legacy option -- skip it */ 484 break; 485 case 't': 486 try_dns++; 487 break; 488 case 'v': 241 489 printf("version: %s\n", CIFSSPNEGO_VERSION); 242 490 goto out; 243 } 244 default:{ 245 syslog(LOG_WARNING, "unknown option: %c", c); 491 default: 492 syslog(LOG_ERR, "unknown option: %c", c); 246 493 goto out; 247 }248 494 } 249 495 } … … 260 506 if (errno != 0) { 261 507 key = 0; 262 syslog(LOG_ WARNING, "Invalid key format: %s", strerror(errno));508 syslog(LOG_ERR, "Invalid key format: %s", strerror(errno)); 263 509 goto out; 264 510 } … … 266 512 rc = keyctl_describe_alloc(key, &buf); 267 513 if (rc == -1) { 268 syslog(LOG_ WARNING, "keyctl_describe_alloc failed: %s",514 syslog(LOG_ERR, "keyctl_describe_alloc failed: %s", 269 515 strerror(errno)); 270 516 rc = 1; 271 517 goto out; 272 518 } 519 520 syslog(LOG_DEBUG, "key description: %s", buf); 273 521 274 522 if ((strncmp(buf, "cifs.resolver", sizeof("cifs.resolver")-1) == 0) || … … 278 526 } 279 527 280 rc = decode_key_description(buf, &kernel_upcall_version, §ype,281 &hostname, &uid);282 if (( rc& DKD_MUSTHAVE_SET) != DKD_MUSTHAVE_SET) {283 syslog(LOG_ WARNING,284 "unable to get from description necessary params");528 have = decode_key_description(buf, &arg); 529 SAFE_FREE(buf); 530 if ((have & DKD_MUSTHAVE_SET) != DKD_MUSTHAVE_SET) { 531 syslog(LOG_ERR, "unable to get necessary params from key " 532 "description (0x%x)", have); 285 533 rc = 1; 286 SAFE_FREE(buf); 287 goto out; 288 } 289 SAFE_FREE(buf); 290 291 if (kernel_upcall_version > CIFS_SPNEGO_UPCALL_VERSION) { 292 syslog(LOG_WARNING, 293 "incompatible kernel upcall version: 0x%x", 294 kernel_upcall_version); 534 goto out; 535 } 536 537 if (arg.ver > CIFS_SPNEGO_UPCALL_VERSION) { 538 syslog(LOG_ERR, "incompatible kernel upcall version: 0x%x", 539 arg.ver); 295 540 rc = 1; 296 541 goto out; 297 542 } 298 543 299 if ( rc& DKD_HAVE_UID) {300 rc = setuid( uid);544 if (have & DKD_HAVE_UID) { 545 rc = setuid(arg.uid); 301 546 if (rc == -1) { 302 syslog(LOG_ WARNING, "setuid: %s", strerror(errno));547 syslog(LOG_ERR, "setuid: %s", strerror(errno)); 303 548 goto out; 304 549 } 305 } 306 307 /* BB: someday upcall SPNEGO blob could be checked here to decide 308 * what mech to use */ 550 551 ccname = find_krb5_cc(CIFS_DEFAULT_KRB5_DIR, arg.uid); 552 } 553 554 host = arg.hostname; 309 555 310 556 // do mech specific authorization 311 switch ( sectype) {557 switch (arg.sec) { 312 558 case MS_KRB5: 313 case KRB5:{ 314 char *princ; 315 size_t len; 316 317 /* for "cifs/" service name + terminating 0 */ 318 len = strlen(hostname) + 5 + 1; 319 princ = SMB_XMALLOC_ARRAY(char, len); 320 if (!princ) { 321 rc = 1; 322 break; 323 } 324 if (use_cifs_service_prefix) { 325 strlcpy(princ, "cifs/", len); 326 } else { 327 strlcpy(princ, "host/", len); 328 } 329 strlcpy(princ + 5, hostname, len - 5); 330 331 if (sectype == MS_KRB5) 332 oid = OID_KERBEROS5_OLD; 333 else 334 oid = OID_KERBEROS5; 335 336 rc = handle_krb5_mech(oid, princ, &secblob, &sess_key); 337 SAFE_FREE(princ); 338 break; 339 } 340 default:{ 341 syslog(LOG_WARNING, "sectype: %d is not implemented", 342 sectype); 343 rc = 1; 344 break; 345 } 346 } 347 348 if (rc) { 349 goto out; 350 } 559 case KRB5: 560 retry_new_hostname: 561 /* for "cifs/" service name + terminating 0 */ 562 datalen = strlen(host) + 5 + 1; 563 princ = SMB_XMALLOC_ARRAY(char, datalen); 564 if (!princ) { 565 rc = -ENOMEM; 566 break; 567 } 568 569 if (arg.sec == MS_KRB5) 570 oid = OID_KERBEROS5_OLD; 571 else 572 oid = OID_KERBEROS5; 573 574 /* 575 * try getting a cifs/ principal first and then fall back to 576 * getting a host/ principal if that doesn't work. 577 */ 578 strlcpy(princ, "cifs/", datalen); 579 strlcpy(princ + 5, host, datalen - 5); 580 rc = handle_krb5_mech(oid, princ, &secblob, &sess_key, ccname); 581 if (!rc) 582 break; 583 584 memcpy(princ, "host/", 5); 585 rc = handle_krb5_mech(oid, princ, &secblob, &sess_key, ccname); 586 if (!rc) 587 break; 588 589 if (!try_dns || !(have & DKD_HAVE_IP)) 590 break; 591 592 rc = ip_to_fqdn(arg.ip, hostbuf, sizeof(hostbuf)); 593 if (rc) 594 break; 595 596 SAFE_FREE(princ); 597 try_dns = 0; 598 host = hostbuf; 599 goto retry_new_hostname; 600 default: 601 syslog(LOG_ERR, "sectype: %d is not implemented", arg.sec); 602 rc = 1; 603 break; 604 } 605 606 SAFE_FREE(princ); 607 608 if (rc) 609 goto out; 351 610 352 611 /* pack SecurityBLob and SessionKey into downcall packet */ … … 358 617 goto out; 359 618 } 360 keydata->version = kernel_upcall_version;619 keydata->version = arg.ver; 361 620 keydata->flags = 0; 362 621 keydata->sesskey_len = sess_key.length; … … 369 628 rc = keyctl_instantiate(key, keydata, datalen, 0); 370 629 if (rc == -1) { 371 syslog(LOG_ WARNING, "keyctl_instantiate: %s", strerror(errno));630 syslog(LOG_ERR, "keyctl_instantiate: %s", strerror(errno)); 372 631 goto out; 373 632 } … … 386 645 data_blob_free(&secblob); 387 646 data_blob_free(&sess_key); 388 SAFE_FREE(hostname); 647 SAFE_FREE(ccname); 648 SAFE_FREE(arg.hostname); 649 SAFE_FREE(arg.ip); 389 650 SAFE_FREE(keydata); 390 651 return rc;
Note:
See TracChangeset
for help on using the changeset viewer.