#!/usr/bin/env python3
"""andotp-decrypt.py

Usage:
andotp-decrypt.py [-o|--old] [--debug] [-h|--help] [--version] INPUT_FILE

Options:
-o --old      Use old encryption (andOTP <= 0.6.2)
--debug       Print debug info
-h --help     Show this screen.
--version     Show version.

"""

import os
import sys
import hashlib
import struct
from getpass import getpass

from Crypto.Cipher import AES
from Crypto.Hash import SHA256

from docopt import docopt


def bytes2Hex(bytes2encode):
    return '(%s) 0x%s' % (len(bytes2encode), ''.join('{:02x}'.format(x) for x in bytes2encode))


def decode(key, data, debug=False):
    """Decode function used for both the old and new style encryption"""
    # Raw data structure is IV[:12] + crypttext[12:-16] + auth_tag[-16:]
    iv = data[:12]
    crypttext = data[12:-16]
    tag = data[-16:]
    if debug:
        print("Input bytes: %", bytes2Hex(data))
        print("IV: %s" % bytes2Hex(iv))
        print("Crypttext: %s" % bytes2Hex(crypttext))
        print("Auth tag: %s" % bytes2Hex(tag))
    try:
        aes = AES.new(key, AES.MODE_GCM, nonce=iv)
    except Exception as e:
        print(e)
        return None

    try:
        dec = aes.decrypt_and_verify(crypttext, tag)
        if debug:
            print("Decrypted data: %s" % bytes2Hex(dec))
        return dec.decode('UTF-8')
    except ValueError as e:
        print(e)
        print("The passphrase was probably wrong")
        return None


def decrypt_aes_new_format(password, input_file, debug=False):
    input_bytes = None
    with open(input_file, 'rb') as f:
        input_bytes = f.read()

    if len(input_bytes) == 0:
        print("No data could be read. The input file is unreadable or empty")
        return None

    try:
        # Raw data structure is iterations[:4] + salt[4:16] + data[16:]
        iterations = struct.unpack(">I", input_bytes[:4])[0]
        salt = input_bytes[4:16]
        data = input_bytes[16:]
        try:
            pbkdf2_key = hashlib.pbkdf2_hmac('sha1', password, salt, iterations, 32)
        except OverflowError:
            print("Could not parse iterations from file, "
                  "either the file is broken or the format has changed.")
            return None
        if debug:
            print("Iterations: %s" % iterations)
            print("Salt: %s" % bytes2Hex(salt))
            print("Pbkdf2 key: %s" % bytes2Hex(pbkdf2_key))
        return decode(pbkdf2_key, data, debug)
    except struct.error:
        print("The input data could not be decrypted")
        return None


def decrypt_aes(password, input_file, debug=False):
    hash = SHA256.new(password)
    symmetric_key = hash.digest()
    if debug:
        print("Symmetric key: %s" % bytes2Hex(symmetric_key))
    input_bytes = None
    with open(input_file, 'rb') as f:
        input_bytes = f.read()

    return decode(symmetric_key, input_bytes, debug)


def get_password():
    # Read the backup password manually via a prompt if stdin is a tty,
    # otherwise read from stdin directly.
    if sys.stdin.isatty():
        pw = getpass('andOTP AES passphrase:')
    else:
        pw = sys.stdin.readline()
    return pw.strip().encode('UTF-8')


def find_entries(data, pattern, limit=None):
    result = []

    for entry in data:
        label = entry['label']
        # NOTE: issuer was not always part of the JSON structure
        issuer = entry.get('issuer', '')
        tags = "".join(entry.get('tags', []))
        key = label + issuer + tags
        if pattern.lower() in key.lower():
            result.append(entry)
            if limit and len(result) == limit:
                break

    return result


def descriptor(entry):
    label = entry.get('label')
    issuer = entry.get('issuer')
    if label and issuer:
        return f'{label}[{issuer}]'
    elif label:
        return label
    elif issuer:
        return f'[{issuer}]'
    else:
        return 'no label or issuer found'


def main():
    arguments = docopt(__doc__, version='andotp-decrypt 0.1')
    input_file = arguments['INPUT_FILE']
    debug = arguments['--debug']
    old_encryption = arguments['--old']
    if not os.path.exists(input_file):
        print("Could not find input file: %s" % input_file)
        return None
    password = get_password()
    if old_encryption:
        print(decrypt_aes(password, input_file, debug))
    else:
        print(decrypt_aes_new_format(password, input_file, debug))


if __name__ == '__main__':
    main()
