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