From ac035db5977320aa7a2e5b59d19b00d76a7a1607 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Tue, 27 Jun 2023 14:49:03 -0400 Subject: [PATCH] Re-organized repo for upcoming v1.5.0 extension management changes --- README.md | 21 +- commit.sh | 2 +- extensions/password-pasture/CMakeLists.txt | 16 + extensions/password-pasture/build.sh | 5 + extensions/password-pasture/main.c | 380 +++++++++++++++++++ extensions/password-pasture/requirements.txt | 3 + extensions/sshyp-mfa/sshyp-mfa.ext | 3 + extensions/sshyp-mfa/sshyp-mfa.py | 90 +++++ pointers/v1.5.0 | 6 + 9 files changed, 513 insertions(+), 13 deletions(-) create mode 100644 extensions/password-pasture/CMakeLists.txt create mode 100755 extensions/password-pasture/build.sh create mode 100644 extensions/password-pasture/main.c create mode 100644 extensions/password-pasture/requirements.txt create mode 100644 extensions/sshyp-mfa/sshyp-mfa.ext create mode 100755 extensions/sshyp-mfa/sshyp-mfa.py create mode 100644 pointers/v1.5.0 diff --git a/README.md b/README.md index dab2498..a436b93 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,17 @@ # sshyp-labs -Experimental extensions for the sshyp password manager. +Extensions for the [sshyp password manager](https://github.com/rwinkhart/sshyp). -sshyp-labs is currently the home of sshyp-mfa and password-pasture (sshyp-gui). +When new functionality is desired that goes outside of sshyp's primary goals or requires venturing outside of the Python standard library, said functionality is implemented as an extension. + +# Installation +For sshyp v1.5.0+, sshyp extensions should be installed from the `sshyp tweak` menu's "extension management" option. + +**Do NOT use the packages from the releases page! These are only for older, unsupported releases of sshyp!** # Available Extensions -sshyp-mfa - [installation and usage instructions](https://github.com/rwinkhart/sshyp-labs/wiki/sshyp-mfa) +[sshyp-mfa](https://github.com/rwinkhart/sshyp-labs/wiki/sshyp-mfa): read mfa data from sshyp entries to generate and copy totp keys to the clipboard (optional Steam support) -sshyp-mfa is a unique approach to generating multi-factor authentication keys. Upon running `sshyp copy -m`, -an MFA key will be generated and copied to your clipboard. sshyp-mfa will continue to run in the background and copy a -new key to your clipboard every time the actively copied one expires. Never worry about a TOTP timer expiring again! -At any time, sshyp-mfa can be closed with ctrl+c to stop this process. - -password-pasture - [installation and usage instructions](https://github.com/rwinkhart/sshyp-labs/wiki/password-pasture) - -password-pasture is an incredibly experimental GTK4 GUI interface for sshyp. It is currently not in a usable state -and has not been updated since sshyp v1.1.X. Development is set to resume after sshyp is more feature-complete. +[password-pasture](https://github.com/rwinkhart/sshyp-labs/wiki/password-pasture): a HIGHLY experimental GTK4 sshyp GUI - very incomplete (last updated for sshyp v1.1.x) # Acknowledgements sshyp-mfa relies on [ValvePython/steam](https://github.com/ValvePython/steam) for Steam support. diff --git a/commit.sh b/commit.sh index 1b0cfa2..fb850d2 100755 --- a/commit.sh +++ b/commit.sh @@ -1,4 +1,4 @@ #!/bin/sh -git add -f extra password-pasture sshyp-mfa LICENSE README.md version commit.sh .gitignore +git add -f LICENSE README.md commit.sh extensions extra pointers git commit -m "$1" git push diff --git a/extensions/password-pasture/CMakeLists.txt b/extensions/password-pasture/CMakeLists.txt new file mode 100644 index 0000000..bd166ba --- /dev/null +++ b/extensions/password-pasture/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.23) +project(password_pasture C) + +set(CMAKE_C_STANDARD 17) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK4 REQUIRED gtk4) + +include_directories(${GTK4_INCLUDE_DIRS}) +link_directories(${GTK4_LIBRARY_DIRS}) + +add_definitions(${GTK4_CFLAGS_OTHER}) + +add_executable(password_pasture main.c) + +target_link_libraries(password_pasture ${GTK4_LIBRARIES}) \ No newline at end of file diff --git a/extensions/password-pasture/build.sh b/extensions/password-pasture/build.sh new file mode 100755 index 0000000..3fece79 --- /dev/null +++ b/extensions/password-pasture/build.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +rm -rf CMakeFiles cmake_install.cmake CMakeCache.txt Makefile +cmake ./ +sleep 10 # prevents clock skew +make -j$(nproc) diff --git a/extensions/password-pasture/main.c b/extensions/password-pasture/main.c new file mode 100644 index 0000000..3772d26 --- /dev/null +++ b/extensions/password-pasture/main.c @@ -0,0 +1,380 @@ +#include +#include +#include +#include +#include + +GtkWidget *label_selected; GtkListBox *list_box; +char entries[1024][1024]; int entry_array_count = 0; + +void str_remove(char *str, const char *sub) { + char *p, *q, *r; + if (*sub && (q = r = strstr(str, sub)) != 0) { + size_t len = strlen(sub); + while ((r = strstr(p = r + len, sub)) != 0) { + while (p < r) + *q++ = *p++; + } + while ((*q++ = *p++) != '\0') + continue; + } +} + +void gen_entry_array(char *path, size_t size, const char *path_orig) { + DIR *dir; + struct dirent *entry; + size_t len = strlen(path); + + if (!(dir = opendir(path))) { + return; } + + while ((entry = readdir(dir)) != 0) { + char *name = entry->d_name; + if (entry->d_type == DT_DIR) { + if (!strcmp(name, ".") || !strcmp(name, "..")) { + continue; } + path[len] = '/'; + strcpy(path + len + 1, name); + gen_entry_array(path, size, path_orig); + path[len] = '\0'; + } else { + char path_filtered[1024]; + strcpy(path_filtered, path); + str_remove(path_filtered, path_orig); + size_t name_len = strlen(name); + name[name_len-4] = 0; + char sshyp_path[1024]; strcpy(sshyp_path, path_filtered); strcat(sshyp_path, "/"); strcat(sshyp_path, name); + strcpy(entries[entry_array_count], sshyp_path); + entry_array_count++; + } + } + closedir(dir); +} + +void password_convert(GtkEntryBuffer *buffer) { + const char *password = gtk_entry_buffer_get_text(buffer); + char *sub1 = "gpg --pinentry-mode loopback --batch --passphrase-fd 0 --armor -qd --output /dev/null ~/.config/sshyp/lock.gpg <<< '", *sub2 = "'", *command = (char *) malloc(2048); + strcpy(command, sub1); strcat(command, password); strcat(command, sub2); + system(command); + free(command); +} + +void gpg_lock() { + system("gpgconf --reload gpg-agent"); +} + +void password_prompt(GtkWindow *window) { + GtkWidget *dialog, *dialog_header, *box_header; // main window and header bar + GtkWidget *button_unlock, *button_cancel, *button_lock; // header bar buttons + GtkWidget *content_area, *password_entry; // content area and widgets + + dialog = gtk_dialog_new(); + password_entry = gtk_entry_new(); + gtk_entry_set_visibility(GTK_ENTRY(password_entry), FALSE); + gtk_entry_set_placeholder_text(GTK_ENTRY(password_entry), "passphrase"); + dialog_header = gtk_header_bar_new(); + gtk_header_bar_set_show_title_buttons(GTK_HEADER_BAR(dialog_header), FALSE); + box_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_halign(box_header, GTK_ALIGN_CENTER); + button_unlock = gtk_button_new_with_label("unlock"); + button_cancel = gtk_button_new_with_label("cancel"); + button_lock = gtk_button_new_from_icon_name("changes-prevent"); + gtk_widget_set_size_request(button_lock, 20, -1); + gtk_box_append(GTK_BOX(box_header), button_unlock); + gtk_box_append(GTK_BOX(box_header), button_cancel); + gtk_box_append(GTK_BOX(box_header), button_lock); + gtk_header_bar_set_title_widget(GTK_HEADER_BAR(dialog_header), box_header); + gtk_window_set_title(GTK_WINDOW(dialog), ""); + gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window)); + gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); + gtk_window_set_default_size(GTK_WINDOW(dialog), 50, 50); + + // add password entry to dialog content area + content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + gtk_box_append(GTK_BOX(content_area), password_entry); + + gtk_window_set_titlebar(GTK_WINDOW(dialog), dialog_header); + gtk_widget_show(dialog); + + // button actions + g_signal_connect_swapped(button_unlock, "clicked", G_CALLBACK(password_convert), gtk_entry_get_buffer(GTK_ENTRY(password_entry))); + g_signal_connect_swapped(button_unlock, "clicked", G_CALLBACK(gtk_window_close), dialog); + g_signal_connect_swapped(button_cancel, "clicked", G_CALLBACK(gtk_window_close), dialog); + g_signal_connect(button_lock, "clicked", G_CALLBACK(gpg_lock), 0); + g_signal_connect_swapped(button_lock, "clicked", G_CALLBACK(gtk_window_close), dialog); +} + +void copy_item(GtkWidget *button) { + char *command = malloc(2048); const char *button_label = gtk_button_get_label(GTK_BUTTON(button)); + strcpy(command, "sshyp copy "); + strcat(command, button_label); strcat(command, " "); + strcat(command, gtk_label_get_text((GtkLabel *) label_selected)); + system(command); + free(command); +} + +void copy_edit_field_prompt(GtkWidget *button, gpointer *window) { + GtkWidget *dialog, *dialog_header; // main window and header bar + GtkWidget *button_cancel; // header bar buttons + GtkWidget *content_area, *button_pwd, *button_usr, *button_url, *button_nte, *button_mfa; // content area and widgets + int mode; // copy/edit mode flag + + // set mode based on button pressed, copy = 1, edit = 2 + const char *button_label = gtk_button_get_icon_name(GTK_BUTTON(button)); + if (strcmp(button_label, "edit-copy") == 0) { + mode = 1; + } else { + mode = 2; + } + + dialog = gtk_dialog_new(); + dialog_header = gtk_header_bar_new(); + gtk_header_bar_set_show_title_buttons(GTK_HEADER_BAR(dialog_header), FALSE); + button_cancel = gtk_button_new_with_label("cancel"); + gtk_header_bar_set_title_widget(GTK_HEADER_BAR(dialog_header), button_cancel); + gtk_window_set_title(GTK_WINDOW(dialog), ""); + gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window)); + gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); + gtk_window_set_default_size(GTK_WINDOW(dialog), 50, 50); + + // add options to dialog content area + content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + button_pwd = gtk_button_new_with_label("password"); + button_usr = gtk_button_new_with_label("username"); + button_url = gtk_button_new_with_label("url"); + button_nte = gtk_button_new_with_label("note"); + button_mfa = gtk_button_new_with_label("mfa"); + gtk_box_append(GTK_BOX(content_area), button_pwd); + gtk_box_append(GTK_BOX(content_area), button_usr); + gtk_box_append(GTK_BOX(content_area), button_url); + gtk_box_append(GTK_BOX(content_area), button_nte); + gtk_box_append(GTK_BOX(content_area), button_mfa); + + gtk_window_set_titlebar(GTK_WINDOW(dialog), dialog_header); + gtk_widget_show(dialog); + + // button actions + if (mode == 1) { + g_signal_connect(button_pwd, "clicked", G_CALLBACK(copy_item), 0); + g_signal_connect(button_usr, "clicked", G_CALLBACK(copy_item), 0); + g_signal_connect(button_url, "clicked", G_CALLBACK(copy_item), 0); + g_signal_connect(button_nte, "clicked", G_CALLBACK(copy_item), 0); + g_signal_connect(button_mfa, "clicked", G_CALLBACK(copy_item), 0); + } + + g_signal_connect_swapped(button_cancel, "clicked", G_CALLBACK(gtk_window_close), dialog); + g_signal_connect_swapped(button_pwd, "clicked", G_CALLBACK(gtk_window_close), dialog); + g_signal_connect_swapped(button_usr, "clicked", G_CALLBACK(gtk_window_close), dialog); + g_signal_connect_swapped(button_url, "clicked", G_CALLBACK(gtk_window_close), dialog); + g_signal_connect_swapped(button_nte, "clicked", G_CALLBACK(gtk_window_close), dialog); + g_signal_connect_swapped(button_mfa, "clicked", G_CALLBACK(gtk_window_close), dialog); +} + +void set_selection_label(GtkWidget *button, gpointer *label) { + const char *button_label = gtk_button_get_label(GTK_BUTTON(button)); + gtk_label_set_text(GTK_LABEL(label), button_label); +} + +void entry_list_gen() { + GtkWidget *entry_button; + + GtkWidget *iter = gtk_widget_get_first_child ((GtkWidget *) list_box); + while (iter != 0) { + GtkWidget *next = gtk_widget_get_next_sibling (iter); + gtk_list_box_remove (list_box, iter); + iter = next; + } + + char path[1024]; char *user_home = getenv("HOME"); strcpy(path, user_home); strcat(path, "/.local/share/sshyp"); + char path_orig[1024]; strcpy(path_orig, path); strcat(path_orig, "/"); + gen_entry_array(path, sizeof path, path_orig); + + // bubble sort entries array + char *temp = (char *) malloc(1024); + for(int i=0; i 0){ + strcpy(temp, entries[j]); + strcpy(entries[j], entries[j+1]); + strcpy(entries[j+1], temp); + } + } + } + free(temp); + + strcpy(entries[entry_array_count], "\0"); + + for (int i_2 = 0; strcmp(entries[i_2], "\0") != 0; i_2++) { + entry_button = gtk_button_new_with_label(entries[i_2]); + gtk_label_set_xalign(GTK_LABEL(gtk_button_get_child(GTK_BUTTON(entry_button))), GTK_JUSTIFY_LEFT); + gtk_button_set_has_frame(GTK_BUTTON(entry_button), FALSE); + g_signal_connect(entry_button, "clicked", G_CALLBACK(set_selection_label), (gpointer *)label_selected); + gtk_list_box_append(list_box, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); + gtk_list_box_append(list_box, entry_button); + } +} + +void sshyp_sync() { + system("sshyp sync"); + entry_array_count = 0; + entry_list_gen(); +} + +void delete_entry() { + char *command = (char *) malloc(2048); + strcpy(command, "sshyp shear "); strcat(command, gtk_label_get_text(GTK_LABEL(label_selected))); + system(command); + free(command); + entry_array_count = 0; + entry_list_gen(); +} + +void are_they_sure(GtkWindow *window) { + GtkWidget *dialog, *dialog_header, *box_header; // main window and header bar + GtkWidget *button_delete, *button_cancel; // header bar buttons + GtkWidget *content_area, *label_sure; // content area and widgets + + dialog = gtk_dialog_new(); + dialog_header = gtk_header_bar_new(); + gtk_header_bar_set_show_title_buttons(GTK_HEADER_BAR(dialog_header), FALSE); + box_header = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_halign(box_header, GTK_ALIGN_CENTER); + button_delete = gtk_button_new_with_label("delete"); + button_cancel = gtk_button_new_with_label("cancel"); + gtk_box_append(GTK_BOX(box_header), button_delete); + gtk_box_append(GTK_BOX(box_header), button_cancel); + gtk_header_bar_set_title_widget(GTK_HEADER_BAR(dialog_header), box_header); + gtk_window_set_title(GTK_WINDOW(dialog), ""); + gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window)); + gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); + gtk_window_set_default_size(GTK_WINDOW(dialog), 50, 50); + + // add label to dialog content area + content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + gtk_widget_set_halign(content_area, GTK_ALIGN_CENTER); + char *label_text = malloc(2048); + strcpy(label_text, "this will delete:\n"); + strcat(label_text, gtk_label_get_text(GTK_LABEL(label_selected))); + strcat(label_text, "\nare you sure?"); + label_sure = gtk_label_new(label_text); + free(label_text); + gtk_label_set_justify(GTK_LABEL(label_sure), GTK_JUSTIFY_CENTER); + gtk_box_append(GTK_BOX(content_area), label_sure); + + // button actions + g_signal_connect(button_delete, "clicked", G_CALLBACK(delete_entry), 0); + g_signal_connect_swapped(button_delete, "clicked", G_CALLBACK(gtk_window_close), dialog); + g_signal_connect_swapped(button_cancel, "clicked", G_CALLBACK(gtk_window_close), dialog); + + gtk_window_set_titlebar(GTK_WINDOW(dialog), dialog_header); + gtk_widget_show(dialog); +} + +static void activate(GtkApplication* app) { + GtkWidget *window, *header_bar; // main window and header bar + GtkWidget *button_sync, *button_unlock; // header bar buttons + GtkWidget *stack, *stack_switcher; GtkStackPage *page_info; // stack widgets + GtkWidget *box_info, *sshyp_logo, *label_home1; // info page widgets + GtkWidget *box_browse_page, *box_browse_controls; // browse page boxes + GtkWidget *button_shear, *button_read, *button_edit, *button_copy; // browse buttons + GtkWidget *scrollable; // scrollable entry list container + + window = gtk_application_window_new(app); + gtk_window_set_title(GTK_WINDOW(window), ""); + gtk_window_set_default_size(GTK_WINDOW(window), 360, 720); + + header_bar = gtk_header_bar_new(); + gtk_header_bar_set_show_title_buttons(GTK_HEADER_BAR(header_bar), FALSE); + + // !!start main stack stuff!! + + // create the stack + stack = gtk_stack_new(); + gtk_stack_set_transition_type(GTK_STACK(stack), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT); + + // attach the stack to the main window + gtk_window_set_child(GTK_WINDOW(window), stack); + + // browse + button_shear = gtk_button_new_from_icon_name("edit-delete"); + gtk_widget_set_hexpand(button_shear, FALSE); + gtk_widget_set_size_request(button_shear, 50, -1); + g_signal_connect_swapped(button_shear, "clicked", G_CALLBACK(are_they_sure), window); + button_read = gtk_button_new_from_icon_name("mail-read"); + gtk_widget_set_hexpand(button_read, TRUE); + button_edit = gtk_button_new_from_icon_name("document-edit"); + gtk_widget_set_hexpand(button_edit, TRUE); + g_signal_connect(button_edit, "clicked", G_CALLBACK(copy_edit_field_prompt), (gpointer *) window); + button_copy = gtk_button_new_from_icon_name("edit-copy"); + gtk_widget_set_hexpand(button_copy, TRUE); + g_signal_connect(button_copy, "clicked", G_CALLBACK(copy_edit_field_prompt), (gpointer *) window); + box_browse_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + label_selected = gtk_label_new("no entry selected"); + gtk_widget_set_hexpand(label_selected, TRUE); + gtk_widget_set_halign(label_selected, GTK_ALIGN_CENTER); + box_browse_controls = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_valign(box_browse_controls, GTK_ALIGN_START); + gtk_widget_set_size_request(box_browse_controls, -1, 50); + gtk_box_append(GTK_BOX(box_browse_controls), button_shear); + gtk_box_append(GTK_BOX(box_browse_controls), button_read); + gtk_box_append(GTK_BOX(box_browse_controls), button_edit); + gtk_box_append(GTK_BOX(box_browse_controls), button_copy); + gtk_box_append(GTK_BOX(box_browse_page), label_selected); + gtk_box_append(GTK_BOX(box_browse_page), box_browse_controls); + + // !!start scrollable list!! + scrollable = gtk_scrolled_window_new(); + list_box = (GtkListBox *) gtk_list_box_new(); + gtk_widget_set_vexpand(GTK_WIDGET(list_box), TRUE); + + entry_list_gen(); + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrollable), GTK_WIDGET(list_box)); + // !!end scrollable list!! + + gtk_box_append(GTK_BOX(box_browse_page), GTK_WIDGET(scrollable)); + + // info + box_info = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + sshyp_logo = gtk_image_new_from_file("PLACEHOLDER"); + gtk_image_set_pixel_size(GTK_IMAGE(sshyp_logo), 300); + label_home1 = gtk_label_new("password pasture\nexperimental gui client for sshyp"); + gtk_label_set_justify(GTK_LABEL(label_home1), GTK_JUSTIFY_CENTER); + gtk_label_set_markup(GTK_LABEL(label_home1), "password pasture\nexperimental gui client for sshyp"); + gtk_box_append(GTK_BOX(box_info), sshyp_logo); + gtk_box_append(GTK_BOX(box_info), label_home1); + + stack_switcher = gtk_stack_switcher_new(); + page_info = gtk_stack_add_titled(GTK_STACK(stack), box_info, "info", "info"); + gtk_stack_page_set_icon_name(GTK_STACK_PAGE(page_info), "help-about"); + gtk_stack_add_titled(GTK_STACK(stack), box_browse_page, "browse", "browse"); + gtk_stack_switcher_set_stack(GTK_STACK_SWITCHER(stack_switcher), GTK_STACK(stack)); + gtk_header_bar_pack_start(GTK_HEADER_BAR(header_bar), stack_switcher); + + // !!end main stack stuff!! + + // header bar buttons + button_sync = gtk_button_new_from_icon_name("view-refresh"); + g_signal_connect(button_sync, "clicked", G_CALLBACK(sshyp_sync), 0); + gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button_sync); + + button_unlock = gtk_button_new_from_icon_name("changes-allow"); + g_signal_connect_swapped(button_unlock, "clicked", G_CALLBACK(password_prompt), window); + gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button_unlock); + + //GtkWidget *button_debug = gtk_button_new_from_icon_name("applications-science"); + //g_signal_connect(button_debug, "clicked", G_CALLBACK(0), 0); + //gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), button_debug); + + gtk_window_set_titlebar(GTK_WINDOW(window), header_bar); + gtk_widget_show(window); +} + +int main(int argc, + char **argv) { + GtkApplication *app = gtk_application_new("org.rwinkhart.sshyp", G_APPLICATION_FLAGS_NONE); + g_signal_connect(app, "activate", G_CALLBACK(activate), 0); + int status = g_application_run(G_APPLICATION(app), argc, argv); + g_object_unref(app); + return status; +} diff --git a/extensions/password-pasture/requirements.txt b/extensions/password-pasture/requirements.txt new file mode 100644 index 0000000..bcf80ca --- /dev/null +++ b/extensions/password-pasture/requirements.txt @@ -0,0 +1,3 @@ +arch: base-devel gtk4 cmake make gcc + +alpine: build-base gtk4.0 gtk4.0-dev cmake make gcc binutils abuild diff --git a/extensions/sshyp-mfa/sshyp-mfa.ext b/extensions/sshyp-mfa/sshyp-mfa.ext new file mode 100644 index 0000000..0f9a762 --- /dev/null +++ b/extensions/sshyp-mfa/sshyp-mfa.ext @@ -0,0 +1,3 @@ +[config] +input = copy -m +output = sshyp-mfa diff --git a/extensions/sshyp-mfa/sshyp-mfa.py b/extensions/sshyp-mfa/sshyp-mfa.py new file mode 100755 index 0000000..62c625b --- /dev/null +++ b/extensions/sshyp-mfa/sshyp-mfa.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +from base64 import b32decode +from configparser import ConfigParser +from os import environ, listdir, uname +from os.path import expanduser, isdir, isfile +from sshyp import decrypt, whitelist_verify +from subprocess import PIPE, Popen, run +from sys import argv, exit as s_exit +from time import sleep, strftime, time +home = expanduser("~") + + +def totp(_secret, _algo, _digits, _period): # uses provided information to generate a standard totp key + from hmac import new as new_mac + from struct import pack, unpack + _secret = b32decode(_secret.upper() + '=' * ((8 - len(_secret)) % 8)) + _counter = pack('>Q', int(time() / _period)) + _mac = new_mac(_secret, _counter, _algo).digest() + _offset = _mac[-1] & 0x0f + _binary = unpack('>L', _mac[_offset:_offset + 4])[0] & 0x7fffffff + return str(_binary)[-_digits:].zfill(_digits) + + +def mfa_read_shortcut(): # extracts MFA info from the user-specified sshyp entry + if not isfile(f"{directory}{arguments[0]}.gpg"): + print(f"\n\u001b[38;5;9merror: entry ({arguments[0]}) does not exist\u001b[0m\n") + s_exit(1) + if quick_unlock_enabled == 'true': + _mfa_data = decrypt(directory + arguments[0], _quick_pass=whitelist_verify(port, username_ssh, ip, device_id)) + else: + _mfa_data = decrypt(directory + arguments[0]) + try: + _type = _mfa_data[4].split('otpauth://')[1].split('/')[0] + _secret = _mfa_data[4].split('?secret=')[1].split('&issuer=')[0] + _algo = _mfa_data[4].split('&algorithm=')[1].split('&digits=')[0] + _digits = int(_mfa_data[4].split('&digits=')[1].split('&period=')[0]) + _period = int(_mfa_data[4].split('&period=')[1]) + return _type, _secret, _algo, _digits, _period + except IndexError: + print(f"\n\u001b[38;5;9merror: entry ({arguments[0]}) does not contain valid mfa data\u001b[0m\n") + s_exit(1) + + +if __name__ == '__main__': + # argument fetcher + arguments = argv[1:] + if len(arguments) < 1 or not arguments[0].startswith('/'): + print("\nsshyp-mfa extension usage: sshyp copy -m\n\nrun 'man sshyp-mfa' for more information\n") + s_exit(1) + + # user data fetcher + sshyp_data = ConfigParser() + sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini") + quick_unlock_enabled = sshyp_data.get('CLIENT-ONLINE', 'quick_unlock_enabled') + username_ssh = sshyp_data.get('SSHYNC', 'user') + ip = sshyp_data.get('SSHYNC', 'ip') + port = sshyp_data.get('SSHYNC', 'port') + directory = sshyp_data.get('SSHYNC', 'local_dir') + device_id = listdir(f"{home}/.config/sshyp/devices")[0] + + # main process: runs functions to generate MFA key, then continuously copies up-to-date MFA key to clipboard + try: + mfa_data, copied = mfa_read_shortcut(), None + print('\nmfa key copied to clipboard\n\nuntil this process is closed, your clipboard will be automatically ' + 'updated with the newest mfa key') + while True: + if str(int(strftime('%S'))/mfa_data[4]).endswith('.0') or copied is None: + if copied is None: + copied = 1 + if mfa_data[0] == 'steam': + from steam.guard import generate_twofactor_code as steam_totp + _mfa_key = steam_totp(b32decode(mfa_data[1])) + else: + _mfa_key = totp(mfa_data[1], mfa_data[2], mfa_data[3], mfa_data[4]) + if 'WSL_DISTRO_NAME' in environ: # WSL clipboard detection + run(('powershell.exe', '-c', 'Set-Clipboard', _mfa_key)) + elif 'WAYLAND_DISPLAY' in environ: # Wayland clipboard detection + run(('wl-copy', _mfa_key)) + elif uname()[0] == 'Haiku': # Haiku clipboard detection + run(('clipboard', '-c', _mfa_key)) + elif uname()[0] == 'Darwin': # MacOS clipboard detection + run('pbcopy', stdin=Popen(('printf', _mfa_key), stdout=PIPE).stdout) + elif isdir("/data/data/com.termux"): # Termux (Android) clipboard detection + run(('termux-clipboard-set', _mfa_key)) + else: # X11 clipboard detection + run(('xclip', '-sel', 'c'), stdin=Popen(('printf', _mfa_key), stdout=PIPE).stdout) + sleep(1) + except KeyboardInterrupt: + print('\n') + s_exit(0) diff --git a/pointers/v1.5.0 b/pointers/v1.5.0 new file mode 100644 index 0000000..a39c3b0 --- /dev/null +++ b/pointers/v1.5.0 @@ -0,0 +1,6 @@ +[sshyp-mfa] +supported = true +desc = read mfa data from sshyp entries to generate and copy totp keys to the clipboard (optional Steam support) +usage = sshyp / copy -m +py = https://raw.githubusercontent.com/rwinkhart/sshyp-labs/v1.5.0.1/sshyp-mfa/sshyp-mfa.py +ext = https://raw.githubusercontent.com/rwinkhart/sshyp-labs/v1.5.0.1/sshyp-mfa/sshyp-mfa.ext