1#!/usr/bin/env python 2 3import sys 4import tempfile 5import shutil 6import inspect 7import os 8import logging 9from timeit import default_timer as perf_timer 10from argparse import ArgumentParser 11from cli_common import ( 12 find_utility, 13 run_proc, 14 run_proc_fast, 15 pswd_pipe, 16 rnp_file_path, 17 size_to_readable, 18 raise_err 19) 20 21RNP = '' 22RNPK = '' 23GPG = '' 24WORKDIR = '' 25RNPDIR = '' 26GPGDIR = '' 27RMWORKDIR = False 28SMALL_ITERATIONS = 100 29LARGE_ITERATIONS = 5 30LARGESIZE = 1024*1024*100 31SMALLSIZE = 0 32SMALLFILE = 'smalltest.txt' 33LARGEFILE = 'largetest.txt' 34PASSWORD = 'password' 35 36def setup(workdir): 37 # Searching for rnp and gnupg 38 global RNP, GPG, RNPK, WORKDIR, RNPDIR, GPGDIR, SMALLSIZE, RMWORKDIR 39 logging.basicConfig(stream=sys.stdout, format="%(message)s") 40 logging.getLogger().setLevel(logging.INFO) 41 42 RNP = rnp_file_path('src/rnp/rnp') 43 RNPK = rnp_file_path('src/rnpkeys/rnpkeys') 44 GPG = find_utility('gpg') 45 if workdir: 46 WORKDIR = workdir 47 else: 48 WORKDIR = tempfile.mkdtemp(prefix = 'rnpptmp') 49 RMWORKDIR = True 50 51 logging.debug('Setting up test in {} ...'.format(WORKDIR)) 52 53 # Creating working directory and populating it with test files 54 RNPDIR = os.path.join(WORKDIR, '.rnp') 55 GPGDIR = os.path.join(WORKDIR, '.gpg') 56 os.mkdir(RNPDIR, 0o700) 57 os.mkdir(GPGDIR, 0o700) 58 59 # Generating key 60 pipe = pswd_pipe(PASSWORD) 61 params = ['--homedir', RNPDIR, '--pass-fd', str(pipe), '--userid', 'performance@rnp', 62 '--generate-key'] 63 # Run key generation 64 run_proc(RNPK, params) 65 os.close(pipe) 66 67 68 # Importing keys to GnuPG so it can build trustdb and so on 69 run_proc(GPG, ['--batch', '--passphrase', '', '--homedir', GPGDIR, '--import', 70 os.path.join(RNPDIR, 'pubring.gpg'), os.path.join(RNPDIR, 'secring.gpg')]) 71 72 # Generating small file for tests 73 SMALLSIZE = 3312 74 st = 'lorem ipsum dol ' * (SMALLSIZE//16+1) 75 with open(os.path.join(WORKDIR, SMALLFILE), 'w+') as small_file: 76 small_file.write(st) 77 78 # Generating large file for tests 79 print('Generating large file of size {}'.format(size_to_readable(LARGESIZE))) 80 81 st = '0123456789ABCDEF' * (1024//16) 82 with open(os.path.join(WORKDIR, LARGEFILE), 'w') as fd: 83 for i in range(0, LARGESIZE // 1024): 84 fd.write(st) 85 86def run_iterated(iterations, func, src, dst, *args): 87 runtime = 0 88 89 for i in range(0, iterations): 90 tstart = perf_timer() 91 func(src, dst, *args) 92 runtime += perf_timer() - tstart 93 os.remove(dst) 94 95 res = runtime / iterations 96 #print '{} average run time: {}'.format(func.__name__, res) 97 return res 98 99def rnp_symencrypt_file(src, dst, cipher, zlevel = 6, zalgo = 'zip', armor = False): 100 params = ['--homedir', RNPDIR, '--password', PASSWORD, '--cipher', cipher, 101 '-z', str(zlevel), '--' + zalgo, '-c', src, '--output', dst] 102 if armor: 103 params += ['--armor'] 104 ret = run_proc_fast(RNP, params) 105 if ret != 0: 106 raise_err('rnp symmetric encryption failed') 107 108def rnp_decrypt_file(src, dst): 109 ret = run_proc_fast(RNP, ['--homedir', RNPDIR, '--password', PASSWORD, '--decrypt', src, 110 '--output', dst]) 111 if ret != 0: 112 raise_err('rnp decryption failed') 113 114def gpg_symencrypt_file(src, dst, cipher = 'AES', zlevel = 6, zalgo = 1, armor = False): 115 params = ['--homedir', GPGDIR, '-c', '-z', str(zlevel), '--s2k-count', '524288', 116 '--compress-algo', str(zalgo), '--batch', '--passphrase', PASSWORD, 117 '--cipher-algo', cipher, '--output', dst, src] 118 if armor: 119 params.insert(2, '--armor') 120 ret = run_proc_fast(GPG, params) 121 if ret != 0: 122 raise_err('gpg symmetric encryption failed for cipher ' + cipher) 123 124def gpg_decrypt_file(src, dst, keypass): 125 ret = run_proc_fast(GPG, ['--homedir', GPGDIR, '--pinentry-mode=loopback', '--batch', 126 '--yes', '--passphrase', keypass, '--trust-model', 'always', 127 '-o', dst, '-d', src]) 128 if ret != 0: 129 raise_err('gpg decryption failed') 130 131def print_test_results(fsize, rnptime, gpgtime, operation): 132 if not rnptime or not gpgtime: 133 logging.info('{}:TEST FAILED'.format(operation)) 134 135 if fsize == SMALLSIZE: 136 rnpruns = 1.0 / rnptime 137 gpgruns = 1.0 / gpgtime 138 runstr = '{:.2f} runs/sec vs {:.2f} runs/sec'.format(rnpruns, gpgruns) 139 140 if rnpruns >= gpgruns: 141 percents = (rnpruns - gpgruns) / gpgruns * 100 142 logging.info('{:<30}: RNP is {:>3.0f}% FASTER then GnuPG ({})'.format( 143 operation, percents, runstr)) 144 else: 145 percents = (gpgruns - rnpruns) / gpgruns * 100 146 logging.info('{:<30}: RNP is {:>3.0f}% SLOWER then GnuPG ({})'.format( 147 operation, percents, runstr)) 148 else: 149 rnpspeed = fsize / 1024.0 / 1024.0 / rnptime 150 gpgspeed = fsize / 1024.0 / 1024.0 / gpgtime 151 spdstr = '{:.2f} MB/sec vs {:.2f} MB/sec'.format(rnpspeed, gpgspeed) 152 153 if rnpspeed >= gpgspeed: 154 percents = (rnpspeed - gpgspeed) / gpgspeed * 100 155 logging.info('{:<30}: RNP is {:>3.0f}% FASTER then GnuPG ({})'.format( 156 operation, percents, spdstr)) 157 else: 158 percents = (gpgspeed - rnpspeed) / gpgspeed * 100 159 logging.info('{:<30}: RNP is {:>3.0f}% SLOWER then GnuPG ({})'.format( 160 operation, percents, spdstr)) 161 162def get_file_params(filetype): 163 if filetype == 'small': 164 infile, outfile, iterations, fsize = (SMALLFILE, SMALLFILE + '.gpg', 165 SMALL_ITERATIONS, SMALLSIZE) 166 else: 167 infile, outfile, iterations, fsize = (LARGEFILE, LARGEFILE + '.gpg', 168 LARGE_ITERATIONS, LARGESIZE) 169 170 infile = os.path.join(WORKDIR, infile) 171 rnpout = os.path.join(WORKDIR, outfile + '.rnp') 172 gpgout = os.path.join(WORKDIR, outfile + '.gpg') 173 return (infile, rnpout, gpgout, iterations, fsize) 174 175 176class Benchmark(object): 177 rnphome = ['--homedir', RNPDIR] 178 gpghome = ['--homedir', GPGDIR] 179 180 def small_file_symmetric_encryption(self): 181 # Running each operation iteratively for a small and large file(s), calculating the average 182 # 1. Encryption 183 ''' 184 Small file symmetric encryption 185 ''' 186 infile, rnpout, gpgout, iterations, fsize = get_file_params('small') 187 for armor in [False, True]: 188 tmrnp = run_iterated(iterations, rnp_symencrypt_file, infile, rnpout, 189 'AES128', 0, 'zip', armor) 190 tmgpg = run_iterated(iterations, gpg_symencrypt_file, infile, gpgout, 191 'AES128', 0, 1, armor) 192 testname = 'ENCRYPT-SMALL-{}'.format('ARMOR' if armor else 'BINARY') 193 print_test_results(fsize, tmrnp, tmgpg, testname) 194 195 def large_file_symmetric_encryption(self): 196 ''' 197 Large file symmetric encryption 198 ''' 199 infile, rnpout, gpgout, iterations, fsize = get_file_params('large') 200 for cipher in ['AES128', 'AES192', 'AES256', 'TWOFISH', 'BLOWFISH', 'CAST5', 'CAMELLIA128', 'CAMELLIA192', 'CAMELLIA256']: 201 tmrnp = run_iterated(iterations, rnp_symencrypt_file, infile, rnpout, 202 cipher, 0, 'zip', False) 203 tmgpg = run_iterated(iterations, gpg_symencrypt_file, infile, gpgout, 204 cipher, 0, 1, False) 205 testname = 'ENCRYPT-{}-BINARY'.format(cipher) 206 print_test_results(fsize, tmrnp, tmgpg, testname) 207 208 def large_file_armored_encryption(self): 209 ''' 210 Large file armored encryption 211 ''' 212 infile, rnpout, gpgout, iterations, fsize = get_file_params('large') 213 tmrnp = run_iterated(iterations, rnp_symencrypt_file, infile, rnpout, 214 'AES128', 0, 'zip', True) 215 tmgpg = run_iterated(iterations, gpg_symencrypt_file, infile, gpgout, 'AES128', 0, 1, True) 216 print_test_results(fsize, tmrnp, tmgpg, 'ENCRYPT-LARGE-ARMOR') 217 218 def small_file_symmetric_decryption(self): 219 ''' 220 Small file symmetric decryption 221 ''' 222 infile, rnpout, gpgout, iterations, fsize = get_file_params('small') 223 inenc = infile + '.enc' 224 for armor in [False, True]: 225 gpg_symencrypt_file(infile, inenc, 'AES', 0, 1, armor) 226 tmrnp = run_iterated(iterations, rnp_decrypt_file, inenc, rnpout) 227 tmgpg = run_iterated(iterations, gpg_decrypt_file, inenc, gpgout, PASSWORD) 228 testname = 'DECRYPT-SMALL-{}'.format('ARMOR' if armor else 'BINARY') 229 print_test_results(fsize, tmrnp, tmgpg, testname) 230 os.remove(inenc) 231 232 def large_file_symmetric_decryption(self): 233 ''' 234 Large file symmetric decryption 235 ''' 236 infile, rnpout, gpgout, iterations, fsize = get_file_params('large') 237 inenc = infile + '.enc' 238 for cipher in ['AES128', 'AES192', 'AES256', 'TWOFISH', 'BLOWFISH', 'CAST5', 239 'CAMELLIA128', 'CAMELLIA192', 'CAMELLIA256']: 240 gpg_symencrypt_file(infile, inenc, cipher, 0, 1, False) 241 tmrnp = run_iterated(iterations, rnp_decrypt_file, inenc, rnpout) 242 tmgpg = run_iterated(iterations, gpg_decrypt_file, inenc, gpgout, PASSWORD) 243 testname = 'DECRYPT-{}-BINARY'.format(cipher) 244 print_test_results(fsize, tmrnp, tmgpg, testname) 245 os.remove(inenc) 246 247 def large_file_armored_decryption(self): 248 ''' 249 Large file armored decryption 250 ''' 251 infile, rnpout, gpgout, iterations, fsize = get_file_params('large') 252 inenc = infile + '.enc' 253 gpg_symencrypt_file(infile, inenc, 'AES128', 0, 1, True) 254 tmrnp = run_iterated(iterations, rnp_decrypt_file, inenc, rnpout) 255 tmgpg = run_iterated(iterations, gpg_decrypt_file, inenc, gpgout, PASSWORD) 256 print_test_results(fsize, tmrnp, tmgpg, 'DECRYPT-LARGE-ARMOR') 257 os.remove(inenc) 258 259 # 3. Signing 260 #print '\n#3. Signing\n' 261 # 4. Verification 262 #print '\n#4. Verification\n' 263 # 5. Cleartext signing 264 #print '\n#5. Cleartext signing and verification\n' 265 # 6. Detached signature 266 #print '\n#6. Detached signing and verification\n' 267 268# Usage ./cli_perf.py [working_directory] 269# 270# It's better to use RAMDISK to perform tests 271# in order to speed up disk reads/writes 272# 273# On linux: 274# mkdir -p /tmp/working 275# sudo mount -t tmpfs -o size=512m tmpfs /tmp/working 276# ./cli_perf.py -w /tmp/working 277# sudo umount /tmp/working 278 279 280if __name__ == '__main__': 281 282 # parse options 283 parser = ArgumentParser(description="RNP benchmarking") 284 parser.add_argument("-b", "--bench", dest="benchmarks", 285 help="Name of the comma-separated benchmarks to run", metavar="benchmarks") 286 parser.add_argument("-w", "--workdir", dest="workdir", 287 help="Working directory to use", metavar="workdir") 288 parser.add_argument("-l", "--list", help="Print list of available benchmarks and exit", 289 action="store_true") 290 args = parser.parse_args() 291 292 # get list of benchamrks to run 293 bench_methods = [ x[0] for x in inspect.getmembers(Benchmark, 294 predicate=lambda x: inspect.ismethod(x) or inspect.isfunction(x))] 295 print(bench_methods) 296 297 if args.list: 298 for name in bench_methods: 299 logging.info(("\t " + name)) 300 sys.exit(0) 301 302 if args.benchmarks: 303 bench_methods = filter(lambda x: x in args.benchmarks.split(","), bench_methods) 304 305 # setup operations 306 setup(args.workdir) 307 308 for name in bench_methods: 309 method = getattr(Benchmark, name) 310 logging.info(("\n" + name + "(): " + inspect.getdoc(method))) 311 method(Benchmark()) 312 313 try: 314 shutil.rmtree(WORKDIR) 315 except Exception: 316 logging.info(("Cleanup failed")) 317