-
Notifications
You must be signed in to change notification settings - Fork 7
/
optestlib.py
630 lines (492 loc) · 17.4 KB
/
optestlib.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
#
# David Schuetz
# November 2018
#
# https://github.com/dschuetz/1password
#
# Library of functions called by all the other tools here.
#
# Not exactly a "clean" library -- many have debugging functions built
# in that make them very noisy. And there are certainly inconsistencies
# between functions regarding debug output, variable naming, calling
# conventions, style, and just general quality.
#
from Cryptodome.Random import get_random_bytes
from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import HKDF
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_OAEP
from Cryptodome.Hash import SHA256, SHA512
from Cryptodome.Util.Padding import pad, unpad
from jwkest.jwk import RSAKey, load_jwks
from jwkest.jwe import JWE
from Crypto.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_OAEP
import hashlib,hmac
import sys, base64, binascii, re, json, struct, termios, tty
DEBUG = 1
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# basic crypto stuff - wrappers around PyCryptoDome, etc.
#
# * encrypt/decrypt AES-GCM with 128-bit GCM tag
# * encrypt/decrypt AES-CBC with HMAC-SHA256 tag
# * encrypt/decrypt 1Password "opdata" structure
# * AES-CBC with HS-256 tag
#
# All use 256-bit keys
#
# Should probably pull RSA stuff out of the other scripts
# and add them here.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Encrypt PT with AES-GCM using key and iv
# * If iv not provided, one will be created
# * Returns IV, and Ciphertext with GCM tag appended
# * Length of GCM tag hard-coded to 16 bytes
#
def enc_aes_gcm(pt, key, iv=None):
if iv == None:
iv = get_random_bytes(16)
C = AES.new(key, AES.MODE_GCM, iv, mac_len=16)
CT, tag = C.encrypt_and_digest(pt)
return iv, CT+tag
#
# Decrypt CT with AES-GCM using key and iv
# * If iv not provided, one will be created
# * Verifies GCM tag
# - if verification fails, program will terminate with error
# * Length of GCM tag hard-coded to 16 bytes
#
def dec_aes_gcm(ct, key, iv, tag):
C = AES.new(key, AES.MODE_GCM, iv, mac_len=16)
PT = C.decrypt_and_verify(ct, tag)
return PT
#
# Encrypt plaintext with AES-CBC using given key and iv
# * if iv not provided, one will be created
# * Pads plaintext to 16-byte boundary if necessary
# * computes HMAC-SHA256 tag using hmac_key
# - computes across IV + ciphertext
# - appends tag to ciphertext
#
# Returns:
# * iv + ciphertext + tag
#
def encrypt_tag_cbc(pt, iv, key, hmac_key):
if iv == None:
iv = get_random_bytes(16)
pt = pad(pt, 16)
C = AES.new(key, AES.MODE_CBC, iv)
ct = C.encrypt(pt)
hm = hmac.new(hmac_key, iv+ct, digestmod=hashlib.sha256)
ht = hm.digest()
out = iv + ct + ht
return out
#
# Decrypt ciphertext with AES-CBC using given
# * takes structure of IV + CT + HMAC-Tag
# * first computes HMAC-SHA256 of "IV+CT"
# * if doesn't match provided tag, terminates program with error
# * otherwise, decrypts using AES-CBC
# * removes any padding to 16-byte boundary
#
# Returns plaintext
#
def decrypt_verify_cbc(iv_ct_tag, key, hmac_key):
msg = iv_ct_tag[0:-32]
iv = iv_ct_tag[0:16]
ct = iv_ct_tag[16:-32]
tag = iv_ct_tag[-32:]
hm = hmac.new(hmac_key, msg, digestmod=hashlib.sha256)
ht = hm.digest()
if ht != tag:
print("HMAC tag doesn't match!")
sys.exit(1)
C = AES.new(key, AES.MODE_CBC, iv)
PT = C.decrypt(ct)
PT = unpad(PT, 16)
return PT
#
# Encrypts data into an opdata format structure
# * Takes 256-bit encryption key and HMAC key
# * If iv not provided, one will be generated at random
# * If padding is not provided, padding will be generated at random
# * encrypts padded payload with AES-CBC
# * Computes HMAC-SHA256 authentication tag:
# - Header + IV + Padding + Plaintext
#
# Returns binary opdata structure:
# * header (opdata01 + payload length)
# * IV
# * Ciphertext
# * HMAC tag
#
def encrypt_opdata(payload, enc_key, hmac_key, iv=None, padding=None):
p_debug('\n** Encrypting opdata01 structure')
header = 'opdata01' + struct.pack('<Q', len(payload))
p_str('PT length', len(payload))
if padding == None:
pad_len = 16 - (len(payload) % 16)
padding = get_random_bytes(pad_len)
p_data('Padded plaintext', padding + payload)
if iv == None:
iv = get_random_bytes(16)
p_data('AES-CBC Key', enc_key, dump=False)
C = AES.new(enc_key, AES.MODE_CBC, iv)
CT = C.encrypt(padding + payload)
p_data('Header', header)
p_data('IV', iv, dump=False)
p_data('Ciphertext', CT, dump=False)
msg = header + iv + CT
hm = hmac.new(hmac_key, msg, digestmod=hashlib.sha256)
ht = hm.digest()
p_data('HMAC-SHA256 Key', hmac_key, dump=False)
p_data('Computed HMAC', ht, dump=False)
msg += ht
p_data('Final opdata', msg)
p_debug('\n')
return msg
#
# Decrypts data from an opdata format structure
# * Takes opdata structure, 256-bit encryption key and HMAC key
# * Extracts payload length and IV
# * Computes HMAC-SHA256 digest across entire structure
# - header + iv + ciphertext
# * If computed tag doesn't match tag in structure, exits with error
# * Otherwise, decrypts ciphertext using provided key and AES-CBC
#
# Returns decrypted plaintext with padding removed
#
def decrypt_opdata(opdata, enc_key, hmac_key):
p_debug('\n** Decrypting OPDATA structure')
p_data('raw opdata', opdata)
if opdata[0:8] != 'opdata01':
print "ERROR - opdata01 block missing 'opdata01' header. Quitting."
sys.exit(1)
p_data('Header', opdata[0:8], opdata[0:8])
pt_len = struct.unpack('<Q', opdata[8:16])[0]
p_data('PT length', opdata[8:16], pt_len)
op_header = opdata[0:16]
iv = opdata[16:32]
p_data('IV', iv)
ct = opdata[32:-32] # header + iv: 32 bytes; trailing HMAC tag: 32 bytes
p_data('CT', ct)
p_debug('CT length (padded): %d' % (len(ct)))
ht = opdata[-32:]
p_data('HMAC digest', ht)
p_debug("\nVerifying HMAC tag")
p_data('OPdata Msg', opdata[0:-32]) # don't HMAC the provided HMAC tag
p_data('HMAC key', hmac_key)
hm = hmac.new(hmac_key, opdata[0:-32], digestmod=hashlib.sha256)
p_data('Computed HMAC', hm.digest())
if hm.digest() != ht:
print("ERROR - Computed HMAC does not match provided value.")
sys.exit(1)
else:
print("HMAC signature verified.")
C = AES.new(enc_key, AES.MODE_CBC, iv)
PT = C.decrypt(ct)
start_at = len(ct) - pt_len
PT=PT[start_at:] # first x bytes are random padding
p_debug("\n\n")
p_debug("*** decrypted opdata")
p_data('Plaintext', PT)
p_debug('\n')
return PT
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# 1Password specific functionality
#
# * Compute 2SKD for generating MUK and SRP-X authenticator
# * Decrypt Windows EMK data
# * Generate and decode keys for local private vaults
#
# Some of these really don't ever get used except by a single
# demonstration script. The line between a useful library
# and just a convenient place to shove things is a little
# blurry here. Whatever. :)
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Implements the Two-Secret Key Derivation process (2SKD)
# Takes the user's:
# * Secret Key (Account Key)
# * Master Password
# * Salt (p2salt)
# * Iterations count (p2c)
# * Algorithm (PBES2g-HS256, SRPg-4096)
#
# Returns the result (used for either MUK or SRP-X)
#
def compute_2skd(sk, password, email, p2salt, iterations, algorithm):
p_debug("** Computing 2SKD\n")
version = sk[0:2]
account_id = sk[3:9]
secret = re.sub('-', '', sk[10:])
email = email.lower() # simple hack...not doing "proper" normalizaiton...
email = str.encode(str(email))
version = str.encode(str(version))
secret = str.encode(str(secret))
account_id = str.encode(str(account_id))
algorithm = str.encode(str(algorithm))
p_str('Password', password)
p_str('Email', email)
p_str('Secret Key', sk)
p_str(' Version', version)
p_str(' AcctID', account_id)
p_str(' Secret', secret)
p_str('Algorithm', algorithm)
p_str('Iterations (p2c)', iterations)
p_str('Salt (p2s)', opb64e(p2salt))
p_data('Salt (decoded)', p2salt, dump=False)
hkdf_pass_salt = HKDF(p2salt, 32, email, SHA256, 1, algorithm)
p_debug('\nHKDF(ikm=p2s, len=32, salt=email, hash=SHA256, count=1, info=algorithm)')
p_data('HKDF out: pass salt', hkdf_pass_salt, dump=False)
password = str.encode(str(password))
password_key = hashlib.pbkdf2_hmac('sha256', password, hkdf_pass_salt, iterations, dklen=32)
p_debug('\nPBKDF2(sha256, password, salt=HKDF_salt, iterations=p2c, 32 bytes)')
p_data('Derived password key', password_key, dump=False)
p_debug('\nHKDF(ikm=secret, len=32, salt=AcctID, hash=SHA256, count=1, info=version)')
hkdf_key = HKDF(secret, 32, account_id, SHA256, 1, 'A3')
p_data('HKDF out: secret key', hkdf_key, dump=False)
final_key = ''
for x in range(0,32):
a = ord(password_key[x])
b = ord(hkdf_key[x])
c = a^b
final_key = final_key + chr(c)
p_debug('\nXOR PBKDF2 output and SecretKey HKDF output')
p_data('Final 2SKD out', final_key, dump=False)
return final_key
#
# Decrypts the given Windows Encrypted Master Key (EMK) structure,
# using the provided Master Password.
#
def decrypt_emk(bin_emk, password):
print "*** EMK Structure from DB\n"
p_data('RAW HEX', bin_emk)
print "\n\n"
iterations = struct.unpack('<I', bin_emk[0:4])[0]
p_data('Iterations', bin_emk[0:4], iterations)
salt_len = struct.unpack('<I', bin_emk[4:8])[0]
if salt_len != 16:
print "huh. haven't seen a salt length of %d before. quitting." % salt_len
sys.exit(1)
salt = bin_emk[8:24]
p_data('Salt len', bin_emk[4:8], salt_len)
p_data('Salt', bin_emk[8:24])
raw_key = hashlib.pbkdf2_hmac('sha512', password, salt, iterations, dklen=64)
print "*** Encryption Key and HMAC Key, derived from password:"
p_data('Raw derived key', raw_key)
emk_enc_key = raw_key[0:32]
emk_hmac_key = raw_key[32:64]
p_data("Derived enc key",emk_enc_key)
p_data("Derived hmac key",emk_hmac_key)
opdata = bin_emk[28:]
dec_data = decrypt_opdata(opdata, emk_enc_key, emk_hmac_key)
return dec_data
#
# Generate and encrypt OnePassword key data for local private vaults
# Requires:
# * User's Master Password
# * A salt for the password derivation process
# * Random data, IV, and Padding for both overview and master keys
#
# Computes, and returns a structure of:
# * private vault master key
# * master hmac_key
# * overview key
# * overview hmac_key
# * encrypted master_key_data
# * encrpted overview_key_data
#
# will probably fail unpredictably if any of the required parameters
# are missing
#
def gen_local_vault_keys(password, salt, mk_d, mk_iv, mk_p, ok_d, ok_iv,ok_p):
p_debug('\n** Generating MasterKey (MK) and OverviewKey (OK) profile info')
iter = 100000
p_data('Salt', salt, dump=False)
raw_key = hashlib.pbkdf2_hmac('sha512', password, salt, iter, dklen=64)
op_kek_mk = raw_key[0:32]
op_kek_hmac = raw_key[32:64]
p_data('Derived key', op_kek_mk)
p_data('Derived HMAC key', op_kek_hmac)
enc_master_key = encrypt_opdata(mk_d, op_kek_mk, op_kek_hmac,
iv=mk_iv, padding=mk_p)
h_raw = SHA512.new(mk_d).digest()
op_mk = h_raw[0:32]
op_mk_hmac = h_raw[32:64]
p_data('Priv vault MK', op_mk)
p_data('Priv vault MK HMAC', op_mk_hmac)
enc_overview_key = encrypt_opdata(ok_d, op_kek_mk, op_kek_hmac,
iv=ok_iv, padding=ok_p)
h_raw = SHA512.new(ok_d).digest()
op_ok = h_raw[0:32]
op_ok_hmac = h_raw[32:64]
p_data('Priv vault OK', op_ok)
p_data('Priv vault OK HMAC', op_ok_hmac)
out = {'master_key': op_mk, 'master_key_hmac': op_mk_hmac,
'overview_key': op_ok, 'overview_key_hmac': op_ok_hmac,
'enc_master_key_data': enc_master_key,
'enc_overview_key_data': enc_overview_key}
return out
#
# Given the user's master password, information from the
# local vault "profiles" table (salt, encrypted master key data
# and overview key data), generates the master and overview
# encryption and hmac keys.
#
def get_local_vault_keys(password, salt, e_mk_d, e_ok_d):
iter = 100000
p_data('Salt', salt, dump=False)
raw_key = hashlib.pbkdf2_hmac('sha512', password, salt, iter, dklen=64)
op_kek_mk = raw_key[0:32]
op_kek_hmac = raw_key[32:64]
p_data('Derived key', op_kek_mk)
p_data('Derived HMAC key', op_kek_hmac)
master_key_data = decrypt_opdata(e_mk_d, op_kek_mk, op_kek_hmac)
h_raw = SHA512.new(master_key_data).digest()
op_mk = h_raw[0:32]
op_mk_hmac = h_raw[32:64]
p_data('Priv vault MK', op_mk)
p_data('Priv vault MK HMAC', op_mk_hmac)
overview_key_data = decrypt_opdata(e_ok_d, op_kek_mk, op_kek_hmac)
h_raw = SHA512.new(overview_key_data).digest()
op_ok = h_raw[0:32]
op_ok_hmac = h_raw[32:64]
p_data('Priv vault OK', op_ok)
p_data('Priv vault OK HMAC', op_ok_hmac)
return op_mk, op_mk_hmac, op_ok, op_ok_hmac
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# basic debug / output stuff for consistent output
#
# most reformat the data into "<title> <data>" format
# and then send to p_debug which decides whether or not to
# actually display the data
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# just prints the string, if DEBUG is true
#
def p_debug(out):
if DEBUG:
print out
#
# formats title and data into a left-justified 20-char
# space for the title, then string
def p_str(title, data):
dat_str = '%s' % data
lines = dat_str.split('\n')
p_debug('%-20s %s' % (title, lines[0]))
for l in lines[1:]:
p_debug('%20s %s' % ('', l))
#
# takes a hex string and formats an old-school DEBUG-like
# dump of hex + ascii
#
def dump_line(dat):
l_raw = binascii.a2b_hex(re.sub(' ', '', dat))
asc = ''
for c in l_raw:
if ord(c) < 31 or ord(c) > 127:
asc += '.'
else:
asc += c
return('%-40s %s' % (dat, asc))
###############################################################
## TKTK - Need to fix this, drops singleton bytes from last line of hex dump
## first re.sub seems to be the problem. just iterate and space.
###############################################################
def p_data(title, raw, decoded='', dump=True):
print ""
hex = re.sub(r'(....)', r'\1 ', binascii.b2a_hex(raw))
hex_lines = re.sub(r'((.... ){1,8})', r'\1\n', hex).split('\n')
if decoded != '' or dump == False:
p_debug('%-20s %-40s %s' % (title, hex_lines[0], decoded))
for l in hex_lines[1:-1]:
p_debug('%-20s %-40s' % ('', l))
else:
d_dat = dump_line(hex_lines[0])
p_debug('%-20s %s' % (title, d_dat))
for l in hex_lines[1:-1]:
d_dat = dump_line(l)
p_debug('%-20s %s' % ('', d_dat))
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Convenience functions for input/output
#
# * getbinary - prompt user for binary data (b64 or hex)
# * opb64d, opb64e - base64 decode with 1Password tricks
# (URL safe altchars, not always including == padding, etc.)
#
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# The strings stored by 1Password don't always have padding characters at the
# end. So we try multiple times until we get a good result.
#
# Also, 1Password uses url-safe encoding with - and _ replacing + and /.
#
def opb64d(b64dat):
try:
out = base64.b64decode(b64dat, altchars='-_')
except:
try:
out = base64.b64decode(b64dat + '=', altchars='-_')
except:
try:
out = base64.b64decode(b64dat + '==', altchars='-_')
except:
print "Problem b64 decoding string: %s" % (b64dat)
sys.exit(1)
return out
#
# Simple - encode something in base64 but use URL-safe
# alt chars - and _ instead of + and /
#
def opb64e(dat):
return base64.b64encode(dat, altchars='-_')
#
# Collects binary data from the user via a terminal prompt
#
# Because on some systems the raw_input can hang after like
# 1024 characters, we have to wrap it in some crazy termios
# stuff.
#
# Then, try to decode it. First assume it's hex, then try
# base64, both using 1Password tricks, then just plain
# vanilla base64.
#
# Not exactly bulletproof. (like, abcd1234 is both a hex
# string and a perfectly acceptable Base-64 encoding.)
# But for what we're doing (binary encodings of random
# keys, IVs, and ciphertexts), it's incredibly unlikely
# that any base-64 string would present as valid hex,
# etc.
#
# See also all my previous warnings about using any of
# tnis code for something that actually matters.
#
def get_binary(prompt):
old_tty_attr = termios.tcgetattr(sys.stdin)
new_tty_attr = old_tty_attr[:]
new_tty_attr[3] = new_tty_attr[3] & ~( tty.ICANON)
termios.tcsetattr(sys.stdin, tty.TCSANOW, new_tty_attr)
raw_dat = raw_input(prompt)
termios.tcsetattr(sys.stdin, tty.TCSANOW, old_tty_attr)
try:
bin = binascii.a2b_hex(raw_dat)
except:
try:
bin = opb64d(raw_dat)
except:
try:
bin = base64.b64decode(raw_dat)
except:
print "Unable to decode the input. Enter in hex or base64."
sys.exit(1)
return bin