1/* Licensed to the Apache Software Foundation (ASF) under one or more 2 * contributor license agreements. See the NOTICE file distributed with 3 * this work for additional information regarding copyright ownership. 4 * The ASF licenses this file to You under the Apache License, Version 2.0 5 * (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17#include "mod_session.h" 18#include "apu_version.h" 19#include "apr_base64.h" /* for apr_base64_decode et al */ 20#include "apr_lib.h" 21#include "apr_strings.h" 22#include "http_log.h" 23#include "http_core.h" 24 25#if APU_MAJOR_VERSION == 1 && APU_MINOR_VERSION < 4 26 27#error session_crypto_module requires APU v1.4.0 or later 28 29#elif APU_HAVE_CRYPTO == 0 30 31#error Crypto support must be enabled in APR 32 33#else 34 35#include "apr_crypto.h" /* for apr_*_crypt et al */ 36 37#define CRYPTO_KEY "session_crypto_context" 38 39module AP_MODULE_DECLARE_DATA session_crypto_module; 40 41/** 42 * Structure to carry the per-dir session config. 43 */ 44typedef struct { 45 apr_array_header_t *passphrases; 46 int passphrases_set; 47 const char *cipher; 48 int cipher_set; 49} session_crypto_dir_conf; 50 51/** 52 * Structure to carry the server wide session config. 53 */ 54typedef struct { 55 const char *library; 56 const char *params; 57 int library_set; 58} session_crypto_conf; 59 60/** 61 * Initialise the encryption as per the current config. 62 * 63 * Returns APR_SUCCESS if successful. 64 */ 65static apr_status_t crypt_init(request_rec *r, 66 const apr_crypto_t *f, apr_crypto_block_key_type_e **cipher, 67 session_crypto_dir_conf * dconf) 68{ 69 apr_status_t res; 70 apr_hash_t *ciphers; 71 72 res = apr_crypto_get_block_key_types(&ciphers, f); 73 if (APR_SUCCESS != res) { 74 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01823) 75 "no ciphers returned by APR. " 76 "session encryption not possible"); 77 return res; 78 } 79 80 *cipher = apr_hash_get(ciphers, dconf->cipher, APR_HASH_KEY_STRING); 81 if (!(*cipher)) { 82 apr_hash_index_t *hi; 83 const void *key; 84 apr_ssize_t klen; 85 int sum = 0; 86 int offset = 0; 87 char *options = NULL; 88 89 for (hi = apr_hash_first(r->pool, ciphers); hi; hi = apr_hash_next(hi)) { 90 apr_hash_this(hi, NULL, &klen, NULL); 91 sum += klen + 2; 92 } 93 for (hi = apr_hash_first(r->pool, ciphers); hi; hi = apr_hash_next(hi)) { 94 apr_hash_this(hi, &key, &klen, NULL); 95 if (!options) { 96 options = apr_palloc(r->pool, sum + 1); 97 } 98 else { 99 options[offset++] = ','; 100 options[offset++] = ' '; 101 } 102 strncpy(options + offset, key, klen); 103 offset += klen; 104 } 105 options[offset] = 0; 106 107 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01824) 108 "cipher '%s' not recognised by crypto driver. " 109 "session encryption not possible, options: %s", dconf->cipher, options); 110 111 return APR_EGENERAL; 112 } 113 114 return APR_SUCCESS; 115} 116 117/** 118 * Encrypt the string given as per the current config. 119 * 120 * Returns APR_SUCCESS if successful. 121 */ 122static apr_status_t encrypt_string(request_rec * r, const apr_crypto_t *f, 123 session_crypto_dir_conf *dconf, const char *in, char **out) 124{ 125 apr_status_t res; 126 apr_crypto_key_t *key = NULL; 127 apr_size_t ivSize = 0; 128 apr_crypto_block_t *block = NULL; 129 unsigned char *encrypt = NULL; 130 unsigned char *combined = NULL; 131 apr_size_t encryptlen, tlen; 132 char *base64; 133 apr_size_t blockSize = 0; 134 const unsigned char *iv = NULL; 135 apr_uuid_t salt; 136 apr_crypto_block_key_type_e *cipher; 137 const char *passphrase; 138 139 /* by default, return an empty string */ 140 *out = ""; 141 142 /* don't attempt to encrypt an empty string, trying to do so causes a segfault */ 143 if (!in || !*in) { 144 return APR_SUCCESS; 145 } 146 147 /* use a uuid as a salt value, and prepend it to our result */ 148 apr_uuid_get(&salt); 149 res = crypt_init(r, f, &cipher, dconf); 150 if (res != APR_SUCCESS) { 151 return res; 152 } 153 154 /* encrypt using the first passphrase in the list */ 155 passphrase = APR_ARRAY_IDX(dconf->passphrases, 0, char *); 156 res = apr_crypto_passphrase(&key, &ivSize, passphrase, 157 strlen(passphrase), 158 (unsigned char *) (&salt), sizeof(apr_uuid_t), 159 *cipher, APR_MODE_CBC, 1, 4096, f, r->pool); 160 if (APR_STATUS_IS_ENOKEY(res)) { 161 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01825) 162 "the passphrase '%s' was empty", passphrase); 163 } 164 if (APR_STATUS_IS_EPADDING(res)) { 165 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01826) 166 "padding is not supported for cipher"); 167 } 168 if (APR_STATUS_IS_EKEYTYPE(res)) { 169 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01827) 170 "the key type is not known"); 171 } 172 if (APR_SUCCESS != res) { 173 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01828) 174 "encryption could not be configured."); 175 return res; 176 } 177 178 res = apr_crypto_block_encrypt_init(&block, &iv, key, &blockSize, r->pool); 179 if (APR_SUCCESS != res) { 180 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01829) 181 "apr_crypto_block_encrypt_init failed"); 182 return res; 183 } 184 185 /* encrypt the given string */ 186 res = apr_crypto_block_encrypt(&encrypt, &encryptlen, (unsigned char *)in, 187 strlen(in), block); 188 if (APR_SUCCESS != res) { 189 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01830) 190 "apr_crypto_block_encrypt failed"); 191 return res; 192 } 193 res = apr_crypto_block_encrypt_finish(encrypt + encryptlen, &tlen, block); 194 if (APR_SUCCESS != res) { 195 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01831) 196 "apr_crypto_block_encrypt_finish failed"); 197 return res; 198 } 199 encryptlen += tlen; 200 201 /* prepend the salt and the iv to the result */ 202 combined = apr_palloc(r->pool, ivSize + encryptlen + sizeof(apr_uuid_t)); 203 memcpy(combined, &salt, sizeof(apr_uuid_t)); 204 memcpy(combined + sizeof(apr_uuid_t), iv, ivSize); 205 memcpy(combined + sizeof(apr_uuid_t) + ivSize, encrypt, encryptlen); 206 207 /* base64 encode the result */ 208 base64 = apr_palloc(r->pool, apr_base64_encode_len(ivSize + encryptlen + 209 sizeof(apr_uuid_t) + 1) 210 * sizeof(char)); 211 apr_base64_encode(base64, (const char *) combined, 212 ivSize + encryptlen + sizeof(apr_uuid_t)); 213 *out = base64; 214 215 return res; 216 217} 218 219/** 220 * Decrypt the string given as per the current config. 221 * 222 * Returns APR_SUCCESS if successful. 223 */ 224static apr_status_t decrypt_string(request_rec * r, const apr_crypto_t *f, 225 session_crypto_dir_conf *dconf, const char *in, char **out) 226{ 227 apr_status_t res; 228 apr_crypto_key_t *key = NULL; 229 apr_size_t ivSize = 0; 230 apr_crypto_block_t *block = NULL; 231 unsigned char *decrypted = NULL; 232 apr_size_t decryptedlen, tlen; 233 apr_size_t decodedlen; 234 char *decoded; 235 apr_size_t blockSize = 0; 236 apr_crypto_block_key_type_e *cipher; 237 int i = 0; 238 239 /* strip base64 from the string */ 240 decoded = apr_palloc(r->pool, apr_base64_decode_len(in)); 241 decodedlen = apr_base64_decode(decoded, in); 242 decoded[decodedlen] = '\0'; 243 244 res = crypt_init(r, f, &cipher, dconf); 245 if (res != APR_SUCCESS) { 246 return res; 247 } 248 249 /* try each passphrase in turn */ 250 for (; i < dconf->passphrases->nelts; i++) { 251 const char *passphrase = APR_ARRAY_IDX(dconf->passphrases, i, char *); 252 apr_size_t len = decodedlen; 253 char *slider = decoded; 254 255 /* encrypt using the first passphrase in the list */ 256 res = apr_crypto_passphrase(&key, &ivSize, passphrase, 257 strlen(passphrase), 258 (unsigned char *)decoded, sizeof(apr_uuid_t), 259 *cipher, APR_MODE_CBC, 1, 4096, f, r->pool); 260 if (APR_STATUS_IS_ENOKEY(res)) { 261 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(01832) 262 "the passphrase '%s' was empty", passphrase); 263 continue; 264 } 265 else if (APR_STATUS_IS_EPADDING(res)) { 266 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(01833) 267 "padding is not supported for cipher"); 268 continue; 269 } 270 else if (APR_STATUS_IS_EKEYTYPE(res)) { 271 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(01834) 272 "the key type is not known"); 273 continue; 274 } 275 else if (APR_SUCCESS != res) { 276 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(01835) 277 "encryption could not be configured."); 278 continue; 279 } 280 281 /* sanity check - decoded too short? */ 282 if (decodedlen < (sizeof(apr_uuid_t) + ivSize)) { 283 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, r, APLOGNO(01836) 284 "too short to decrypt, skipping"); 285 res = APR_ECRYPT; 286 continue; 287 } 288 289 /* bypass the salt at the start of the decoded block */ 290 slider += sizeof(apr_uuid_t); 291 len -= sizeof(apr_uuid_t); 292 293 res = apr_crypto_block_decrypt_init(&block, &blockSize, (unsigned char *)slider, key, 294 r->pool); 295 if (APR_SUCCESS != res) { 296 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(01837) 297 "apr_crypto_block_decrypt_init failed"); 298 continue; 299 } 300 301 /* bypass the iv at the start of the decoded block */ 302 slider += ivSize; 303 len -= ivSize; 304 305 /* decrypt the given string */ 306 res = apr_crypto_block_decrypt(&decrypted, &decryptedlen, 307 (unsigned char *)slider, len, block); 308 if (res) { 309 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(01838) 310 "apr_crypto_block_decrypt failed"); 311 continue; 312 } 313 *out = (char *) decrypted; 314 315 res = apr_crypto_block_decrypt_finish(decrypted + decryptedlen, &tlen, block); 316 if (APR_SUCCESS != res) { 317 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(01839) 318 "apr_crypto_block_decrypt_finish failed"); 319 continue; 320 } 321 decryptedlen += tlen; 322 decrypted[decryptedlen] = 0; 323 324 break; 325 } 326 327 if (APR_SUCCESS != res) { 328 ap_log_rerror(APLOG_MARK, APLOG_INFO, res, r, APLOGNO(01840) 329 "decryption failed"); 330 } 331 332 return res; 333 334} 335 336/** 337 * Crypto encoding for the session. 338 * 339 * @param r The request pointer. 340 * @param z A pointer to where the session will be written. 341 */ 342static apr_status_t session_crypto_encode(request_rec * r, session_rec * z) 343{ 344 345 char *encoded = NULL; 346 apr_status_t res; 347 const apr_crypto_t *f = NULL; 348 session_crypto_dir_conf *dconf = ap_get_module_config(r->per_dir_config, 349 &session_crypto_module); 350 351 if (dconf->passphrases_set && z->encoded && *z->encoded) { 352 apr_pool_userdata_get((void **)&f, CRYPTO_KEY, r->server->process->pconf); 353 res = encrypt_string(r, f, dconf, z->encoded, &encoded); 354 if (res != OK) { 355 ap_log_rerror(APLOG_MARK, APLOG_DEBUG, res, r, APLOGNO(01841) 356 "encrypt session failed"); 357 return res; 358 } 359 z->encoded = encoded; 360 } 361 362 return OK; 363 364} 365 366/** 367 * Crypto decoding for the session. 368 * 369 * @param r The request pointer. 370 * @param z A pointer to where the session will be written. 371 */ 372static apr_status_t session_crypto_decode(request_rec * r, 373 session_rec * z) 374{ 375 376 char *encoded = NULL; 377 apr_status_t res; 378 const apr_crypto_t *f = NULL; 379 session_crypto_dir_conf *dconf = ap_get_module_config(r->per_dir_config, 380 &session_crypto_module); 381 382 if ((dconf->passphrases_set) && z->encoded && *z->encoded) { 383 apr_pool_userdata_get((void **)&f, CRYPTO_KEY, 384 r->server->process->pconf); 385 res = decrypt_string(r, f, dconf, z->encoded, &encoded); 386 if (res != APR_SUCCESS) { 387 ap_log_rerror(APLOG_MARK, APLOG_ERR, res, r, APLOGNO(01842) 388 "decrypt session failed, wrong passphrase?"); 389 return res; 390 } 391 z->encoded = encoded; 392 } 393 394 return OK; 395 396} 397 398/** 399 * Initialise the SSL in the post_config hook. 400 */ 401static int session_crypto_init(apr_pool_t *p, apr_pool_t *plog, 402 apr_pool_t *ptemp, server_rec *s) 403{ 404 const apr_crypto_driver_t *driver = NULL; 405 apr_crypto_t *f = NULL; 406 407 session_crypto_conf *conf = ap_get_module_config(s->module_config, 408 &session_crypto_module); 409 410 /* session_crypto_init() will be called twice. Don't bother 411 * going through all of the initialization on the first call 412 * because it will just be thrown away.*/ 413 if (ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_CREATE_PRE_CONFIG) { 414 return OK; 415 } 416 417 if (conf->library) { 418 419 const apu_err_t *err = NULL; 420 apr_status_t rv; 421 422 rv = apr_crypto_init(p); 423 if (APR_SUCCESS != rv) { 424 ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(01843) 425 "APR crypto could not be initialised"); 426 return rv; 427 } 428 429 rv = apr_crypto_get_driver(&driver, conf->library, conf->params, &err, p); 430 if (APR_EREINIT == rv) { 431 ap_log_error(APLOG_MARK, APLOG_WARNING, rv, s, APLOGNO(01844) 432 "warning: crypto for '%s' was already initialised, " 433 "using existing configuration", conf->library); 434 rv = APR_SUCCESS; 435 } 436 if (APR_SUCCESS != rv && err) { 437 ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(01845) 438 "The crypto library '%s' could not be loaded: %s (%s: %d)", conf->library, err->msg, err->reason, err->rc); 439 return rv; 440 } 441 if (APR_ENOTIMPL == rv) { 442 ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(01846) 443 "The crypto library '%s' could not be found", 444 conf->library); 445 return rv; 446 } 447 if (APR_SUCCESS != rv || !driver) { 448 ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(01847) 449 "The crypto library '%s' could not be loaded", 450 conf->library); 451 return rv; 452 } 453 454 rv = apr_crypto_make(&f, driver, conf->params, p); 455 if (APR_SUCCESS != rv) { 456 ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(01848) 457 "The crypto library '%s' could not be initialised", 458 conf->library); 459 return rv; 460 } 461 462 ap_log_error(APLOG_MARK, APLOG_INFO, rv, s, APLOGNO(01849) 463 "The crypto library '%s' was loaded successfully", 464 conf->library); 465 466 apr_pool_userdata_set((const void *)f, CRYPTO_KEY, 467 apr_pool_cleanup_null, s->process->pconf); 468 469 } 470 471 return OK; 472} 473 474static void *create_session_crypto_config(apr_pool_t * p, server_rec *s) 475{ 476 session_crypto_conf *new = 477 (session_crypto_conf *) apr_pcalloc(p, sizeof(session_crypto_conf)); 478 479 /* if no library has been configured, set the recommended library 480 * as a sensible default. 481 */ 482#ifdef APU_CRYPTO_RECOMMENDED_DRIVER 483 new->library = APU_CRYPTO_RECOMMENDED_DRIVER; 484#endif 485 486 return (void *) new; 487} 488 489static void *create_session_crypto_dir_config(apr_pool_t * p, char *dummy) 490{ 491 session_crypto_dir_conf *new = 492 (session_crypto_dir_conf *) apr_pcalloc(p, sizeof(session_crypto_dir_conf)); 493 494 new->passphrases = apr_array_make(p, 10, sizeof(char *)); 495 496 /* default cipher AES256-SHA */ 497 new->cipher = "aes256"; 498 499 return (void *) new; 500} 501 502static void *merge_session_crypto_dir_config(apr_pool_t * p, void *basev, void *addv) 503{ 504 session_crypto_dir_conf *new = (session_crypto_dir_conf *) apr_pcalloc(p, sizeof(session_crypto_dir_conf)); 505 session_crypto_dir_conf *add = (session_crypto_dir_conf *) addv; 506 session_crypto_dir_conf *base = (session_crypto_dir_conf *) basev; 507 508 new->passphrases = (add->passphrases_set == 0) ? base->passphrases : add->passphrases; 509 new->passphrases_set = add->passphrases_set || base->passphrases_set; 510 new->cipher = (add->cipher_set == 0) ? base->cipher : add->cipher; 511 new->cipher_set = add->cipher_set || base->cipher_set; 512 513 return new; 514} 515 516static const char *set_crypto_driver(cmd_parms * cmd, void *config, const char *arg) 517{ 518 session_crypto_conf *conf = 519 (session_crypto_conf *)ap_get_module_config(cmd->server->module_config, 520 &session_crypto_module); 521 522 const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY); 523 524 if (err != NULL) { 525 return err; 526 } 527 528 conf->library = ap_getword_conf(cmd->pool, &arg); 529 conf->params = arg; 530 conf->library_set = 1; 531 532 return NULL; 533} 534 535static const char *set_crypto_passphrase(cmd_parms * cmd, void *config, const char *arg) 536{ 537 int arglen = strlen(arg); 538 char **argv; 539 char *result; 540 const char **passphrase; 541 session_crypto_dir_conf *dconf = (session_crypto_dir_conf *) config; 542 543 passphrase = apr_array_push(dconf->passphrases); 544 545 if ((arglen > 5) && strncmp(arg, "exec:", 5) == 0) { 546 if (apr_tokenize_to_argv(arg+5, &argv, cmd->temp_pool) != APR_SUCCESS) { 547 return apr_pstrcat(cmd->pool, 548 "Unable to parse exec arguments from ", 549 arg+5, NULL); 550 } 551 argv[0] = ap_server_root_relative(cmd->temp_pool, argv[0]); 552 553 if (!argv[0]) { 554 return apr_pstrcat(cmd->pool, 555 "Invalid SessionCryptoPassphrase exec location:", 556 arg+5, NULL); 557 } 558 result = ap_get_exec_line(cmd->pool, 559 (const char*)argv[0], (const char * const *)argv); 560 561 if(!result) { 562 return apr_pstrcat(cmd->pool, 563 "Unable to get bind password from exec of ", 564 arg+5, NULL); 565 } 566 *passphrase = result; 567 } 568 else { 569 *passphrase = arg; 570 } 571 572 dconf->passphrases_set = 1; 573 574 return NULL; 575} 576 577static const char *set_crypto_passphrase_file(cmd_parms *cmd, void *config, 578 const char *filename) 579{ 580 char buffer[MAX_STRING_LEN]; 581 char *arg; 582 const char *args; 583 ap_configfile_t *file; 584 apr_status_t rv; 585 586 filename = ap_server_root_relative(cmd->temp_pool, filename); 587 rv = ap_pcfg_openfile(&file, cmd->temp_pool, filename); 588 if (rv != APR_SUCCESS) { 589 return apr_psprintf(cmd->pool, "%s: Could not open file %s: %pm", 590 cmd->cmd->name, filename, &rv); 591 } 592 593 while (!(ap_cfg_getline(buffer, sizeof(buffer), file))) { 594 args = buffer; 595 while (*(arg = ap_getword_conf(cmd->pool, &args)) != '\0') { 596 if (*arg == '#') { 597 break; 598 } 599 set_crypto_passphrase(cmd, config, arg); 600 } 601 } 602 603 ap_cfg_closefile(file); 604 605 return NULL; 606} 607 608static const char *set_crypto_cipher(cmd_parms * cmd, void *config, const char *cipher) 609{ 610 session_crypto_dir_conf *dconf = (session_crypto_dir_conf *) config; 611 612 dconf->cipher = cipher; 613 dconf->cipher_set = 1; 614 615 return NULL; 616} 617 618static const command_rec session_crypto_cmds[] = 619{ 620 AP_INIT_ITERATE("SessionCryptoPassphrase", set_crypto_passphrase, NULL, RSRC_CONF|OR_AUTHCFG, 621 "The passphrase(s) used to encrypt the session. First will be used for encryption, all phrases will be accepted for decryption"), 622 AP_INIT_TAKE1("SessionCryptoPassphraseFile", set_crypto_passphrase_file, NULL, RSRC_CONF|ACCESS_CONF, 623 "File containing passphrase(s) used to encrypt the session, one per line. First will be used for encryption, all phrases will be accepted for decryption"), 624 AP_INIT_TAKE1("SessionCryptoCipher", set_crypto_cipher, NULL, RSRC_CONF|OR_AUTHCFG, 625 "The underlying crypto cipher to use"), 626 AP_INIT_RAW_ARGS("SessionCryptoDriver", set_crypto_driver, NULL, RSRC_CONF, 627 "The underlying crypto library driver to use"), 628 { NULL } 629}; 630 631static void register_hooks(apr_pool_t * p) 632{ 633 ap_hook_session_encode(session_crypto_encode, NULL, NULL, APR_HOOK_LAST); 634 ap_hook_session_decode(session_crypto_decode, NULL, NULL, APR_HOOK_FIRST); 635 ap_hook_post_config(session_crypto_init, NULL, NULL, APR_HOOK_LAST); 636} 637 638AP_DECLARE_MODULE(session_crypto) = 639{ 640 STANDARD20_MODULE_STUFF, 641 create_session_crypto_dir_config, /* dir config creater */ 642 merge_session_crypto_dir_config, /* dir merger --- default is to override */ 643 create_session_crypto_config, /* server config */ 644 NULL, /* merge server config */ 645 session_crypto_cmds, /* command apr_table_t */ 646 register_hooks /* register hooks */ 647}; 648 649#endif 650