class Main(): def __init__(self): if os.geteuid() != 0: root_popup = Popup('Error', 'You need to be root to run the VPN.') root_popup.set_icon_from_file("surfshark_linux_client.png") root_popup.connect('destroy', Gtk.main_quit) Gtk.main() return None self.debug_on = True # set to False to disable debug infos # TODO : Make a loader # TODO : Find a way to have a popup to run the app as root self.folder_path = os.path.abspath(os.path.dirname(__file__)) + "/" self.servers = self.get_servers() self.unhash_pass = "" self.config_files = {} self.vpn_command = False self.thread = False self.ip = "" with open(self.folder_path + "config.json", "r") as file: self.config = json.load(file) style = Gtk.CssProvider() #Theme if (self.config['theme'] == 'light'): style.load_from_path(self.folder_path + "style/style_lightmode.css") else: style.load_from_path(self.folder_path + "style/style_darkmode.css") Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) if (self.config['password_needed'] or not self.config['registered']): self.log_window = LogWindow(self) self.log_window.connect("destroy", Gtk.main_quit) self.log_window.show_all() self.main_window = MainWindow(self) self.main_window.connect("destroy", self.soft_quit_g) if (not self.config['password_needed'] and self.config['registered']): screen = self.main_window.get_display() monitor_size = screen.get_monitor_at_window( Gdk.get_default_root_window()).get_geometry() self.main_window.move( monitor_size.width / 2 - self.main_window.get_size().width / 2, monitor_size.height / 2 - self.main_window.get_size().height / 2) self.main_window.credentials_username.set_text( self.config['vpn_username']) self.main_window.credentials_password.set_text( self.config['vpn_password']) self.main_window.show_all() try: Gtk.main() except (KeyboardInterrupt, SystemExit): self.soft_quit() def log_action(self, widget): hashed_password = self.hash_pass(self.log_window.password.get_text()) if self.config['registered']: if (hashed_password == self.config['password']): self.log_in() else: self.log_window.password.get_style_context().add_class('error') self.debug("Wrong password") else: if (widget == self.log_window.log_without_pass_button): self.config['registered'] = True self.config['password_needed'] = False self.save_config() self.log_in() else: if (self.log_window.password.get_text() == self.log_window.confirm_password.get_text()): self.config['registered'] = True self.config['password_needed'] = True self.config['password'] = hashed_password self.save_config() self.debug("Account created") self.log_in() else: self.log_window.password.get_style_context().add_class( 'error') self.log_window.confirm_password.get_style_context( ).add_class('error') def get_servers(self): with urllib.request.urlopen( "https://api.surfshark.com/v3/server/clusters/all") as url: return json.loads(url.read().decode()) def log_in(self): if (self.config['password_needed']): self.unhash_pass = self.log_window.password.get_text() old_pos = self.log_window.get_position() self.log_window.hide() self.main_window.move(old_pos.root_x, old_pos.root_y) self.main_window.show_all() if ('vpn_password' in self.config and 'vpn_username' in self.config and self.config['vpn_username'] != "" and self.config['vpn_password'] != ""): if (self.config['password_needed']): try: vpn_username = self.sym_decrypt( self.config['vpn_username']) vpn_password = self.sym_decrypt( self.config['vpn_password']) except: vpn_username = "" vpn_password = "" else: vpn_username = self.config['vpn_username'] vpn_password = self.config['vpn_password'] self.main_window.credentials_username.set_text(vpn_username) self.main_window.credentials_password.set_text(vpn_password) self.debug("Logged In") def save_config(self): with open(self.folder_path + "config.json", "w") as file: file.write(json.dumps(self.config)) self.debug("Config saved") def debug(self, message, type=None): if (self.debug_on): type = "INFO" if not type else type print(type + ": " + message) def save_credentials(self, widget): vpn_username = self.main_window.credentials_username.get_text() vpn_password = self.main_window.credentials_password.get_text() error_count = 0 if (vpn_username == ""): self.main_window.credentials_username.get_style_context( ).add_class('error') error_count += 1 if (vpn_password == ""): self.main_window.credentials_password.get_style_context( ).add_class('error') error_count += 1 if (error_count > 0): return if (self.config['password_needed']): vpn_username = self.sym_encrypt(vpn_username) vpn_password = self.sym_encrypt(vpn_password) self.config['vpn_username'] = vpn_username self.config['vpn_password'] = vpn_password self.save_config() creds_updated = threading.Thread(target=self.credential_updated) creds_updated.start() def sym_encrypt(self, raw): private_key = hashlib.sha256(self.unhash_pass.encode("utf-8")).digest() raw = pad(raw.encode('utf-8'), AES.block_size) iv = Random.new().read(AES.block_size) cipher = AES.new(private_key, AES.MODE_CBC, iv) return base64.b64encode(iv + cipher.encrypt(raw)).decode('utf8') def sym_decrypt(self, encrypted_text): private_key = hashlib.sha256(self.unhash_pass.encode("utf-8")).digest() encrypted_text = base64.b64decode(encrypted_text) iv = encrypted_text[:16] cipher = AES.new(private_key, AES.MODE_CBC, iv) return unpad(cipher.decrypt(encrypted_text[16:]), AES.block_size).decode('utf8') def hash_pass(self, password): return base64.b64encode( hashlib.sha3_512( password.encode("utf-8")).hexdigest().encode()).decode() def change_protocol(self, switch, is_tcp): if (is_tcp): self.config["connection_protocol"] = "tcp" self.main_window.udp_label.set_markup("UDP") self.main_window.tcp_label.set_markup("<b>TCP</b>") else: self.config["connection_protocol"] = "udp" self.main_window.udp_label.set_markup("<b>UDP</b>") self.main_window.tcp_label.set_markup("TCP") self.save_config() def change_password_need(self, button): button.set_sensitive(False) if (self.config['password_needed']): self.config['password_needed'] = False self.config['vpn_username'] = self.sym_decrypt( self.config['vpn_username']) self.config['vpn_password'] = self.sym_decrypt( self.config['vpn_password']) button.set_label("Enable Pass") else: self.config['password_needed'] = True self.config['vpn_username'] = self.sym_encrypt( self.config['vpn_username']) self.config['vpn_password'] = self.sym_encrypt( self.config['vpn_password']) button.set_label("Disable Pass") self.save_config() button.set_sensitive(True) def update_password(self, button): self.main_window.new_password.get_style_context().remove_class("error") self.main_window.confirm_new_password.get_style_context().remove_class( "error") if (self.main_window.new_password.get_text() == self.main_window.confirm_new_password.get_text()): self.config['password'] = self.hash_pass( self.main_window.new_password.get_text()) if (self.config['password_needed']): vpn_user = self.sym_decrypt(self.config['vpn_username']) vpn_pass = self.sym_decrypt(self.config['vpn_password']) else: vpn_user = self.config['vpn_username'] vpn_pass = self.config['vpn_password'] self.config['password_needed'] = True self.main_window.disable_pass_button.set_label("Disable Pass") self.main_window.enable_password_container.set_sensitive(True) self.unhash_pass = self.main_window.new_password.get_text() self.config['vpn_username'] = self.sym_encrypt(vpn_user) self.config['vpn_password'] = self.sym_encrypt(vpn_pass) self.main_window.new_password.set_text("") self.main_window.confirm_new_password.set_text("") self.save_config() password_updated_t = threading.Thread(target=self.password_updated) password_updated_t.start() else: self.main_window.new_password.get_style_context().add_class( "error") self.main_window.confirm_new_password.get_style_context( ).add_class("error") def password_updated(self): self.main_window.updated_password_label.set_label("Password updated") time.sleep(2) self.main_window.updated_password_label.set_label("") def credential_updated(self): self.main_window.updated_vpn_credential_label.set_label( "Credentials updated") time.sleep(2) self.main_window.updated_vpn_credential_label.set_label("") def change_theme(self, switch, current_theme): style = Gtk.CssProvider() if (current_theme): style.load_from_path(self.folder_path + "style/style_darkmode.css") self.config['theme'] = "dark" self.main_window.light_label.set_markup("Light") self.main_window.dark_label.set_markup("<b>Dark</b>") else: style.load_from_path(self.folder_path + "style/style_lightmode.css") self.config['theme'] = "light" self.main_window.light_label.set_markup("<b>Light</b>") self.main_window.dark_label.set_markup("Dark") Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), style, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) self.save_config() def change_killswitch(self, switch, killswitch_status): if (killswitch_status): self.config["killswitch"] = "on" self.main_window.killswitch_off_label.set_markup("OFF") self.main_window.killswitch_on_label.set_markup("<b>ON</b>") if (self.vpn_command): self.enable_killswitch() success_popup = Popup( "Killswitch activated", "The killswitch is now activated!\nThe current version of the killswitch can leak on port 1194" ) else: self.config["killswitch"] = "off" self.main_window.killswitch_off_label.set_markup("<b>OFF</b>") self.main_window.killswitch_on_label.set_markup("ON") self.disable_killswitch() success_popup = Popup( "Killswitch deactivated", "The killswitch is now inactive!\nYour network traffic is now possible to leak" ) self.save_config() def credential_updated(self): self.main_window.updated_vpn_credential_label.set_label( "Credentials updated") time.sleep(2) self.main_window.updated_vpn_credential_label.set_label("") def switch_server(self, button): text = self.main_window.selected_label.get_text() if (text == "Nothing"): return False self.main_window.connected_to_label.set_label("Connecting...") self.main_window.switch_server_btn.set_sensitive(False) openvpn_config_file = self.config_files[text] + "_" + self.config[ 'connection_protocol'] + ".ovpn" if (self.vpn_command): self.vpn_command.terminate() self.thread.join() self.main_window.ip_label.set_label("") with open(self.folder_path + '.tmp_creds_file', 'a+') as cred_file: if (self.config['password_needed']): cred_file.write( self.sym_decrypt(self.config['vpn_username']) + "\n" + self.sym_decrypt(self.config['vpn_password'])) else: cred_file.write(self.config['vpn_username'] + "\n" + self.config['vpn_password']) subprocess.call([ "cp", self.folder_path + "vpn_config_files/" + openvpn_config_file, self.folder_path + ".tmp_cfg_file" ]) try: i = 0 with open(self.folder_path + '.tmp_cfg_file', 'r+') as cfg_file: file = cfg_file.readlines() for line in file: if ("auth-user-pass" in line): break i += 1 except: self.debug("VPN config file is missing", "ERROR") self.main_window.connected_to_label.set_label("ERROR") self.main_window.switch_server_btn.set_sensitive(True) Popup( 'Error', "Couldn't find config file for this server, try to update the config files.", self.main_window) return False with open(self.folder_path + '.tmp_cfg_file', 'w') as cfg_file: file[i] = "auth-user-pass " + self.folder_path + ".tmp_creds_file" cfg_file.writelines(file) #disable the killswitch to connect to a new server if (self.config["killswitch"] == "on"): self.disable_killswitch() self.vpn_command = subprocess.Popen( ["openvpn", self.folder_path + ".tmp_cfg_file"], stdout=subprocess.PIPE) self.thread = threading.Thread(target=self.command_log) self.thread.start() def command_log(self): time.sleep(.1) subprocess.call(["rm", self.folder_path + ".tmp_cfg_file"]) subprocess.call(["rm", self.folder_path + ".tmp_creds_file"]) for line in iter(self.vpn_command.stdout.readline, b''): line = line.decode()[:-1] if ("Exiting due to fatal error" in line): self.debug('Fatal error, VPN disconnected', type="ERROR") self.ip = "" self.main_window.vpn_exited() elif ("AUTH_FAILED" in line): self.debug('Wrong credentials', type="ERROR") self.ip = "" self.main_window.vpn_connection_failed() elif ("Initialization Sequence Completed" in line): self.debug('Connected to VPN') p = subprocess.Popen(["curl", "ipecho.net/plain"], stdout=subprocess.PIPE) ip, err = p.communicate() self.ip = ip.decode() self.main_window.confirm_connection() if (self.config["killswitch"] == "on"): self.enable_killswitch() with open( self.folder_path + "logs/openvpn-logs-" + str(date.today()) + ".txt", 'a') as logfile: logfile.write(line + "\n") def disconnect(self, button): self.debug('DISCONNECTED') self.vpn_command.terminate() self.thread.join() self.disable_killswitch() self.ip = "" self.main_window.connected_to_label.set_label("Disconnected") self.main_window.ip_label.set_label("") self.main_window.disconnect_btn.set_sensitive(False) self.main_window.switch_server_btn.set_sensitive(True) def check_updates(self, button): subprocess.call([ "wget", "https://account.surfshark.com/api/v1/server/configurations", "-O", self.folder_path + "vpn_config_files/conf.zip" ]) p = subprocess.Popen( ["md5sum", self.folder_path + "vpn_config_files/conf.zip"], stdout=subprocess.PIPE) checksum, err = p.communicate() checksum = checksum.decode().split(" ")[0] if (checksum != self.config['config_md5']): subprocess.call("rm " + self.folder_path + "vpn_config_files/*.ovpn", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.call([ "unzip", self.folder_path + "vpn_config_files/conf.zip", "-d", self.folder_path + "vpn_config_files" ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) self.config['config_md5'] = checksum self.save_config() self.main_window.updates_info.set_label( "The config files were updated !") else: self.main_window.updates_info.set_label("Everything is okay !") subprocess.call(["rm", self.folder_path + "vpn_config_files/conf.zip"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def soft_quit(self): if self.vpn_command and self.thread: self.vpn_command.terminate() self.thread.join() self.disable_killswitch() self.create_tray() Gtk.main_quit() def soft_quit_g(self, window): self.soft_quit() def enable_killswitch(self): #enable killswitch enable_killswitch_command = "sudo ./enablekillswitch.sh" command = enable_killswitch_command.split() subprocess.run(command) def disable_killswitch(self): #restore old iptable rules restore_iptables_command = "sudo ./restoreiptables.sh" command = restore_iptables_command.split() subprocess.run(command) def create_tray(self): #TODO pass