Package Gnumed :: Package pycommon :: Module gmCrypto
[frames] | no frames]

Source Code for Module Gnumed.pycommon.gmCrypto

  1  # -*- coding: utf-8 -*- 
  2   
  3  __doc__ = """GNUmed crypto tools. 
  4   
  5  First and only rule: 
  6   
  7          DO NOT REIMPLEMENT ENCRYPTION 
  8   
  9          Use existing tools. 
 10  """ 
 11  #=========================================================================== 
 12  __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>" 
 13  __license__ = "GPL v2 or later (details at http://www.gnu.org)" 
 14   
 15  # std libs 
 16  import sys 
 17  import os 
 18  import logging 
 19  import tempfile 
 20   
 21   
 22  # GNUmed libs 
 23  if __name__ == '__main__': 
 24          sys.path.insert(0, '../../') 
 25  from Gnumed.pycommon import gmLog2 
 26  from Gnumed.pycommon import gmShellAPI 
 27  from Gnumed.pycommon import gmTools 
 28   
 29   
 30  _log = logging.getLogger('gm.encryption') 
 31   
 32  #=========================================================================== 
 33  # archiving methods 
 34  #--------------------------------------------------------------------------- 
35 -def create_encrypted_zip_archive_from_dir(source_dir, comment=None, overwrite=True, passphrase=None, verbose=False):
36 """Use 7z to create an encrypted ZIP archive of a directory. 37 38 <source_dir> will be included into the archive 39 <comment> included as a file containing the comment 40 <overwrite> remove existing archive before creation, avoiding 41 *updating* of those, and thereby including unintended data 42 <passphrase> minimum length of 5 43 44 The resulting zip archive will always be named 45 "datawrapper.zip" for confidentiality reasons. If callers 46 want another name they will have to shutil.move() the zip 47 file themselves. This archive will be compressed and 48 AES256 encrypted with the given passphrase. Therefore, 49 the result will not decrypt with earlier versions of 50 unzip software. On Windows, 7z oder WinZip are needed. 51 52 The zip format does not support header encryption thereby 53 allowing attackers to gain knowledge of patient details 54 by observing the names of files and directories inside 55 the encrypted archive. 56 57 To reduce that attack surface, GNUmed will create 58 _another_ zip archive inside "datawrapper.zip", which 59 eventually wraps up the patient data as "data.zip". That 60 archive is not compressed and not encrypted, and can thus 61 be unpacked with any old unzipper. 62 63 Note that GNUmed does NOT remember the passphrase for 64 you. You will have to take care of that yourself, and 65 possibly also safely hand over the passphrase to any 66 receivers of the zip archive. 67 """ 68 if len(passphrase) < 5: 69 _log.error('<passphrase> must be at least 5 characters/signs/digits') 70 return None 71 gmLog2.add_word2hide(passphrase) 72 73 source_dir = os.path.abspath(source_dir) 74 if not os.path.isdir(source_dir): 75 _log.error('<source_dir> does not exist or is not a directory: %s', source_dir) 76 return False 77 78 for cmd in ['7z', '7z.exe']: 79 found, binary = gmShellAPI.detect_external_binary(binary = cmd) 80 if found: 81 break 82 if not found: 83 _log.warning('no 7z binary found') 84 return None 85 86 sandbox_dir = gmTools.mk_sandbox_dir() 87 archive_path_inner = os.path.join(sandbox_dir, 'data') 88 if not gmTools.mkdir(archive_path_inner): 89 _log.error('cannot create scratch space for inner achive: %s', archive_path_inner) 90 archive_fname_inner = 'data.zip' 91 archive_name_inner = os.path.join(archive_path_inner, archive_fname_inner) 92 archive_path_outer = gmTools.gmPaths().tmp_dir 93 archive_fname_outer = 'datawrapper.zip' 94 archive_name_outer = os.path.join(archive_path_outer, archive_fname_outer) 95 # remove existing archives so they don't get *updated* rather than newly created 96 if overwrite: 97 if not gmTools.remove_file(archive_name_inner, force = True): 98 _log.error('cannot remove existing archive [%s]', archive_name_inner) 99 return False 100 101 if not gmTools.remove_file(archive_name_outer, force = True): 102 _log.error('cannot remove existing archive [%s]', archive_name_outer) 103 return False 104 105 # 7z does not support ZIP comments so create a text file holding the comment 106 if comment is not None: 107 tmp, fname = os.path.split(source_dir.rstrip(os.sep)) 108 comment_filename = os.path.join(sandbox_dir, '000-%s-comment.txt' % fname) 109 with open(comment_filename, mode = 'wt', encoding = 'utf8', errors = 'replace') as comment_file: 110 comment_file.write(comment) 111 112 # create inner (data) archive: uncompressed, unencrypted, similar to a tar archive 113 args = [ 114 binary, 115 'a', # create archive 116 '-sas', # be smart about archive name extension 117 '-bd', # no progress indicator 118 '-mx0', # no compression (only store files) 119 '-mcu=on', # UTF8 filenames 120 '-l', # store content of links, not links 121 '-scsUTF-8', # console charset 122 '-tzip' # force ZIP format 123 ] 124 if verbose: 125 args.append('-bb3') 126 args.append('-bt') 127 else: 128 args.append('-bb1') 129 args.append(archive_name_inner) 130 args.append(source_dir) 131 if comment is not None: 132 args.append(comment_filename) 133 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) 134 if not success: 135 _log.error('cannot create inner archive') 136 return None 137 138 # create "decompress instructions" file 139 instructions_filename = os.path.join(archive_path_inner, '000-on_Windows-open_with-WinZip_or_7z_tools') 140 open(instructions_filename, mode = 'wt').close() 141 142 # create outer (wrapper) archive: compressed, encrypted 143 args = [ 144 binary, 145 'a', # create archive 146 '-sas', # be smart about archive name extension 147 '-bd', # no progress indicator 148 '-mx9', # best available zip compression ratio 149 '-mcu=on', # UTF8 filenames 150 '-l', # store content of links, not links 151 '-scsUTF-8', # console charset 152 '-tzip', # force ZIP format 153 '-mem=AES256', # force useful encryption 154 '-p%s' % passphrase # set passphrase 155 ] 156 if verbose: 157 args.append('-bb3') 158 args.append('-bt') 159 else: 160 args.append('-bb1') 161 args.append(archive_name_outer) 162 args.append(archive_path_inner) 163 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) 164 if success: 165 return archive_name_outer 166 _log.error('cannot create outer archive') 167 return None
168 169 #---------------------------------------------------------------------------
170 -def create_zip_archive_from_dir(source_dir, archive_name=None, comment=None, overwrite=True, verbose=False):
171 172 source_dir = os.path.abspath(source_dir) 173 if not os.path.isdir(source_dir): 174 _log.error('<source_dir> does not exist or is not a directory: %s', source_dir) 175 return False 176 177 for cmd in ['7z', '7z.exe']: 178 found, binary = gmShellAPI.detect_external_binary(binary = cmd) 179 if found: 180 break 181 if not found: 182 _log.warning('no 7z binary found') 183 return None 184 185 if archive_name is None: 186 # do not assume we can write to "sourcedir/../" 187 archive_path = gmTools.gmPaths().tmp_dir 188 # but do take archive name from source_dir 189 tmp, archive_fname = os.path.split(source_dir.rstrip(os.sep) + '.zip') 190 archive_name = os.path.join(archive_path, archive_fname) 191 # remove any existing archives so they don't get *updated* 192 # rather than newly created 193 if overwrite: 194 if not gmTools.remove_file(archive_name, force = True): 195 _log.error('cannot remove existing archive [%s]', archive_name) 196 return False 197 # 7z does not support ZIP comments so create 198 # a text file holding the comment ... 199 if comment is not None: 200 comment_filename = os.path.abspath(archive_name) + '.comment.txt' 201 if gmTools.remove_file(comment_filename, force = True): 202 with open(comment_filename, mode = 'wt', encoding = 'utf8', errors = 'replace') as comment_file: 203 comment_file.write(comment) 204 else: 205 _log.error('cannot remove existing archive comment file [%s]', comment_filename) 206 comment = None 207 208 # compress 209 args = [ 210 binary, 211 'a', # create archive 212 '-sas', # be smart about archive name extension 213 '-bd', # no progress indicator 214 '-mx9', # best available zip compression ratio 215 '-mcu=on', # UTF8 filenames 216 '-l', # store content of links, not links 217 '-scsUTF-8', # console charset 218 '-tzip' # force ZIP format 219 ] 220 if verbose: 221 args.append('-bb3') 222 args.append('-bt') 223 else: 224 args.append('-bb1') 225 args.append(archive_name) 226 args.append(source_dir) 227 if comment is not None: 228 args.append(comment_filename) 229 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) 230 if comment is not None: 231 gmTools.remove_file(comment_filename) 232 if success: 233 return archive_name 234 235 return None
236 237 #=========================================================================== 238 # file decryption methods 239 #---------------------------------------------------------------------------
240 -def gpg_decrypt_file(filename=None, passphrase=None, verbose=False, target_ext=None):
241 assert (filename is not None), '<filename> must not be None' 242 243 _log.debug('attempting GPG decryption') 244 for cmd in ['gpg2', 'gpg', 'gpg2.exe', 'gpg.exe']: 245 found, binary = gmShellAPI.detect_external_binary(binary = cmd) 246 if found: 247 break 248 if not found: 249 _log.warning('no gpg binary found') 250 return None 251 252 basename = os.path.splitext(filename)[0] 253 filename_decrypted = gmTools.get_unique_filename(prefix = '%s-decrypted-' % basename, suffix = target_ext) 254 args = [ 255 binary, 256 '--utf8-strings', 257 '--display-charset', 'utf-8', 258 '--batch', 259 '--no-greeting', 260 '--enable-progress-filter', 261 '--decrypt', 262 '--output', filename_decrypted 263 ##'--use-embedded-filename' # not all encrypted files carry a filename 264 ] 265 if verbose: 266 args.extend ([ 267 '--verbose', '--verbose', 268 '--debug-level', '8', 269 '--debug', 'packet,mpi,crypto,filter,iobuf,memory,cache,memstat,trust,hashing,clock,lookup,extprog' 270 ##'--debug-all', # will log passphrase 271 ##'--debug, 'ipc', # will log passphrase 272 ##'--debug-level', 'guru', # will log passphrase 273 ##'--debug-level', '9', # will log passphrase 274 ]) 275 args.append(filename) 276 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, verbose = verbose, encoding = 'utf-8') 277 if success: 278 return filename_decrypted 279 return None
280 281 #=========================================================================== 282 # file encryption methods 283 #---------------------------------------------------------------------------
284 -def gpg_encrypt_file_symmetric(filename=None, comment=None, verbose=False, passphrase=None, remove_unencrypted=False):
285 286 #add short decr instr to comment 287 assert (filename is not None), '<filename> must not be None' 288 289 _log.debug('attempting symmetric GPG encryption') 290 for cmd in ['gpg2', 'gpg', 'gpg2.exe', 'gpg.exe']: 291 found, binary = gmShellAPI.detect_external_binary(binary = cmd) 292 if found: 293 break 294 if not found: 295 _log.warning('no gpg binary found') 296 return None 297 filename_encrypted = filename + '.asc' 298 args = [ 299 binary, 300 '--utf8-strings', 301 '--display-charset', 'utf-8', 302 '--batch', 303 '--no-greeting', 304 '--enable-progress-filter', 305 '--symmetric', 306 '--cipher-algo', 'AES256', 307 '--armor', 308 '--output', filename_encrypted 309 ] 310 if comment is not None: 311 args.extend(['--comment', comment]) 312 if verbose: 313 args.extend ([ 314 '--verbose', '--verbose', 315 '--debug-level', '8', 316 '--debug', 'packet,mpi,crypto,filter,iobuf,memory,cache,memstat,trust,hashing,clock,lookup,extprog', 317 ##'--debug-all', # will log passphrase 318 ##'--debug, 'ipc', # will log passphrase 319 ##'--debug-level', 'guru', # will log passphrase 320 ##'--debug-level', '9', # will log passphrase 321 ]) 322 pwd_fname = None 323 if passphrase is not None: 324 pwd_file = tempfile.NamedTemporaryFile(mode = 'w+t', encoding = 'utf8', delete = False) 325 pwd_fname = pwd_file.name 326 args.extend ([ 327 '--pinentry-mode', 'loopback', 328 '--passphrase-file', pwd_fname 329 ]) 330 pwd_file.write(passphrase) 331 pwd_file.close() 332 args.append(filename) 333 try: 334 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, verbose = verbose, encoding = 'utf-8') 335 finally: 336 if pwd_fname is not None: 337 os.remove(pwd_fname) 338 if not success: 339 return None 340 if not remove_unencrypted: 341 return filename_encrypted 342 if gmTools.remove_file(filename): 343 return filename_encrypted 344 gmTools.remove_file(filename_encrypted) 345 return None
346 347 #---------------------------------------------------------------------------
348 -def aes_encrypt_file(filename=None, passphrase=None, comment=None, verbose=False, remove_unencrypted=False):
349 assert (filename is not None), '<filename> must not be None' 350 assert (passphrase is not None), '<passphrase> must not be None' 351 352 if len(passphrase) < 5: 353 _log.error('<passphrase> must be at least 5 characters/signs/digits') 354 return None 355 gmLog2.add_word2hide(passphrase) 356 357 #add 7z/winzip url to comment.txt 358 _log.debug('attempting 7z AES encryption') 359 for cmd in ['7z', '7z.exe']: 360 found, binary = gmShellAPI.detect_external_binary(binary = cmd) 361 if found: 362 break 363 if not found: 364 _log.warning('no 7z binary found, trying gpg') 365 return None 366 367 if comment is not None: 368 archive_path, archive_name = os.path.split(os.path.abspath(filename)) 369 comment_filename = gmTools.get_unique_filename ( 370 prefix = '%s.7z.comment-' % archive_name, 371 tmp_dir = archive_path, 372 suffix = '.txt' 373 ) 374 with open(comment_filename, mode = 'wt', encoding = 'utf8', errors = 'replace') as comment_file: 375 comment_file.write(comment) 376 else: 377 comment_filename = '' 378 filename_encrypted = '%s.7z' % filename 379 args = [binary, 'a', '-bb3', '-mx0', "-p%s" % passphrase, filename_encrypted, filename, comment_filename] 380 encrypted, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) 381 gmTools.remove_file(comment_filename) 382 if not encrypted: 383 return None 384 if not remove_unencrypted: 385 return filename_encrypted 386 if gmTools.remove_file(filename): 387 return filename_encrypted 388 gmTools.remove_file(filename_encrypted) 389 return None
390 391 #---------------------------------------------------------------------------
392 -def encrypt_pdf(filename=None, passphrase=None, verbose=False):
393 assert (filename is not None), '<filename> must not be None' 394 assert (passphrase is not None), '<passphrase> must not be None' 395 396 if len(passphrase) < 5: 397 _log.error('<passphrase> must be at least 5 characters/signs/digits') 398 return None 399 gmLog2.add_word2hide(passphrase) 400 401 _log.debug('attempting PDF encryption') 402 for cmd in ['qpdf', 'qpdf.exe']: 403 found, binary = gmShellAPI.detect_external_binary(binary = cmd) 404 if found: 405 break 406 if not found: 407 _log.warning('no qpdf binary found') 408 return None 409 410 filename_encrypted = '%s.encrypted.pdf' % os.path.splitext(filename)[0] 411 args = [ 412 binary, 413 '--verbose', 414 '--encrypt', passphrase, '', '128', 415 '--print=full', '--modify=none', '--extract=n', 416 '--use-aes=y', 417 '--', 418 filename, 419 filename_encrypted 420 ] 421 success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) 422 if success: 423 return filename_encrypted 424 return None
425 426 #---------------------------------------------------------------------------
427 -def encrypt_file_symmetric(filename=None, passphrase=None, comment=None, verbose=False, remove_unencrypted=False):
428 assert (filename is not None), '<filename> must not be None' 429 430 # pdf ? 431 enc_filename = encrypt_pdf(filename = filename, passphrase = passphrase, verbose = verbose) 432 if enc_filename is not None: 433 return enc_filename 434 # try 7z based AES 435 enc_filename = aes_encrypt_file(filename = filename, passphrase = passphrase, comment = comment, verbose = verbose, remove_unencrypted = remove_unencrypted) 436 if enc_filename is not None: 437 return enc_filename 438 # try GPG based 439 return gpg_encrypt_file_symmetric(filename = filename, passphrase = passphrase, comment = comment, verbose = verbose, remove_unencrypted = remove_unencrypted)
440 441 #---------------------------------------------------------------------------
442 -def encrypt_file(filename=None, receiver_key_ids=None, passphrase=None, comment=None, verbose=False, remove_unencrypted=False):
443 assert (filename is not None), '<filename> must not be None' 444 445 # cannot do asymmetric 446 if receiver_key_ids is None: 447 _log.debug('no receiver key IDs: cannot try asymmetric encryption') 448 return encrypt_file_symmetric(filename = filename, passphrase = passphrase, comment = comment, verbose = verbose, remove_unencrypted = remove_unencrypted) 449 450 # asymmetric not implemented yet 451 return None
452 453 #---------------------------------------------------------------------------
454 -def encrypt_directory_content(directory=None, receiver_key_ids=None, passphrase=None, comment=None, verbose=False, remove_unencrypted=True):
455 assert (directory is not None), 'source <directory> must not be None' 456 _log.debug('encrypting content of [%s]', directory) 457 try: 458 items = os.listdir(directory) 459 except OSError: 460 return False 461 462 for item in items: 463 full_item = os.path.join(directory, item) 464 if os.path.isdir(full_item): 465 subdir_encrypted = encrypt_directory_content ( 466 directory = full_item, 467 receiver_key_ids = receiver_key_ids, 468 passphrase = passphrase, 469 comment = comment, 470 verbose = verbose 471 ) 472 if subdir_encrypted is False: 473 return False 474 continue 475 476 fname_encrypted = encrypt_file ( 477 filename = full_item, 478 receiver_key_ids = receiver_key_ids, 479 passphrase = passphrase, 480 comment = comment, 481 verbose = verbose, 482 remove_unencrypted = remove_unencrypted 483 ) 484 if fname_encrypted is None: 485 return False 486 487 return True
488 489 #=========================================================================== 490 # main 491 #--------------------------------------------------------------------------- 492 if __name__ == '__main__': 493 494 if len(sys.argv) < 2: 495 sys.exit() 496 497 if sys.argv[1] != 'test': 498 sys.exit() 499 500 # for testing: 501 logging.basicConfig(level = logging.DEBUG) 502 from Gnumed.pycommon import gmI18N 503 gmI18N.activate_locale() 504 gmI18N.install_domain() 505 506 #-----------------------------------------------------------------------
507 - def test_gpg_decrypt():
508 print(gpg_decrypt_file(filename = sys.argv[2], verbose = True))
509 510 #-----------------------------------------------------------------------
511 - def test_gpg_encrypt_symmetric():
512 print(gpg_encrypt_file_symmetric(filename = sys.argv[2], passphrase = sys.argv[3], verbose = True, comment = 'GNUmed testing'))
513 514 #-----------------------------------------------------------------------
515 - def test_aes_encrypt():
516 print(aes_encrypt_file(filename = sys.argv[2], passphrase = sys.argv[3], comment = sys.argv[4], verbose = True))
517 518 #-----------------------------------------------------------------------
519 - def test_encrypt_pdf():
520 print(encrypt_pdf(filename = sys.argv[2], passphrase = sys.argv[3], verbose = True))
521 522 #-----------------------------------------------------------------------
523 - def test_encrypt_file():
524 print(encrypt_file(filename = sys.argv[2], passphrase = sys.argv[3], verbose = True))
525 526 #-----------------------------------------------------------------------
527 - def test_zip_archive_from_dir():
528 print(create_zip_archive_from_dir ( 529 sys.argv[2], 530 #archive_name=None, 531 comment = 'GNUmed test archive', 532 overwrite = True, 533 verbose = True 534 ))
535 536 #-----------------------------------------------------------------------
537 - def test_encrypted_zip_archive_from_dir():
538 print(create_encrypted_zip_archive_from_dir ( 539 sys.argv[2], 540 comment = 'GNUmed test archive', 541 overwrite = True, 542 passphrase = sys.argv[3], 543 verbose = True 544 ))
545 546 #----------------------------------------------------------------------- 547 # encryption 548 #test_aes_encrypt() 549 test_encrypt_pdf() 550 #test_gpg_encrypt_symmetric() 551 #test_encrypt_file() 552 553 # decryption 554 #test_gpg_decrypt() 555 556 #test_zip_archive_from_dir() 557 #test_encrypted_zip_archive_from_dir() 558