#!/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] enc = enc.replace(" ","+") 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"))