Home | History | Annotate | Line # | Download | only in hdb
print.c revision 1.1.1.2.6.1
      1 /*	$NetBSD: print.c,v 1.1.1.2.6.1 2017/03/20 06:51:44 pgoyette Exp $	*/
      2 
      3 /*
      4  * Copyright (c) 1999-2005 Kungliga Tekniska Hgskolan
      5  * (Royal Institute of Technology, Stockholm, Sweden).
      6  * All rights reserved.
      7  *
      8  * Redistribution and use in source and binary forms, with or without
      9  * modification, are permitted provided that the following conditions
     10  * are met:
     11  *
     12  * 1. Redistributions of source code must retain the above copyright
     13  *    notice, this list of conditions and the following disclaimer.
     14  *
     15  * 2. Redistributions in binary form must reproduce the above copyright
     16  *    notice, this list of conditions and the following disclaimer in the
     17  *    documentation and/or other materials provided with the distribution.
     18  *
     19  * 3. Neither the name of KTH nor the names of its contributors may be
     20  *    used to endorse or promote products derived from this software without
     21  *    specific prior written permission.
     22  *
     23  * THIS SOFTWARE IS PROVIDED BY KTH AND ITS CONTRIBUTORS ``AS IS'' AND ANY
     24  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     25  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     26  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KTH OR ITS CONTRIBUTORS BE
     27  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     28  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
     29  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
     30  * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
     31  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
     32  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
     33  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
     34 
     35 #include "hdb_locl.h"
     36 #include <krb5/hex.h>
     37 #include <ctype.h>
     38 
     39 /*
     40    This is the present contents of a dump line. This might change at
     41    any time. Fields are separated by white space.
     42 
     43   principal
     44   keyblock
     45   	kvno
     46 	keys...
     47 		mkvno
     48 		enctype
     49 		keyvalue
     50 		salt (- means use normal salt)
     51   creation date and principal
     52   modification date and principal
     53   principal valid from date (not used)
     54   principal valid end date (not used)
     55   principal key expires (not used)
     56   max ticket life
     57   max renewable life
     58   flags
     59   generation number
     60   */
     61 
     62 /*
     63  * These utility functions return the number of bytes written or -1, and
     64  * they set an error in the context.
     65  */
     66 static ssize_t
     67 append_string(krb5_context context, krb5_storage *sp, const char *fmt, ...)
     68 {
     69     ssize_t sz;
     70     char *s;
     71     int rc;
     72     va_list ap;
     73     va_start(ap, fmt);
     74     rc = vasprintf(&s, fmt, ap);
     75     va_end(ap);
     76     if(rc < 0) {
     77 	krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
     78 	return -1;
     79     }
     80     sz = krb5_storage_write(sp, s, strlen(s));
     81     free(s);
     82     return sz;
     83 }
     84 
     85 static krb5_error_code
     86 append_hex(krb5_context context, krb5_storage *sp,
     87            int always_encode, int lower, krb5_data *data)
     88 {
     89     ssize_t sz;
     90     int printable = 1;
     91     size_t i;
     92     char *p;
     93 
     94     p = data->data;
     95     if (!always_encode) {
     96         for (i = 0; i < data->length; i++) {
     97             if (!isalnum((unsigned char)p[i]) && p[i] != '.'){
     98                 printable = 0;
     99                 break;
    100             }
    101         }
    102     }
    103     if (printable && !always_encode)
    104 	return append_string(context, sp, "\"%.*s\"",
    105 			     data->length, data->data);
    106     sz = hex_encode(data->data, data->length, &p);
    107     if (sz == -1) return sz;
    108     if (lower)
    109         strlwr(p);
    110     sz = append_string(context, sp, "%s", p);
    111     free(p);
    112     return sz;
    113 }
    114 
    115 static char *
    116 time2str(time_t t)
    117 {
    118     static char buf[128];
    119     strftime(buf, sizeof(buf), "%Y%m%d%H%M%S", gmtime(&t));
    120     return buf;
    121 }
    122 
    123 static ssize_t
    124 append_event(krb5_context context, krb5_storage *sp, Event *ev)
    125 {
    126     krb5_error_code ret;
    127     ssize_t sz;
    128     char *pr = NULL;
    129     if(ev == NULL)
    130 	return append_string(context, sp, "- ");
    131     if (ev->principal != NULL) {
    132        ret = krb5_unparse_name(context, ev->principal, &pr);
    133        if (ret) return -1; /* krb5_unparse_name() sets error info */
    134     }
    135     sz = append_string(context, sp, "%s:%s ", time2str(ev->time),
    136                        pr ? pr : "UNKNOWN");
    137     free(pr);
    138     return sz;
    139 }
    140 
    141 #define KRB5_KDB_SALTTYPE_NORMAL        0
    142 #define KRB5_KDB_SALTTYPE_V4            1
    143 #define KRB5_KDB_SALTTYPE_NOREALM       2
    144 #define KRB5_KDB_SALTTYPE_ONLYREALM     3
    145 #define KRB5_KDB_SALTTYPE_SPECIAL       4
    146 #define KRB5_KDB_SALTTYPE_AFS3          5
    147 
    148 static ssize_t
    149 append_mit_key(krb5_context context, krb5_storage *sp,
    150                krb5_const_principal princ,
    151                unsigned int kvno, Key *key)
    152 {
    153     krb5_error_code ret;
    154     krb5_salt k5salt;
    155     ssize_t sz;
    156     size_t key_versions = key->salt ? 2 : 1;
    157     size_t decrypted_key_length;
    158     char buf[2];
    159     krb5_data keylenbytes;
    160     unsigned int salttype;
    161 
    162     sz = append_string(context, sp, "\t%u\t%u\t%d\t%d\t", key_versions, kvno,
    163                         key->key.keytype, key->key.keyvalue.length + 2);
    164     if (sz == -1) return sz;
    165     ret = krb5_enctype_keysize(context, key->key.keytype, &decrypted_key_length);
    166     if (ret) return -1; /* XXX we lose the error code */
    167     buf[0] = decrypted_key_length & 0xff;
    168     buf[1] = (decrypted_key_length & 0xff00) >> 8;
    169     keylenbytes.data = buf;
    170     keylenbytes.length = sizeof (buf);
    171     sz = append_hex(context, sp, 1, 1, &keylenbytes);
    172     if (sz == -1) return sz;
    173     sz = append_hex(context, sp, 1, 1, &key->key.keyvalue);
    174     if (!key->salt)
    175         return sz;
    176 
    177     /* Map salt to MIT KDB style */
    178     switch (key->salt->type) {
    179     case KRB5_PADATA_PW_SALT:
    180 
    181         /*
    182          * Compute normal salt and then see whether it matches the stored one
    183          */
    184         ret = krb5_get_pw_salt(context, princ, &k5salt);
    185         if (ret) return -1;
    186         if (k5salt.saltvalue.length == key->salt->salt.length &&
    187             memcmp(k5salt.saltvalue.data, key->salt->salt.data,
    188                    k5salt.saltvalue.length) == 0)
    189             salttype = KRB5_KDB_SALTTYPE_NORMAL; /* matches */
    190         else if (key->salt->salt.length == strlen(princ->realm) &&
    191                  memcmp(key->salt->salt.data, princ->realm,
    192                         key->salt->salt.length) == 0)
    193             salttype = KRB5_KDB_SALTTYPE_ONLYREALM; /* matches realm */
    194         else if (key->salt->salt.length ==
    195 		 k5salt.saltvalue.length - strlen(princ->realm) &&
    196                  memcmp((char *)k5salt.saltvalue.data + strlen(princ->realm),
    197                         key->salt->salt.data, key->salt->salt.length) == 0)
    198             salttype = KRB5_KDB_SALTTYPE_NOREALM; /* matches w/o realm */
    199         else
    200             salttype = KRB5_KDB_SALTTYPE_NORMAL;  /* hope for best */
    201 
    202 	break;
    203 
    204     case KRB5_PADATA_AFS3_SALT:
    205         salttype = KRB5_KDB_SALTTYPE_AFS3;
    206 	break;
    207 
    208     default:
    209 	return -1;
    210     }
    211 
    212     sz = append_string(context, sp, "\t%u\t%u\t", salttype,
    213                        key->salt->salt.length);
    214     if (sz == -1) return sz;
    215     return append_hex(context, sp, 1, 1, &key->salt->salt);
    216 }
    217 
    218 static krb5_error_code
    219 entry2string_int (krb5_context context, krb5_storage *sp, hdb_entry *ent)
    220 {
    221     char *p;
    222     size_t i;
    223     krb5_error_code ret;
    224 
    225     /* --- principal */
    226     ret = krb5_unparse_name(context, ent->principal, &p);
    227     if(ret)
    228 	return ret;
    229     append_string(context, sp, "%s ", p);
    230     free(p);
    231     /* --- kvno */
    232     append_string(context, sp, "%d", ent->kvno);
    233     /* --- keys */
    234     for(i = 0; i < ent->keys.len; i++){
    235 	/* --- mkvno, keytype */
    236 	if(ent->keys.val[i].mkvno)
    237 	    append_string(context, sp, ":%d:%d:",
    238 			  *ent->keys.val[i].mkvno,
    239 			  ent->keys.val[i].key.keytype);
    240 	else
    241 	    append_string(context, sp, "::%d:",
    242 			  ent->keys.val[i].key.keytype);
    243 	/* --- keydata */
    244 	append_hex(context, sp, 0, 0, &ent->keys.val[i].key.keyvalue);
    245 	append_string(context, sp, ":");
    246 	/* --- salt */
    247 	if(ent->keys.val[i].salt){
    248 	    append_string(context, sp, "%u/", ent->keys.val[i].salt->type);
    249 	    append_hex(context, sp, 0, 0, &ent->keys.val[i].salt->salt);
    250 	}else
    251 	    append_string(context, sp, "-");
    252     }
    253     append_string(context, sp, " ");
    254     /* --- created by */
    255     append_event(context, sp, &ent->created_by);
    256     /* --- modified by */
    257     append_event(context, sp, ent->modified_by);
    258 
    259     /* --- valid start */
    260     if(ent->valid_start)
    261 	append_string(context, sp, "%s ", time2str(*ent->valid_start));
    262     else
    263 	append_string(context, sp, "- ");
    264 
    265     /* --- valid end */
    266     if(ent->valid_end)
    267 	append_string(context, sp, "%s ", time2str(*ent->valid_end));
    268     else
    269 	append_string(context, sp, "- ");
    270 
    271     /* --- password ends */
    272     if(ent->pw_end)
    273 	append_string(context, sp, "%s ", time2str(*ent->pw_end));
    274     else
    275 	append_string(context, sp, "- ");
    276 
    277     /* --- max life */
    278     if(ent->max_life)
    279 	append_string(context, sp, "%d ", *ent->max_life);
    280     else
    281 	append_string(context, sp, "- ");
    282 
    283     /* --- max renewable life */
    284     if(ent->max_renew)
    285 	append_string(context, sp, "%d ", *ent->max_renew);
    286     else
    287 	append_string(context, sp, "- ");
    288 
    289     /* --- flags */
    290     append_string(context, sp, "%d ", HDBFlags2int(ent->flags));
    291 
    292     /* --- generation number */
    293     if(ent->generation) {
    294 	append_string(context, sp, "%s:%d:%d ", time2str(ent->generation->time),
    295 		      ent->generation->usec,
    296 		      ent->generation->gen);
    297     } else
    298 	append_string(context, sp, "- ");
    299 
    300     /* --- extensions */
    301     if(ent->extensions && ent->extensions->len > 0) {
    302 	for(i = 0; i < ent->extensions->len; i++) {
    303 	    void *d;
    304 	    size_t size, sz = 0;
    305 
    306 	    ASN1_MALLOC_ENCODE(HDB_extension, d, size,
    307 			       &ent->extensions->val[i], &sz, ret);
    308 	    if (ret) {
    309 		krb5_clear_error_message(context);
    310 		return ret;
    311 	    }
    312 	    if(size != sz)
    313 		krb5_abortx(context, "internal asn.1 encoder error");
    314 
    315 	    if (hex_encode(d, size, &p) < 0) {
    316 		free(d);
    317 		krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
    318 		return ENOMEM;
    319 	    }
    320 
    321 	    free(d);
    322 	    append_string(context, sp, "%s%s", p,
    323 			  ent->extensions->len - 1 != i ? ":" : "");
    324 	    free(p);
    325 	}
    326     } else
    327 	append_string(context, sp, "-");
    328 
    329     return 0;
    330 }
    331 
    332 #define KRB5_KDB_DISALLOW_POSTDATED     0x00000001
    333 #define KRB5_KDB_DISALLOW_FORWARDABLE   0x00000002
    334 #define KRB5_KDB_DISALLOW_TGT_BASED     0x00000004
    335 #define KRB5_KDB_DISALLOW_RENEWABLE     0x00000008
    336 #define KRB5_KDB_DISALLOW_PROXIABLE     0x00000010
    337 #define KRB5_KDB_DISALLOW_DUP_SKEY      0x00000020
    338 #define KRB5_KDB_DISALLOW_ALL_TIX       0x00000040
    339 #define KRB5_KDB_REQUIRES_PRE_AUTH      0x00000080
    340 #define KRB5_KDB_REQUIRES_HW_AUTH       0x00000100
    341 #define KRB5_KDB_REQUIRES_PWCHANGE      0x00000200
    342 #define KRB5_KDB_DISALLOW_SVR           0x00001000
    343 #define KRB5_KDB_PWCHANGE_SERVICE       0x00002000
    344 #define KRB5_KDB_SUPPORT_DESMD5         0x00004000
    345 #define KRB5_KDB_NEW_PRINC              0x00008000
    346 
    347 static int
    348 flags_to_attr(HDBFlags flags)
    349 {
    350     int a = 0;
    351 
    352     if (!flags.postdate)
    353         a |= KRB5_KDB_DISALLOW_POSTDATED;
    354     if (!flags.forwardable)
    355         a |= KRB5_KDB_DISALLOW_FORWARDABLE;
    356     if (flags.initial)
    357         a |= KRB5_KDB_DISALLOW_TGT_BASED;
    358     if (!flags.renewable)
    359         a |= KRB5_KDB_DISALLOW_RENEWABLE;
    360     if (!flags.proxiable)
    361         a |= KRB5_KDB_DISALLOW_PROXIABLE;
    362     if (flags.invalid)
    363         a |= KRB5_KDB_DISALLOW_ALL_TIX;
    364     if (flags.require_preauth)
    365         a |= KRB5_KDB_REQUIRES_PRE_AUTH;
    366     if (flags.require_hwauth)
    367         a |= KRB5_KDB_REQUIRES_HW_AUTH;
    368     if (!flags.server)
    369         a |= KRB5_KDB_DISALLOW_SVR;
    370     if (flags.change_pw)
    371         a |= KRB5_KDB_PWCHANGE_SERVICE;
    372     return a;
    373 }
    374 
    375 krb5_error_code
    376 entry2mit_string_int(krb5_context context, krb5_storage *sp, hdb_entry *ent)
    377 {
    378     krb5_error_code ret;
    379     ssize_t sz;
    380     size_t i, k;
    381     size_t num_tl_data = 0;
    382     size_t num_key_data = 0;
    383     char *p;
    384     HDB_Ext_KeySet *hist_keys = NULL;
    385     HDB_extension *extp;
    386     time_t last_pw_chg = 0;
    387     time_t exp = 0;
    388     time_t pwexp = 0;
    389     unsigned int max_life = 0;
    390     unsigned int max_renew = 0;
    391 
    392     if (ent->modified_by)
    393         num_tl_data++;
    394 
    395     ret = hdb_entry_get_pw_change_time(ent, &last_pw_chg);
    396     if (ret) return ret;
    397     if (last_pw_chg)
    398         num_tl_data++;
    399 
    400     extp = hdb_find_extension(ent, choice_HDB_extension_data_hist_keys);
    401     if (extp)
    402         hist_keys = &extp->data.u.hist_keys;
    403 
    404     for (i = 0; i < ent->keys.len;i++) {
    405         if (ent->keys.val[i].key.keytype == ETYPE_DES_CBC_MD4 ||
    406             ent->keys.val[i].key.keytype == ETYPE_DES_CBC_MD5)
    407             continue;
    408         num_key_data++;
    409     }
    410     if (hist_keys) {
    411         for (i = 0; i < hist_keys->len; i++) {
    412             /*
    413              * MIT uses the highest kvno as the current kvno instead of
    414              * tracking kvno separately, so we can't dump keysets with kvno
    415              * higher than the entry's kvno.
    416              */
    417             if (hist_keys->val[i].kvno >= ent->kvno)
    418                 continue;
    419             for (k = 0; k < hist_keys->val[i].keys.len; k++) {
    420                 if (ent->keys.val[k].key.keytype == ETYPE_DES_CBC_MD4 ||
    421                     ent->keys.val[k].key.keytype == ETYPE_DES_CBC_MD5)
    422                     continue;
    423                 num_key_data++;
    424             }
    425         }
    426     }
    427 
    428     ret = krb5_unparse_name(context, ent->principal, &p);
    429     if (ret) return ret;
    430     sz = append_string(context, sp, "princ\t38\t%u\t%u\t%u\t0\t%s\t%d",
    431                        strlen(p), num_tl_data, num_key_data, p,
    432                        flags_to_attr(ent->flags));
    433     free(p);
    434     if (sz == -1) return ENOMEM;
    435 
    436     if (ent->max_life)
    437         max_life = *ent->max_life;
    438     if (ent->max_renew)
    439         max_renew = *ent->max_renew;
    440     if (ent->valid_end)
    441         exp = *ent->valid_end;
    442     if (ent->pw_end)
    443         pwexp = *ent->pw_end;
    444 
    445     sz = append_string(context, sp, "\t%u\t%u\t%u\t%u\t0\t0\t0",
    446                        max_life, max_renew, exp, pwexp);
    447     if (sz == -1) return ENOMEM;
    448 
    449     /* Dump TL data we know: last pw chg and modified_by */
    450 #define mit_KRB5_TL_LAST_PWD_CHANGE     1
    451 #define mit_KRB5_TL_MOD_PRINC           2
    452     if (last_pw_chg) {
    453         krb5_data d;
    454         time_t val;
    455         unsigned char *ptr;
    456 
    457         ptr = (unsigned char *)&last_pw_chg;
    458         val = ptr[0] | (ptr[1] << 8) | (ptr[2] << 16) | (ptr[3] << 24);
    459         d.data = &val;
    460         d.length = sizeof (last_pw_chg);
    461         sz = append_string(context, sp, "\t%u\t%u\t",
    462                            mit_KRB5_TL_LAST_PWD_CHANGE, d.length);
    463         if (sz == -1) return ENOMEM;
    464         sz = append_hex(context, sp, 1, 1, &d);
    465         if (sz == -1) return ENOMEM;
    466     }
    467     if (ent->modified_by) {
    468         krb5_data d;
    469         unsigned int val;
    470         size_t plen;
    471         unsigned char *ptr;
    472         char *modby_p;
    473 
    474         ptr = (unsigned char *)&ent->modified_by->time;
    475         val = ptr[0] | (ptr[1] << 8) | (ptr[2] << 16) | (ptr[3] << 24);
    476         d.data = &val;
    477         d.length = sizeof (ent->modified_by->time);
    478         ret = krb5_unparse_name(context, ent->modified_by->principal, &modby_p);
    479         if (ret) return ret;
    480         plen = strlen(modby_p);
    481         sz = append_string(context, sp, "\t%u\t%u\t",
    482                            mit_KRB5_TL_MOD_PRINC,
    483                            d.length + plen + 1 /* NULL counted */);
    484         if (sz == -1) return ENOMEM;
    485         sz = append_hex(context, sp, 1, 1, &d);
    486         if (sz == -1) {
    487             free(modby_p);
    488             return ENOMEM;
    489         }
    490         d.data = modby_p;
    491         d.length = plen + 1;
    492         sz = append_hex(context, sp, 1, 1, &d);
    493         free(modby_p);
    494         if (sz == -1) return ENOMEM;
    495     }
    496     /*
    497      * Dump keys (remembering to not include any with kvno higher than
    498      * the entry's because MIT doesn't track entry kvno separately from
    499      * the entry's keys -- max kvno is it)
    500      */
    501     for (i = 0; i < ent->keys.len; i++) {
    502         if (ent->keys.val[i].key.keytype == ETYPE_DES_CBC_MD4 ||
    503             ent->keys.val[i].key.keytype == ETYPE_DES_CBC_MD5)
    504             continue;
    505         sz = append_mit_key(context, sp, ent->principal, ent->kvno,
    506                             &ent->keys.val[i]);
    507         if (sz == -1) return ENOMEM;
    508     }
    509     for (i = 0; hist_keys && i < ent->kvno; i++) {
    510         size_t m;
    511 
    512         /* dump historical keys */
    513         for (k = 0; k < hist_keys->len; k++) {
    514             if (hist_keys->val[k].kvno != ent->kvno - i)
    515                 continue;
    516             for (m = 0; m < hist_keys->val[k].keys.len; m++) {
    517                 if (ent->keys.val[k].key.keytype == ETYPE_DES_CBC_MD4 ||
    518                     ent->keys.val[k].key.keytype == ETYPE_DES_CBC_MD5)
    519                     continue;
    520                 sz = append_mit_key(context, sp, ent->principal,
    521                                     hist_keys->val[k].kvno,
    522                                     &hist_keys->val[k].keys.val[m]);
    523                 if (sz == -1) return ENOMEM;
    524             }
    525         }
    526     }
    527     sz = append_string(context, sp, "\t-1;"); /* "extra data" */
    528     if (sz == -1) return ENOMEM;
    529     return 0;
    530 }
    531 
    532 krb5_error_code
    533 hdb_entry2string(krb5_context context, hdb_entry *ent, char **str)
    534 {
    535     krb5_error_code ret;
    536     krb5_data data;
    537     krb5_storage *sp;
    538 
    539     sp = krb5_storage_emem();
    540     if (sp == NULL) {
    541 	krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
    542 	return ENOMEM;
    543     }
    544 
    545     ret = entry2string_int(context, sp, ent);
    546     if (ret) {
    547 	krb5_storage_free(sp);
    548 	return ret;
    549     }
    550 
    551     krb5_storage_write(sp, "\0", 1);
    552     krb5_storage_to_data(sp, &data);
    553     krb5_storage_free(sp);
    554     *str = data.data;
    555     return 0;
    556 }
    557 
    558 /* print a hdb_entry to (FILE*)data; suitable for hdb_foreach */
    559 
    560 krb5_error_code
    561 hdb_print_entry(krb5_context context, HDB *db, hdb_entry_ex *entry,
    562                 void *data)
    563 {
    564     struct hdb_print_entry_arg *parg = data;
    565     krb5_error_code ret;
    566     krb5_storage *sp;
    567 
    568     fflush(parg->out);
    569     sp = krb5_storage_from_fd(fileno(parg->out));
    570     if (sp == NULL) {
    571 	krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
    572 	return ENOMEM;
    573     }
    574 
    575     switch (parg->fmt) {
    576     case HDB_DUMP_HEIMDAL:
    577         ret = entry2string_int(context, sp, &entry->entry);
    578         break;
    579     case HDB_DUMP_MIT:
    580         ret = entry2mit_string_int(context, sp, &entry->entry);
    581         break;
    582     default:
    583         heim_abort("Only two dump formats supported: Heimdal and MIT");
    584     }
    585     if (ret) {
    586 	krb5_storage_free(sp);
    587 	return ret;
    588     }
    589 
    590     krb5_storage_write(sp, "\n", 1);
    591     krb5_storage_free(sp);
    592     return 0;
    593 }
    594