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