From c9c72159b70fd7f33519687e6c86464a690b7013 Mon Sep 17 00:00:00 2001 From: Max Moser Date: Sat, 3 Feb 2018 17:40:56 +0100 Subject: [PATCH] Port most of nm-openvpn-auth-dialog to Python --- auth-dialog/nm-openvpn-auth-dialog.py | 320 ++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100755 auth-dialog/nm-openvpn-auth-dialog.py diff --git a/auth-dialog/nm-openvpn-auth-dialog.py b/auth-dialog/nm-openvpn-auth-dialog.py new file mode 100755 index 0000000..5418939 --- /dev/null +++ b/auth-dialog/nm-openvpn-auth-dialog.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +"""A tool for finding the passwords required by the plugin""" + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +import argparse +import sys + +supported_services = ["org.freedesktop.NetworkManager.wireguard"] + +# for parsing the data-keys +DATA_KEY_TAG = "DATA_KEY=" +DATA_VAL_TAG = "DATA_VAL=" +SECRET_KEY_TAG = "SECRET_KEY=" +SECRET_VAL_TAG = "SECRET_VAL=" + +# strings used in hints +VPN_MESSAGE = "x-vpn-message:" +VPN_PASS = "password" +VPN_CERTPASS = "cert-pass" +VPN_PROXY_PASS = "http-proxy-password" + +# NMSettingSecretFlags +NM_SETTING_SECRET_FLAG_NONE = 0 +NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1 +NM_SETTING_SECRET_FLAG_NOT_SAVED = 2 +NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4 + +class PasswordDialog(Gtk.Dialog): + """The dialog used for communication with the User (for password entry)""" + def __init__(self, parent, prompt, password, need_pw, certpass, need_cp, proxy_pass, need_pp): + Gtk.Dialog.__init__(self, "Authenticate VPN", parent, 0, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OK, Gtk.ResponseType.OK)) + + self.password = password if password is not None else "" + self.certpass = certpass if certpass is not None else "" + self.proxypass = proxy_pass if proxy_pass is not None else "" + + self.set_default_size(150, 100) + label = Gtk.Label(prompt) + area = Gtk.Box() + area.set_spacing(10) + box = self.get_content_area() + box.set_spacing(10) + box.add(label) + + # create the labels / text-fields for the passwords + pw_box = Gtk.Box() + self.pw = Gtk.Label("Password") + self.pw_field = Gtk.Entry() + self.pw_field.set_text(self.password) + pw_box.set_homogeneous(True) + pw_box.add(self.pw) + pw_box.add(self.pw_field) + cp_box = Gtk.Box() + self.cp = Gtk.Label("Certificate Password") + self.cp_field = Gtk.Entry() + self.cp_field.set_text(self.certpass) + cp_box.set_homogeneous(True) + cp_box.add(self.cp) + cp_box.add(self.cp_field) + pp_box = Gtk.Box() + self.pp = Gtk.Label("Proxy Password") + self.pp_field = Gtk.Entry() + self.pp_field.set_text(self.proxypass) + pp_box.set_homogeneous(True) + pp_box.add(self.pp) + pp_box.add(self.pp_field) + + if need_pw: + box.add(pw_box) + if need_cp: + box.add(cp_box) + if need_pp: + box.add(pp_box) + + area.add(box) + self.show_all() + + def get_password(self): + """Return the password entered by the user (only valid after .run())""" + return self.pw_field.get_text() + + def get_certpass(self): + """Return the cert-pass entered by the user (only valid after .run())""" + return self.cp_field.get_text() + + def get_proxypass(self): + """Return the http-proxy-password entered by the user (only valid after .run())""" + return self.pp_field.get_text() + + +def read_details(input_file): + """Read the DATA and SECRET things passed to the stdin and create dictionaries from them""" + data = {} + secrets = {} + data_key = None + secret_key = None + + for line in input_file: + line = line.strip() + if line == "DONE": + break + else: + if line.startswith(DATA_KEY_TAG): + data_key = line[len(DATA_KEY_TAG):] + elif line.startswith(DATA_VAL_TAG): + data_val = line[len(DATA_VAL_TAG):] + data[data_key] = data_val + elif line.startswith(SECRET_KEY_TAG): + secret_key = line[len(SECRET_KEY_TAG):] + elif line.startswith(SECRET_VAL_TAG): + secret_val = line[len(SECRET_VAL_TAG):] + secrets[secret_key] = secret_val + + return data, secrets + + +def check_passwords_required(hints, name): + """Check which passwords we need for the plugin""" + prompt = "You need to authenticate to access the VPN '%s'" % name + need_password, need_certpass, need_proxy_pass = False, False, False + for hint in hints: + if hint.startswith(VPN_MESSAGE): + prompt = hint[len(VPN_MESSAGE):] + elif hint == VPN_PASS: + need_password = True + elif hint == VPN_CERTPASS: + need_certpass = True + elif hint == VPN_PROXY_PASS: + need_proxy_pass = True + + # TODO implement other logic from `get_passwords_required()` as well + + return prompt, need_password, need_certpass, need_proxy_pass + + +def keyring_lookup(uuid, key): + """Look up the information stored in the keyring for uuid and key""" + # TODO + return "asdf" + + +def nm_vpn_service_plugin_get_secret_flags(data_dict, secret_name, out_flags): + """Check if the """ + lookup_item = "%s-flags" % secret_name + + if lookup_item not in data_dict: + # if the constructed lookup item is not in the dictionary... + return False, out_flags + else: + try: + entry = data_dict[lookup_item] + flag = int(entry) + # check if the flag is one of the NMSecretSettingFlags + if flag not in [0,1,2,4] or str(flag) != entry: + return False, out_flags + return True, flag + except: + return False, out_flags + + +def find_existing_passwords(data_dict, secrets_dict, vpn_uuid, need_pass, need_certpass, need_proxypass): + """Check if any of the required passwords can be found in the secrets_dict or the keyring""" + pw_flags = NM_SETTING_SECRET_FLAG_NONE + cp_flags = NM_SETTING_SECRET_FLAG_NONE + pp_flags = NM_SETTING_SECRET_FLAG_NONE + stored_pw = None + stored_cp = None + stored_pp = None + + # password + # check if the DATA dictionary contains any flags regarding the password + # and if so, check if it is anything else than NM_SETTING_SECRET_FLAG_NOT_SAVED + tmp, pw_flags = nm_vpn_service_plugin_get_secret_flags(data_dict, VPN_PASS, pw_flags) + if pw_flags != NM_SETTING_SECRET_FLAG_NOT_SAVED: + # check out the SECRETS dictionary + # or, if that fails + # check out the keyring (TODO) + if VPN_PASS in secrets_dict: + stored_pw = secrets_dict[VPN_PASS] + else: + pass + # stored_pw = keyring_lookup(vpn_uuid, VPN_PASS) + + # certpass + tmp, cp_flags = nm_vpn_service_plugin_get_secret_flags(data_dict, VPN_CERTPASS, cp_flags) + if cp_flags != NM_SETTING_SECRET_FLAG_NOT_SAVED: + if VPN_CERTPASS in secrets_dict: + stored_cp = secrets_dict[VPN_CERTPASS] + else: + pass + # stored_cp = keyring_lookup(vpn_uuid, VPN_CERTPASS) + + # proxy + tmp, pp_flags = nm_vpn_service_plugin_get_secret_flags(data_dict, VPN_PROXY_PASS, pp_flags) + if pp_flags != NM_SETTING_SECRET_FLAG_NOT_SAVED: + if VPN_PROXY_PASS in secrets_dict: + stored_pp = secrets_dict[VPN_PROXY_PASS] + else: + pass + # stored_pp = keyring_lookup(vpn_uuid, VPN_PROXY_PASS) + + return stored_pw, stored_cp, stored_pp + + +def wait_for_quit(): + """Wait until we read QUIT from stdin""" + for line in sys.stdin: + line = line.strip() + if line == "QUIT": + break + + +descr = "" +epilog = "" + +parser = argparse.ArgumentParser(description=descr, epilog=epilog) + +parser.add_argument("-r", "--reprompt", type=str, help="Reprompt for password") +parser.add_argument("-u", "--uuid", type=str, help="UUID of VPN connection") +parser.add_argument("-n", "--name", type=str, help="Name of VPN connection") +parser.add_argument("-s", "--service", type=str, help="VPN service type") +parser.add_argument("-i", "--allow-interaction", action="store_true", default=False, help="Allow user interaction") +parser.add_argument("--external-ui-mode", action="store_true", default=False, help="External UI mode") +parser.add_argument("-t", "--hint", action="append", help="Hints from the VPN plugin") + +args = parser.parse_args() +canceled = False +hints = args.hint +ext_ui = args.external_ui_mode +retry = args.reprompt +interactive = args.allow_interaction +uuid = args.uuid +name = args.name +service = args.service +if hints is None: + hints = [] + +if uuid is None or name is None or service is None: + print("UUID (-u), Name (-n) and Service (-s) have to be provided", file=sys.stderr) + exit(1) + +if service not in supported_services: + print("The service '%s' is not supported" % service, file=sys.stderr) + print("Supported services: %s" % supported_services, file=sys.stderr) + exit(1) + +if ext_ui: + print("Sorry, but external UI is not supported", file=sys.stderr) + +# read data and secrets from stdin until "DONE", in the format +# +# DATA_KEY=key +# DATA_VAL=value +# SECRET_KEY=key +# SECRET_VAL=value +# DONE +data, secrets = read_details(sys.stdin) +if not data and not secrets: + # sys.exit(1) + pass + +print(data) +print(secrets) + +# check, which passwords are required +prompt, need_password, need_certpass, need_proxy_pass = check_passwords_required(hints, name) + +if need_password or need_certpass or need_proxy_pass: + + # find whatever we need in the secrets map or the keyring (TODO) + pw, cp, pp = find_existing_passwords(data, secrets, uuid, need_password, need_certpass, need_proxy_pass) + required_secrets = False + + # check if there is anything left we need + if need_password and pw is None: + required_secrets = True + if need_certpass and cp is None: + required_secrets = True + if need_proxy_pass and pp is None: + required_secrets = True + + # if there is, we have to ask the user + if interactive and (required_secrets or retry): + dialog = PasswordDialog(None, prompt, + pw, need_password, + cp, need_certpass, + pp, need_proxy_pass) + resp = dialog.run() + + if resp == Gtk.ResponseType.OK: + # fetch what we need from the dialog + if need_password: + pw = dialog.get_password() + if need_certpass: + cp = dialog.get_certpass() + if need_proxy_pass: + pp = dialog.get_proxypass() + else: + canceled = True + + # if the dialog wasn't cancelled or the session wasn't interactive at all, + # print to stdout what we found out + if not canceled: + # print whatever we found out + if pw is not None: + print("%s\n%s" % (VPN_PASS, pw)) + if cp is not None: + print("%s\n%s" % (VPN_CERTPASS, cp)) + if pp is not None: + print("%s\n%s" % (VPN_PROXY_PASS, pp)) + print("\n\n", end="") + + # wait for QUIT from the stdin + wait_for_quit() + +exit(1 if canceled else 0)