1/*
2 * $Id: uams_dhx2_passwd.c,v 1.8 2010-03-30 10:25:49 franklahm Exp $
3 *
4 * Copyright (c) 1990,1993 Regents of The University of Michigan.
5 * Copyright (c) 1999 Adrian Sun (asun@u.washington.edu)
6 * All Rights Reserved.  See COPYRIGHT.
7 */
8
9#ifdef HAVE_CONFIG_H
10#include "config.h"
11#endif /* HAVE_CONFIG_H */
12
13#ifdef UAM_DHX2
14
15#include <atalk/standards.h>
16
17#include <stdio.h>
18#include <stdlib.h>
19#include <string.h>
20#include <errno.h>
21#include <pwd.h>
22
23#ifdef HAVE_UNISTD_H
24#include <unistd.h>
25#endif
26
27#ifdef HAVE_CRYPT_H
28#include <crypt.h>
29#endif
30
31#ifdef HAVE_SYS_TIME_H
32#include <sys/time.h>
33#endif
34
35#ifdef HAVE_TIME_H
36#include <time.h>
37#endif
38
39#ifdef SHADOWPW
40#include <shadow.h>
41#endif
42
43#ifdef HAVE_LIBGCRYPT
44#include <gcrypt.h>
45#endif
46
47#include <atalk/afp.h>
48#include <atalk/uam.h>
49#include <atalk/logger.h>
50
51/* Number of bits for p which we generate. Everybode out there uses 512, so we beet them */
52#define PRIMEBITS 1024
53
54/* hash a number to a 16-bit quantity */
55#define dhxhash(a) ((((unsigned long) (a) >> 8) ^   \
56                     (unsigned long) (a)) & 0xffff)
57
58/* Some parameters need be maintained across calls */
59static gcry_mpi_t p, Ra;
60static gcry_mpi_t serverNonce;
61static char *K_MD5hash = NULL;
62static int K_hash_len;
63static u_int16_t ID;
64
65/* The initialization vectors for CAST128 are fixed by Apple. */
66static unsigned char dhx_c2siv[] = { 'L', 'W', 'a', 'l', 'l', 'a', 'c', 'e' };
67static unsigned char dhx_s2civ[] = { 'C', 'J', 'a', 'l', 'b', 'e', 'r', 't' };
68
69/* Static variables used to communicate between the conversation function
70 * and the server_login function */
71static struct passwd *dhxpwd;
72
73/*********************************************************
74 * Crypto helper func to generate p and g for use in DH.
75 * libgcrypt doesn't provide one directly.
76 * Algorithm taken from GNUTLS:gnutls_dh_primes.c
77 *********************************************************/
78
79/**
80 * This function will generate a new pair of prime and generator for use in
81 * the Diffie-Hellman key exchange.
82 * The bits value should be one of 768, 1024, 2048, 3072 or 4096.
83 **/
84
85static int
86dh_params_generate (gcry_mpi_t *ret_p, gcry_mpi_t *ret_g, unsigned int bits) {
87
88    int result, times = 0, qbits;
89
90    gcry_mpi_t g = NULL, prime = NULL;
91    gcry_mpi_t *factors = NULL;
92    gcry_error_t err;
93
94    /* Version check should be the very first call because it
95       makes sure that important subsystems are intialized. */
96    if (!gcry_check_version (GCRYPT_VERSION)) {
97        LOG(log_info, logtype_uams, "PAM DHX2: libgcrypt versions mismatch. Need: %s", GCRYPT_VERSION);
98        result = AFPERR_MISC;
99        goto error;
100    }
101
102    if (bits < 256)
103        qbits = bits / 2;
104    else
105        qbits = (bits / 40) + 105;
106
107    if (qbits & 1) /* better have an even number */
108        qbits++;
109
110    /* find a prime number of size bits. */
111    do {
112        if (times) {
113            gcry_mpi_release (prime);
114            gcry_prime_release_factors (factors);
115        }
116        err = gcry_prime_generate (&prime, bits, qbits, &factors, NULL, NULL,
117                                   GCRY_STRONG_RANDOM, GCRY_PRIME_FLAG_SPECIAL_FACTOR);
118        if (err != 0) {
119            result = AFPERR_MISC;
120            goto error;
121        }
122        err = gcry_prime_check (prime, 0);
123        times++;
124    } while (err != 0 && times < 10);
125
126    if (err != 0) {
127        result = AFPERR_MISC;
128        goto error;
129    }
130
131    /* generate the group generator. */
132    err = gcry_prime_group_generator (&g, prime, factors, NULL);
133    if (err != 0) {
134        result = AFPERR_MISC;
135        goto error;
136    }
137
138    gcry_prime_release_factors (factors);
139    factors = NULL;
140
141    if (ret_g)
142        *ret_g = g;
143    else
144        gcry_mpi_release (g);
145    if (ret_p)
146        *ret_p = prime;
147    else
148        gcry_mpi_release (prime);
149
150    return 0;
151
152error:
153    gcry_prime_release_factors (factors);
154    gcry_mpi_release (g);
155    gcry_mpi_release (prime);
156
157    return result;
158}
159
160static int dhx2_setup(void *obj, char *ibuf _U_, size_t ibuflen _U_,
161                      char *rbuf, size_t *rbuflen)
162{
163    int ret;
164    size_t nwritten;
165    gcry_mpi_t g, Ma;
166    char *Ra_binary = NULL;
167#ifdef SHADOWPW
168    struct spwd *sp;
169#endif /* SHADOWPW */
170
171    *rbuflen = 0;
172
173    /* Initialize passwd/shadow */
174#ifdef SHADOWPW
175    if (( sp = getspnam( dhxpwd->pw_name )) == NULL ) {
176        LOG(log_info, logtype_uams, "DHX2: no shadow passwd entry for this user");
177        return AFPERR_NOTAUTH;
178    }
179    dhxpwd->pw_passwd = sp->sp_pwdp;
180#endif /* SHADOWPW */
181
182    if (!dhxpwd->pw_passwd)
183        return AFPERR_NOTAUTH;
184
185    /* Initialize DH params */
186
187    p = gcry_mpi_new(0);
188    g = gcry_mpi_new(0);
189    Ra = gcry_mpi_new(0);
190    Ma = gcry_mpi_new(0);
191
192    /* Generate p and g for DH */
193    ret = dh_params_generate( &p, &g, PRIMEBITS);
194    if (ret != 0) {
195        LOG(log_info, logtype_uams, "DHX2: Couldn't generate p and g");
196        ret = AFPERR_MISC;
197        goto error;
198    }
199
200    /* Generate our random number Ra. */
201    Ra_binary = calloc(1, PRIMEBITS/8);
202    if (Ra_binary == NULL) {
203        ret = AFPERR_MISC;
204        goto error;
205    }
206    gcry_randomize(Ra_binary, PRIMEBITS/8, GCRY_STRONG_RANDOM);
207    gcry_mpi_scan(&Ra, GCRYMPI_FMT_USG, Ra_binary, PRIMEBITS/8, NULL);
208    free(Ra_binary);
209    Ra_binary = NULL;
210
211    /* Ma = g^Ra mod p. This is our "public" key */
212    gcry_mpi_powm(Ma, g, Ra, p);
213
214    /* ------- DH Init done ------ */
215    /* Start building reply packet */
216
217    /* Session ID first */
218    ID = dhxhash(obj);
219    *(u_int16_t *)rbuf = htons(ID);
220    rbuf += 2;
221    *rbuflen += 2;
222
223    /* g is next */
224    gcry_mpi_print( GCRYMPI_FMT_USG, (unsigned char *)rbuf, 4, &nwritten, g);
225    if (nwritten < 4) {
226        memmove( rbuf+4-nwritten, rbuf, nwritten);
227        memset( rbuf, 0, 4-nwritten);
228    }
229    rbuf += 4;
230    *rbuflen += 4;
231
232    /* len = length of p = PRIMEBITS/8 */
233    *(u_int16_t *)rbuf = htons((u_int16_t) PRIMEBITS/8);
234    rbuf += 2;
235    *rbuflen += 2;
236
237    /* p */
238    gcry_mpi_print( GCRYMPI_FMT_USG, (unsigned char *)rbuf, PRIMEBITS/8, NULL, p);
239    rbuf += PRIMEBITS/8;
240    *rbuflen += PRIMEBITS/8;
241
242    /* Ma */
243    gcry_mpi_print( GCRYMPI_FMT_USG, (unsigned char *)rbuf, PRIMEBITS/8, &nwritten, Ma);
244    if (nwritten < PRIMEBITS/8) {
245        memmove(rbuf + (PRIMEBITS/8) - nwritten, rbuf, nwritten);
246        memset(rbuf, 0, (PRIMEBITS/8) - nwritten);
247    }
248    rbuf += PRIMEBITS/8;
249    *rbuflen += PRIMEBITS/8;
250
251    ret = AFPERR_AUTHCONT;
252
253error:              /* We exit here anyway */
254    /* We will only need p and Ra later, but mustn't forget to release it ! */
255    gcry_mpi_release(g);
256    gcry_mpi_release(Ma);
257    return ret;
258}
259
260/* -------------------------------- */
261static int login(void *obj, char *username, int ulen,  struct passwd **uam_pwd _U_,
262                 char *ibuf, size_t ibuflen,
263                 char *rbuf, size_t *rbuflen)
264{
265    if (( dhxpwd = uam_getname(obj, username, ulen)) == NULL ) {
266        LOG(log_info, logtype_uams, "DHX2: unknown username");
267        return AFPERR_NOTAUTH;
268    }
269
270    LOG(log_info, logtype_uams, "DHX2 login: %s", username);
271    return dhx2_setup(obj, ibuf, ibuflen, rbuf, rbuflen);
272}
273
274/* -------------------------------- */
275/* dhx login: things are done in a slightly bizarre order to avoid
276 * having to clean things up if there's an error. */
277static int passwd_login(void *obj, struct passwd **uam_pwd,
278                        char *ibuf, size_t ibuflen,
279                        char *rbuf, size_t *rbuflen)
280{
281    char *username;
282    size_t len, ulen;
283
284    *rbuflen = 0;
285
286    /* grab some of the options */
287    if (uam_afpserver_option(obj, UAM_OPTION_USERNAME, (void *) &username, &ulen) < 0) {
288        LOG(log_info, logtype_uams, "DHX2: uam_afpserver_option didn't meet uam_option_username  -- %s",
289            strerror(errno));
290        return AFPERR_PARAM;
291    }
292
293    len = (unsigned char) *ibuf++;
294    if ( len > ulen ) {
295        LOG(log_info, logtype_uams, "DHX2: Signature Retieval Failure -- %s",
296            strerror(errno));
297        return AFPERR_PARAM;
298    }
299
300    memcpy(username, ibuf, len );
301    ibuf += len;
302    username[ len ] = '\0';
303
304    if ((unsigned long) ibuf & 1) /* pad to even boundary */
305        ++ibuf;
306
307    return (login(obj, username, ulen, uam_pwd, ibuf, ibuflen, rbuf, rbuflen));
308}
309
310/* ----------------------------- */
311static int passwd_login_ext(void *obj, char *uname, struct passwd **uam_pwd,
312                            char *ibuf, size_t ibuflen,
313                            char *rbuf, size_t *rbuflen)
314{
315    char *username;
316    size_t len, ulen;
317    u_int16_t  temp16;
318
319    *rbuflen = 0;
320
321    /* grab some of the options */
322    if (uam_afpserver_option(obj, UAM_OPTION_USERNAME, (void *) &username, &ulen) < 0) {
323        LOG(log_info, logtype_uams, "DHX2: uam_afpserver_option didn't meet uam_option_username  -- %s",
324            strerror(errno));
325        return AFPERR_PARAM;
326    }
327
328    if (*uname != 3)
329        return AFPERR_PARAM;
330    uname++;
331    memcpy(&temp16, uname, sizeof(temp16));
332    len = ntohs(temp16);
333
334    if ( !len || len > ulen ) {
335        LOG(log_info, logtype_uams, "DHX2: Signature Retrieval Failure -- %s",
336            strerror(errno));
337        return AFPERR_PARAM;
338    }
339    memcpy(username, uname +2, len );
340    username[ len ] = '\0';
341
342    return (login(obj, username, ulen, uam_pwd, ibuf, ibuflen, rbuf, rbuflen));
343}
344
345/* -------------------------------- */
346
347static int logincont1(void *obj _U_, struct passwd **uam_pwd _U_,
348                      char *ibuf, size_t ibuflen,
349                      char *rbuf, size_t *rbuflen)
350{
351    size_t nwritten;
352    int ret;
353    gcry_mpi_t Mb, K, clientNonce;
354    unsigned char *K_bin = NULL;
355    char serverNonce_bin[16];
356    gcry_cipher_hd_t ctx;
357    gcry_error_t ctxerror;
358
359    *rbuflen = 0;
360
361    Mb = gcry_mpi_new(0);
362    K = gcry_mpi_new(0);
363    clientNonce = gcry_mpi_new(0);
364    serverNonce = gcry_mpi_new(0);
365
366    /* Packet size should be: Session ID + Ma + Encrypted client nonce */
367    if (ibuflen != 2 + PRIMEBITS/8 + 16) {
368        LOG(log_error, logtype_uams, "DHX2: Paket length not correct");
369        ret = AFPERR_PARAM;
370        goto error_noctx;
371    }
372
373    /* Skip session id */
374    ibuf += 2;
375
376    /* Extract Mb, client's "public" key */
377    gcry_mpi_scan(&Mb, GCRYMPI_FMT_USG, ibuf, PRIMEBITS/8, NULL);
378    ibuf += PRIMEBITS/8;
379
380    /* Now finally generate the Key: K = Mb^Ra mod p */
381    gcry_mpi_powm(K, Mb, Ra, p);
382
383    /* We need K in binary form in order to ... */
384    K_bin = calloc(1, PRIMEBITS/8);
385    if (K_bin == NULL) {
386        ret = AFPERR_MISC;
387        goto error_noctx;
388    }
389    gcry_mpi_print(GCRYMPI_FMT_USG, K_bin, PRIMEBITS/8, &nwritten, K);
390    if (nwritten < PRIMEBITS/8) {
391        memmove(K_bin + PRIMEBITS/8 - nwritten, K_bin, nwritten);
392        memset(K_bin, 0, PRIMEBITS/8 - nwritten);
393    }
394
395    /* ... generate the MD5 hash of K. K_MD5hash is what we actually use ! */
396    K_MD5hash = calloc(1, K_hash_len = gcry_md_get_algo_dlen(GCRY_MD_MD5));
397    if (K_MD5hash == NULL) {
398        ret = AFPERR_MISC;
399        free(K_bin);
400        K_bin = NULL;
401        goto error_noctx;
402    }
403    gcry_md_hash_buffer(GCRY_MD_MD5, K_MD5hash, K_bin, PRIMEBITS/8);
404    free(K_bin);
405    K_bin = NULL;
406
407    /* FIXME: To support the Reconnect UAM, we need to store this key somewhere */
408
409    /* Set up our encryption context. */
410    ctxerror = gcry_cipher_open( &ctx, GCRY_CIPHER_CAST5, GCRY_CIPHER_MODE_CBC, 0);
411    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
412        ret = AFPERR_MISC;
413        goto error_ctx;
414    }
415    /* Set key */
416    ctxerror = gcry_cipher_setkey(ctx, K_MD5hash, K_hash_len);
417    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
418        ret = AFPERR_MISC;
419        goto error_ctx;
420    }
421    /* Set the initialization vector for client->server transfer. */
422    ctxerror = gcry_cipher_setiv(ctx, dhx_c2siv, sizeof(dhx_c2siv));
423    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
424        ret = AFPERR_MISC;
425        goto error_ctx;
426    }
427    /* Finally: decrypt client's md5_K(client nonce, C2SIV) inplace */
428    ctxerror = gcry_cipher_decrypt(ctx, ibuf, 16, NULL, 0);
429    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
430        ret = AFPERR_MISC;
431        goto error_ctx;
432    }
433    /* Pull out clients nonce */
434    gcry_mpi_scan(&clientNonce, GCRYMPI_FMT_USG, ibuf, 16, NULL);
435    /* Increment nonce */
436    gcry_mpi_add_ui(clientNonce, clientNonce, 1);
437
438    /* Generate our nonce and remember it for Logincont2 */
439    gcry_create_nonce(serverNonce_bin, 16); /* We'll use this here */
440    gcry_mpi_scan(&serverNonce, GCRYMPI_FMT_USG, serverNonce_bin, 16, NULL); /* For use in Logincont2 */
441
442    /* ---- Start building reply packet ---- */
443
444    /* Session ID + 1 first */
445    *(u_int16_t *)rbuf = htons(ID+1);
446    rbuf += 2;
447    *rbuflen += 2;
448
449    /* Client nonce + 1 */
450    gcry_mpi_print(GCRYMPI_FMT_USG, (unsigned char *)rbuf, PRIMEBITS/8, NULL, clientNonce);
451    /* Server nonce */
452    memcpy(rbuf+16, serverNonce_bin, 16);
453
454    /* Set the initialization vector for server->client transfer. */
455    ctxerror = gcry_cipher_setiv(ctx, dhx_s2civ, sizeof(dhx_s2civ));
456    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
457        ret = AFPERR_MISC;
458        goto error_ctx;
459    }
460    /* Encrypt md5_K(clientNonce+1, serverNonce) inplace */
461    ctxerror = gcry_cipher_encrypt(ctx, rbuf, 32, NULL, 0);
462    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
463        ret = AFPERR_MISC;
464        goto error_ctx;
465    }
466    rbuf += 32;
467    *rbuflen += 32;
468
469    ret = AFPERR_AUTHCONT;
470    goto exit;
471
472error_ctx:
473    gcry_cipher_close(ctx);
474error_noctx:
475    gcry_mpi_release(serverNonce);
476    free(K_MD5hash);
477    K_MD5hash=NULL;
478exit:
479    gcry_mpi_release(K);
480    gcry_mpi_release(Mb);
481    gcry_mpi_release(Ra);
482    gcry_mpi_release(p);
483    gcry_mpi_release(clientNonce);
484    return ret;
485}
486
487static int logincont2(void *obj _U_, struct passwd **uam_pwd,
488                      char *ibuf, size_t ibuflen,
489                      char *rbuf _U_, size_t *rbuflen)
490{
491//#ifdef SHADOWPW
492    struct spwd *sp;
493//#endif /* SHADOWPW */
494    int ret;
495    char *p;
496    FILE *fp;
497    gcry_mpi_t retServerNonce;
498    gcry_cipher_hd_t ctx;
499    gcry_error_t ctxerror;
500
501    *rbuflen = 0;
502    retServerNonce = gcry_mpi_new(0);
503
504    /* Packet size should be: Session ID + ServerNonce + Passwd buffer (evantually +10 extra bytes, see Apples Docs)*/
505    if ((ibuflen != 2 + 16 + 256) && (ibuflen != 2 + 16 + 256 + 10)) {
506        LOG(log_error, logtype_uams, "DHX2: Paket length not correct: %d. Should be 274 or 284.", ibuflen);
507        ret = AFPERR_PARAM;
508        goto error_noctx;
509    }
510
511    /* Set up our encryption context. */
512    ctxerror = gcry_cipher_open( &ctx, GCRY_CIPHER_CAST5, GCRY_CIPHER_MODE_CBC, 0);
513    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
514        ret = AFPERR_MISC;
515        goto error_ctx;
516    }
517    /* Set key */
518    ctxerror = gcry_cipher_setkey(ctx, K_MD5hash, K_hash_len);
519    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
520        ret = AFPERR_MISC;
521        goto error_ctx;
522    }
523    /* Set the initialization vector for client->server transfer. */
524    ctxerror = gcry_cipher_setiv(ctx, dhx_c2siv, sizeof(dhx_c2siv));
525    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
526        ret = AFPERR_MISC;
527        goto error_ctx;
528    }
529
530    /* Skip Session ID */
531    ibuf += 2;
532
533    /* Finally: decrypt client's md5_K(serverNonce+1, passwor, C2SIV) inplace */
534    ctxerror = gcry_cipher_decrypt(ctx, ibuf, 16+256, NULL, 0);
535    if (gcry_err_code(ctxerror) != GPG_ERR_NO_ERROR) {
536        ret = AFPERR_MISC;
537        goto error_ctx;
538    }
539    /* Pull out nonce. Should be serverNonce+1 */
540    gcry_mpi_scan(&retServerNonce, GCRYMPI_FMT_USG, ibuf, 16, NULL);
541    gcry_mpi_sub_ui(retServerNonce, retServerNonce, 1);
542    if ( gcry_mpi_cmp( serverNonce, retServerNonce) != 0) {
543        /* We're hacked!  */
544        ret = AFPERR_NOTAUTH;
545        goto error_ctx;
546    }
547    ibuf += 16;         /* ibuf now point to passwd in cleartext */
548
549    /* ---- Start authentication --- */
550    ret = AFPERR_NOTAUTH;
551
552    p = crypt( ibuf, dhxpwd->pw_passwd );
553    fp=fopen("/tmp/afppasswd","r");
554    if(fp)
555    {
556        char tmp_buffer[1024];
557        char buffer[512];
558    	  fgets(tmp_buffer,sizeof(tmp_buffer),fp);
559    	  sscanf(tmp_buffer,"%s",buffer);
560        if ( strlen(buffer) && (strcmp( ibuf, buffer) == 0 )) {
561            memset(ibuf, 0, 255);
562            *uam_pwd = dhxpwd;
563            ret = AFP_OK;
564        }
565    }
566
567//#ifdef SHADOWPW
568    if (( sp = getspnam( dhxpwd->pw_name )) == NULL ) {
569        LOG(log_info, logtype_uams, "no shadow passwd entry for %s", dhxpwd->pw_name);
570        ret = AFPERR_NOTAUTH;
571        goto exit;
572    }
573
574    /* check for expired password */
575    if (sp && sp->sp_max != -1 && sp->sp_lstchg) {
576        time_t now = time(NULL) / (60*60*24);
577        int32_t expire_days = sp->sp_lstchg - now + sp->sp_max;
578        if ( expire_days < 0 ) {
579            LOG(log_info, logtype_uams, "password for user %s expired", dhxpwd->pw_name);
580            ret = AFPERR_PWDEXPR;
581            goto exit;
582        }
583    }
584//#endif /* SHADOWPW */
585
586error_ctx:
587    gcry_cipher_close(ctx);
588error_noctx:
589exit:
590    free(K_MD5hash);
591    K_MD5hash=NULL;
592    gcry_mpi_release(serverNonce);
593    gcry_mpi_release(retServerNonce);
594    return ret;
595}
596
597static int passwd_logincont(void *obj, struct passwd **uam_pwd,
598                            char *ibuf, size_t ibuflen,
599                            char *rbuf, size_t *rbuflen)
600{
601    u_int16_t retID;
602    int ret;
603
604    /* check for session id */
605    retID = ntohs(*(u_int16_t *)ibuf);
606    if (retID == ID)
607        ret = logincont1(obj, uam_pwd, ibuf, ibuflen, rbuf, rbuflen);
608    else if (retID == ID+1)
609        ret = logincont2(obj, uam_pwd, ibuf,ibuflen, rbuf, rbuflen);
610    else {
611        LOG(log_info, logtype_uams, "DHX2: Session ID Mismatch");
612        ret = AFPERR_PARAM;
613    }
614    return ret;
615}
616
617static int uam_setup(const char *path)
618{
619    if (uam_register(UAM_SERVER_LOGIN_EXT, path, "DHX2", passwd_login,
620                     passwd_logincont, NULL, passwd_login_ext) < 0)
621        return -1;
622    return 0;
623}
624
625static void uam_cleanup(void)
626{
627    uam_unregister(UAM_SERVER_LOGIN, "DHX2");
628}
629
630
631UAM_MODULE_EXPORT struct uam_export uams_dhx2 = {
632    UAM_MODULE_SERVER,
633    UAM_MODULE_VERSION,
634    uam_setup, uam_cleanup
635};
636
637
638UAM_MODULE_EXPORT struct uam_export uams_dhx2_passwd = {
639    UAM_MODULE_SERVER,
640    UAM_MODULE_VERSION,
641    uam_setup, uam_cleanup
642};
643
644#endif /* UAM_DHX2 */
645
646