summaryrefslogtreecommitdiffstats
path: root/decode-qr-uri.py
blob: 1f0489de619c24679ba8094304f367dd1241bbd7 (plain) (blame)
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
#!/bin/env python3
import urllib.parse
import hmac
import hashlib
from hashlib import pbkdf2_hmac
import base64
import argparse
import logging
from Crypto.Cipher import AES

logging.basicConfig(level=logging.WARNING)

parser = argparse.ArgumentParser(description='Decrypt the encrypted data from an Entrust IdentityGuard QR code')
parser.add_argument('URI', type=str, nargs=1, help='Example: igmobileotp://?action=secactivate&enc=VRUq6IoLWQRCMRITZEHtHUSWJiPwgu%2FN1BFyUHE5kxuHIEYoE3zmNTrAHeeUM5S3gzCnTy%2F%2Bdnbu%2FsjjQW%2BNEISx8C4ra8rLpxOl8E8w4KXHgjeBRgdvSzl%2BbzX5RYRrQlWgK8hsBT4pQYE0eFgW2TmRbzXu1Mu7XjKDcwsJLew32jQC2qyPLP8hljnv2rHwwsMfhQwgJUJYfctwLWWEDUFukEckaZ4O&v=1&mac=mhVL8BWKaishMa5%2B'.replace("%", "%%"))
parser.add_argument('Password', type=str, nargs=1, help='The password given with the QR code. Example: 54998317')

args = parser.parse_args()

# Parse URL
o = urllib.parse.urlparse(args.URI[0])

# Validate scheme
if o.scheme != 'igmobileotp':
    logging.warning("Only the scheme igmobileotp is currently supported")

logging.info("Scheme: %s", o.scheme)

# Parse query string
query = urllib.parse.parse_qs(o.query)

# Validate action
try:
    if query['action'][0] != 'secactivate':
        logging.warning("Only the secactivate action is currently supported")
    logging.info("Action: %s", query['action'][0])
except:
    logging.warning("No action was found in the URI. Are you sure this is from a valid QR code?")

# Validate some encrypted data actually exists
enc = False
try:
    enc = query['enc'][0]
except:
    raise Exception('An "enc" parameter is a required part of the URI')

# Decode the enc parameter from base64
try:
    enc = base64.b64decode(enc, validate=True)
except:
    raise Exception('Could not decode base64 from enc paramater')

# Get the salt from enc
kdfSalt = enc[0:8]

logging.debug("KDF Salt: 0x%s", kdfSalt.hex())

# Run PBKDF2 to obtain our AES key
key = pbkdf2_hmac(
    hash_name='sha256',
    password=args.Password[0].encode('utf-8'),
    salt=kdfSalt,
    iterations=1000,
    dklen=64
)

logging.debug("KDF Output: 0x%s", key.hex())

# Validate whether our key is correct using the provided MAC
# The MAC'd payload does not include the MAC itself
macedPayload = o.query[0:o.query.rfind('&')] # mac is last param, so can remove it this way

hmacKey = key[16:48]
logging.debug("HMAC Key: 0x%s", hmacKey.hex())

hmacer = hmac.new(hmacKey, digestmod=hashlib.sha256)
hmacer.update(macedPayload.encode('utf-8'))
hmacDigest = hmacer.digest()

logging.info('HMAC Digest: 0x%s', hmacDigest.hex())

try:
    mac = query['mac'][0]
    if base64.b64decode(mac) != hmacDigest[0:12]:
        logging.warning("Falied to validate HMAC. Are you use this passcode is correct?")
except:
    logging.warning("No MAC was provided in URI. Cannot verify if key is correct")

# Remove the KDF salt from the encrypted data
encdata = enc[8:]

# Get our parameters required for decryption
iv = key[48:]
aesKey = key[0:16]

logging.debug("IV: 0x%s", iv.hex())
logging.debug("AES Key: 0x%s", aesKey.hex())

# custom unpad, as pycrytodome does not support pkcs5
unpad = lambda s: s[0:-(s[-1])]

cipher = AES.new(aesKey, mode=AES.MODE_CBC, iv=iv)
decrypted_data = unpad(cipher.decrypt(encdata))

print(decrypted_data.decode("utf-8"))