#!/usr/bin/env python3

# Requires python-gpg library
import os
import sys
import json
import struct
import shutil
import difflib
from urllib.parse import urlparse
from collections import OrderedDict
import gnupg

if sys.platform == "win32":
    # Interacts with windows registry to register this app
    import winreg
    # On Windows, the default I/O mode is O_TEXT. Set this to O_BINARY
    # to avoid unwanted modifications of the input/output streams.
    import msvcrt
    msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
    msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)

EXTENSION_NAME = "com.piaotech.chrome.extension.pass"
EXTENSION_ID = "oblajhnjmknenodebpekmkliopipoolo"


def get_gpg_bin():
    if 'PASS_GPG_BIN' in os.environ:
        return os.environ['PASS_GPG_BIN']
    if shutil.which('gpg2'):
        return shutil.which('gpg2')
    return shutil.which('gpg')


def get_store_path():
    if 'PASSWORD_STORE_DIR' in os.environ:
        path = os.environ['PASSWORD_STORE_DIR']
    else:
        path = os.path.expanduser('~') + "/.password-store"
    return path


# Returns a similarity score between two strings.
def similarity(pattern1, pattern2):
    return difflib.SequenceMatcher(a=pattern1.lower(),
                                   b=pattern2.lower()).ratio()


# Returns the similarity score of a credential to a pattern. This method is
# used as sort key for sorting the credential list based on similarity to the
# search pattern.
def sort_key(credential):
    score = similarity(credential[1], credential[3])
    return score


# Returns a list of credentials ordered by similarity to the pattern argument.
# The list is a list of lists with the format:
#
#  [
#    [ "store path", "url", "username", "pattern" ],
#    [ "store path", "url", "username", "pattern" ],
#    [ "store path", "url", "username", "pattern" ],
#    .....
#  ]
def get_list(pattern):
    credentials = []
    for root, _, files in os.walk(get_store_path(), followlinks=True):
        if len(files) > 0:
            for f in files:
                if f.endswith(".gpg"):
                    cred_path = root.replace(get_store_path(), "")
                    cred_url = get_url(cred_path)
                    cred_user = f.replace(".gpg", "")
                    credentials.append(
                        [cred_path, cred_url, cred_user, pattern])

    return sorted(credentials, key=sort_key, reverse=True)


def get_url(path):
    return os.path.basename(os.path.normpath(path))


# Returns the pass password for the specified path. The path must corresponde
# to a unique existing gpg file inside the password store.
def get_pass(path):
    password = ""
    gpg = gnupg.GPG(use_agent=True, gpgbinary=get_gpg_bin())
    txt = open(get_store_path() + "/" + path + ".gpg", "rb")
    data = gpg.decrypt_file(txt)
    if data.ok:
        password = data.data.decode('utf-8').split("\n")[0]
        return password

    raise RuntimeError("Failed to decrypt %s" % txt)


# Sends the response message with the format that chrome HostNativeApplications
# expect.
def send_message(message):
    response = json.dumps(message).encode('utf-8')
    sys.stdout.buffer.write(struct.pack('I', len(response)))
    sys.stdout.buffer.write(response)
    sys.stdout.buffer.flush()


# Method that implements Chrome Native App protocol for messaging.
def process_native():
    size = sys.stdin.buffer.read(4)

    if not size:
        send_message({"action": "error", "msg": "no data"})
        sys.exit()

    try:
        length = struct.unpack('I', size)[0]
        data = sys.stdin.buffer.read(length)
        request = json.loads(data.decode('utf-8'))
        action = request["action"]
        if action == "get-creds":
            pattern = urlparse(request["url"]).netloc
            send_message({
                "action": "fill-creds",
                "credentials": get_list(pattern),
                "url": pattern
            })
        elif action == "get-pass":
            user = request["user"]
            path = request["path"]
            send_message({
                "action": "fill-pass",
                "user": user,
                "pass": get_pass(path)
            })
    except RuntimeError as e:
        send_message({"action": "error", "msg": str(e)})
    except Exception:
        send_message({"action": "error", "msg": sys.exc_info()[0]})


# Method prints to stdout the list of passwords ordered by a similarty pattern
def print_pass(pattern):
    for credential in get_list(pattern)[:20]:
        account = credential[0] + "/" + credential[2]
        try:
            password = get_pass(account)
            print("pass %s  %s" % (account, password))
        except RuntimeError as e:
            print("pass %s  %s" % (account, str(e)))


# Method prints to stdout the list of credentials ordered by a similarity to
# pattern.
def print_creds(pattern):
    for credential in get_list(pattern):
        score = similarity(credential[1], pattern)
        print("compare %s and %s score %.4f" %
              (credential[1], credential[3], score))


# Determines the path were the native app manifest should be installed.
def native_path_chrome():
    if sys.platform == "darwin":
        return os.path.expanduser(
            '~'
        ) + "/Library/Application Support/Google/Chrome/NativeMessagingHosts/"
    if sys.platform == "linux" or sys.platform == "linux2":
        return os.path.expanduser(
            '~') + "/.config/google-chrome/NativeMessagingHosts/"
    if sys.platform == "win32":
        return os.path.join("Google", "Chrome", "NativeMessagingHosts",
                            EXTENSION_NAME)

    sys.stderr.write("Only linux, OSX or Windows are supported")
    sys.exit(1)


def native_path_chromium():
    if sys.platform == "darwin":
        return os.path.expanduser(
            '~'
        ) + "/Library/Application Support/Chromium/NativeMessagingHosts/"
    if sys.platform == "linux" or sys.platform == "linux2":
        return os.path.expanduser(
            '~') + "/.config/chromium/NativeMessagingHosts/"
    if sys.platform == "win32":
        return os.path.expanduser(
            '~') + "/AppData/Roaming/Chromium/NativeMessagingHosts/"

    sys.stderr.write("Only linux or OSX are supported")
    sys.exit(1)


# Installs the Native Host Application manifest for this script into Chrome.
def install(nativePath, extension_id):
    if sys.platform == "win32":
        # Appends APPDATA to nativePath and set this path as a registry value
        reg_key = os.path.join("Software", nativePath)
        nativePath = os.path.join(os.environ['APPDATA'], nativePath)
        outfile = os.path.join(nativePath, EXTENSION_NAME + '.json')
        winreg.SetValue(winreg.HKEY_CURRENT_USER, reg_key, winreg.REG_SZ,
                        outfile)

    if not os.path.exists(nativePath):
        os.makedirs(nativePath)

    if sys.platform == "win32":
        batch = "python \"{}\" %*".format(os.path.realpath(__file__))
        native_app = EXTENSION_NAME + '.bat'
        outfile = os.path.join(nativePath, native_app)
        with open(outfile, 'w') as file:
            file.write("@echo off\n\n")
            file.write(batch)
    else:
        native_app = os.path.realpath(__file__)

    manifest = OrderedDict()
    manifest['name'] = EXTENSION_NAME
    manifest["description"] = "Chrome native host application for pass."
    manifest["path"] = native_app
    manifest["type"] = "stdio"
    manifest["allowed_origins"] = ["chrome-extension://" + extension_id + "/"]

    outfile = os.path.join(nativePath, manifest['name'] + '.json')

    with open(outfile, 'w') as file:
        json.dump(manifest, file, indent='\t')


if len(sys.argv) > 1:
    if sys.argv[1].startswith('chrome-extension://'):
        process_native()
    elif sys.argv[1] == "install":
        if len(sys.argv) > 2:
            install(native_path_chrome(), sys.argv[2])
            install(native_path_chromium(), sys.argv[2])
        else:
            install(native_path_chrome(), EXTENSION_ID)
            install(native_path_chromium(), EXTENSION_ID)
    elif sys.argv[1] == "pass":
        if len(sys.argv) > 2:
            print_pass(sys.argv[2])
    elif sys.argv[1] == "gpgbin":
        print("GPG Binary path: %s" % get_gpg_bin())
    else:
        print_creds(sys.argv[1])
