mirror of
https://github.com/rwinkhart/sshyp-labs.git
synced 2026-08-28 12:56:33 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb1aca70bc | ||
|
|
80425bec2e | ||
|
|
d72c4a5c12 | ||
|
|
d339e4c734 | ||
|
|
240939178e | ||
|
|
b98122f4d8 | ||
|
|
7be5509fa7 | ||
|
|
8413c305c2 | ||
|
|
25a5731936 | ||
|
|
286645dd31 | ||
|
|
ec633c5104 | ||
|
|
9540e9874f | ||
|
|
0020ac313b | ||
|
|
ee8d875095 | ||
|
|
5d1eeb1f75 | ||
|
|
769f522b2a | ||
|
|
018912cfc5 | ||
|
|
d53fc304d5 | ||
|
|
adcb13fd89 | ||
|
|
2421bc2ff3 | ||
|
|
0a4efc2002 | ||
|
|
90884b4cd7 | ||
|
|
bc09ec5e30 | ||
|
|
e9a1728c6f | ||
|
|
08bc5dec87 | ||
|
|
23be7f8c30 | ||
|
|
0bbcda37f2 | ||
|
|
19ff50a5db | ||
|
|
ac035db597 | ||
|
|
fb267085eb | ||
|
|
90c3de83b5 | ||
|
|
a635207cee | ||
|
|
b9b06a4800 | ||
|
|
4e76b641a6 | ||
|
|
ed40b01e85 | ||
|
|
c8e54cef43 | ||
|
|
8fe7675ca3 | ||
|
|
45fd0631ca | ||
|
|
d4ed9b0da1 | ||
|
|
0953f60d0b | ||
|
|
580d511a50 | ||
|
|
da6b1658a4 | ||
|
|
5df741726f | ||
|
|
ea020c4bf1 | ||
|
|
3568601520 | ||
|
|
e9ee114452 | ||
|
|
8c1a3c340c | ||
|
|
0ad86a1040 | ||
|
|
931d7a0494 | ||
|
|
f0eb398f07 | ||
|
|
1216d06975 | ||
|
|
8e45b15fee | ||
|
|
6ee49d6b95 | ||
|
|
7b03cfdc5e | ||
|
|
45caab270e | ||
|
|
1c6150afe4 | ||
|
|
8916f9b2e2 | ||
|
|
c0281a4dc3 | ||
|
|
37efe8f96a | ||
|
|
2271281683 | ||
|
|
c5c3600e6d | ||
|
|
a97aa6052a | ||
|
|
d133406de9 | ||
|
|
25f608dd1e | ||
|
|
a957d80a67 | ||
|
|
521714fd12 | ||
|
|
3c80d72a36 | ||
|
|
68c370be55 | ||
|
|
bd09ab32cd | ||
|
|
755b1183e3 | ||
|
|
9c67f41199 | ||
|
|
951cb5adc5 | ||
|
|
4227a264c8 | ||
|
|
d19cc1ed2c | ||
|
|
04ab10fdd9 | ||
|
|
fd5266351e | ||
|
|
73947dd7ef | ||
|
|
fcf43986a8 | ||
|
|
dfb90ca294 | ||
|
|
42ef63c0d9 | ||
|
|
939d07e804 | ||
|
|
be41d32f78 | ||
|
|
ba2ac73965 | ||
|
|
ebb420d30f | ||
|
|
8cf5728c42 | ||
|
|
9e0c121843 |
@@ -0,0 +1 @@
|
||||
*
|
||||
@@ -1,17 +1,21 @@
|
||||
# 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 soon to be the home of 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 most sshyp-supported platforms, sshyp extensions should be installed from the `sshyp tweak` menu's "extension management" option.
|
||||
|
||||
For **Haiku and Termux _ONLY_**, use the packages from the [releases page](https://github.com/rwinkhart/sshyp-labs/releases).
|
||||
|
||||
# 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 (includes Steam support)
|
||||
|
||||
sshyp-mfa is a unique approach to generating multi-factor authentication keys. Upon running `sshyp-mfa <target entry>`,
|
||||
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](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.
|
||||
|
||||
sshyp-mfa's TOTP support is partially derrived from [susam/mintotp](https://github.com/susam/mintotp).
|
||||
|
||||
sshyp-mfa's Steam support is partially derrived from [ValvePython/steam](https://github.com/ValvePython/steam).
|
||||
|
||||
These packages are _not_ required as dependencies; the necessary code from each is included in sshyp-mfa.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
git add -f LICENSE README.md commit.sh extensions extra pointers .gitignore
|
||||
git commit -m "$1"
|
||||
git push
|
||||
@@ -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})
|
||||
Executable
+5
@@ -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)
|
||||
@@ -0,0 +1,380 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
#include <stddef.h>
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
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<entry_array_count; i++){
|
||||
for(int j=0; j<entry_array_count-1-i; j++){
|
||||
if(strcmp(entries[j], entries[j+1]) > 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), "<span size='large'><b>password pasture</b></span>\n<span size='small'>experimental gui client for sshyp</span>");
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
arch: base-devel gtk4 cmake make gcc
|
||||
|
||||
alpine: build-base gtk4.0 gtk4.0-dev cmake make gcc binutils abuild
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
|
||||
version='1.5.1.3'
|
||||
if [ -z "$2" ]; then
|
||||
revision=1
|
||||
else
|
||||
revision="$2"
|
||||
fi
|
||||
|
||||
_create_hpkg() {
|
||||
printf '\npackaging for Haiku...\n'
|
||||
mkdir -p output/haikutemp/lib/sshyp/extensions
|
||||
printf "name sshyp_mfa
|
||||
version "$version"-"$revision"
|
||||
architecture any
|
||||
summary \"An MFA (TOTP/Steam) key generator for the sshyp password manager\"
|
||||
description \"sshyp-mfa is an extension for the sshyp password manager that reads MFA data from sshyp entries and generates generic TOTP and Steam keys.\"
|
||||
packager \"Randall Winkhart <idgr at tutanota dot com>\"
|
||||
vendor \"Randall Winkhart\"
|
||||
licenses {
|
||||
\"GNU GPL v3\"
|
||||
}
|
||||
copyrights {
|
||||
\"2021-2023 Randall Winkhart\"
|
||||
}
|
||||
provides {
|
||||
sshyp_mfa = "$version"
|
||||
}
|
||||
requires {
|
||||
sshyp_client
|
||||
}
|
||||
urls {
|
||||
\"https://github.com/rwinkhart/sshyp-labs\"
|
||||
}
|
||||
" > output/haikutemp/.PackageInfo
|
||||
cp ./sshyp-mfa.py output/haikutemp/lib/sshyp/sshyp-mfa
|
||||
printf '[config]\ninput = copy -m\noutput = /system/lib/sshyp/sshyp-mfa\n' > ./output/haikutemp/lib/sshyp/extensions/sshyp-mfa.ini
|
||||
sed -i '1 s/.*/#!\/bin\/env\ python3.11/' output/haikutemp/lib/sshyp/sshyp-mfa
|
||||
cd output/haikutemp
|
||||
package create -b HAIKU-sshyp_mfa-"$version"-"$revision"_all.hpkg
|
||||
package add HAIKU-sshyp_mfa-"$version"-"$revision"_all.hpkg lib
|
||||
cd ../..
|
||||
mv output/haikutemp/HAIKU-sshyp_mfa-"$version"-"$revision"_all.hpkg output/
|
||||
rm -rf output/haikutemp
|
||||
printf "\nHaiku packaging complete\n\n"
|
||||
} &&
|
||||
|
||||
_create_termux() {
|
||||
printf '\npackaging for Termux...\n'
|
||||
mkdir -p output/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/DEBIAN \
|
||||
output/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/data/data/com.termux/files/usr/lib/sshyp/extensions
|
||||
printf "Package: sshyp-mfa
|
||||
Version: $version
|
||||
Section: utils
|
||||
Architecture: all
|
||||
Maintainer: Randall Winkhart <idgr at tutanota dot com>
|
||||
Description: An MFA (TOTP/Steam) key generator for the sshyp password manager
|
||||
Depends: sshyp-client
|
||||
Priority: optional
|
||||
Installed-Size: 100
|
||||
" > output/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/DEBIAN/control
|
||||
cp ./sshyp-mfa.py output/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/data/data/com.termux/files/usr/lib/sshyp/sshyp-mfa
|
||||
printf '[config]\ninput = copy -m\noutput = /data/data/com.termux/files/usr/lib/sshyp/sshyp-mfa\n' > ./output/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/data/data/com.termux/files/usr/lib/sshyp/extensions/sshyp-mfa.ini
|
||||
dpkg-deb --build --root-owner-group output/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/
|
||||
mv output/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux.deb output/TERMUX-sshyp-mfa_"$version"-"$revision"_all.deb
|
||||
rm -rf output/termuxtemp
|
||||
printf '\nTermux packaging complete\n\n'
|
||||
} &&
|
||||
|
||||
case "$1" in
|
||||
haiku)
|
||||
_create_hpkg
|
||||
;;
|
||||
termux)
|
||||
_create_termux
|
||||
;;
|
||||
*)
|
||||
printf '\nusage: package.sh [target] <revision>\n\ntargets: haiku termux\n\n'
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,3 @@
|
||||
[config]
|
||||
input = copy -m
|
||||
output = /usr/lib/sshyp/sshyp-mfa
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
from base64 import b32decode
|
||||
from configparser import ConfigParser
|
||||
from hmac import new as hmac_new
|
||||
from os import environ, listdir, uname
|
||||
from os.path import expanduser, isdir, isfile
|
||||
from sshyp import decrypt, whitelist_verify
|
||||
from struct import pack, unpack
|
||||
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
|
||||
_secret = b32decode(_secret.upper() + '=' * ((8 - len(_secret)) % 8))
|
||||
_counter = pack('>Q', int(time() / _period))
|
||||
_hmac = hmac_new(_secret, _counter, _algo).digest()
|
||||
_offset = _hmac[-1] & 0x0f
|
||||
_binary = unpack('>L', _hmac[_offset:_offset + 4])[0] & 0x7fffffff
|
||||
return str(_binary)[-_digits:].zfill(_digits)
|
||||
|
||||
|
||||
def steam_otp(_secret): # uses provided information to generate a Steam-compatible otp
|
||||
_hmac = hmac_new(bytes(_secret), msg=pack('>Q', int(time()//30)), digestmod='sha1').digest()
|
||||
_start = ord(_hmac[19:20]) & 0xF
|
||||
_codeint = unpack('>I', _hmac[_start:_start+4])[0] & 0x7fffffff
|
||||
_charset = '23456789BCDFGHJKMNPQRTVWXY'
|
||||
_code = ''
|
||||
for _ in range(5):
|
||||
_codeint, _i = divmod(_codeint, len(_charset))
|
||||
_code += _charset[_i]
|
||||
return _code
|
||||
|
||||
|
||||
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:
|
||||
_mfa_data = decrypt(directory + arguments[0],
|
||||
_quick_pass=whitelist_verify(sshyp_data.get('SSHYNC', 'port'),
|
||||
sshyp_data.get('SSHYNC', 'user'),
|
||||
sshyp_data.get('SSHYNC', 'ip'),
|
||||
listdir(f"{home}/.config/sshyp/devices")[0],
|
||||
sshyp_data.get('SSHYNC', 'identity_file')))
|
||||
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 </entry name> 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")
|
||||
directory = f"{home}/.local/share/sshyp/"
|
||||
quick_unlock_enabled = sshyp_data.getboolean('CLIENT-ONLINE', 'quick_unlock_enabled')
|
||||
|
||||
# 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':
|
||||
_mfa_key = steam_otp(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', stdin=Popen(('printf', _mfa_key), stdout=PIPE).stdout)
|
||||
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:
|
||||
s_exit(0)
|
||||
Executable → Regular
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -1,41 +0,0 @@
|
||||
#!/bin/python3
|
||||
|
||||
from os import path, remove, system, walk
|
||||
from pathlib import Path
|
||||
from shutil import move, rmtree
|
||||
|
||||
if __name__ == "__main__":
|
||||
gpg_id = input('\nWhat is the ID of the GPG key you would like to use for re-encryption?\n\nID: ')
|
||||
directory = path.expanduser('~/.local/share/sshyp')
|
||||
if Path(f"{directory}.new").exists():
|
||||
rmtree(f"{directory}.new")
|
||||
if Path(f"{directory}.old").exists():
|
||||
rmtree(f"{directory}.old")
|
||||
if path.isdir(directory):
|
||||
for dirPath, dirNames, filenames in walk(directory):
|
||||
for filename in sorted(filenames):
|
||||
Path(dirPath.replace(directory, directory + '.new')).mkdir(0o700, parents=True, exist_ok=True)
|
||||
system(f"gpg -d '{dirPath}/{filename}' "
|
||||
f"> '{dirPath.replace(directory, directory + '.new')}/{filename[:-4]}'")
|
||||
_old_contents, _new_contents = \
|
||||
open(f"{dirPath.replace(directory, directory + '.new')}/{filename[:-4]}", 'r').readlines(), ''
|
||||
for _num in range(len(_old_contents)):
|
||||
if _num == 3:
|
||||
if _old_contents[_num] == ' ' or _old_contents[_num] == '\n':
|
||||
pass
|
||||
else:
|
||||
_new_contents += _old_contents[_num]
|
||||
else:
|
||||
_new_contents += _old_contents[_num]
|
||||
for _num in reversed(range(len(_new_contents))):
|
||||
if _new_contents[_num] == '\n' or _new_contents[_num] == '':
|
||||
_new_contents = _new_contents[:-1]
|
||||
else:
|
||||
break
|
||||
open(f"{dirPath.replace(directory, directory + '.new')}/{filename[:-4]}", 'w').writelines(_new_contents)
|
||||
system(f"gpg -qr {str(gpg_id)} -e '{dirPath.replace(directory, directory + '.new')}/{filename[:-4]}'")
|
||||
remove(f"{dirPath.replace(directory, directory + '.new')}/{filename[:-4]}")
|
||||
else:
|
||||
print(f"\nError: no entry folder ({directory}) found\n")
|
||||
move(directory, f"{directory}.old")
|
||||
move(f"{directory}.new", directory)
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/python3
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from os import path, remove, system, walk
|
||||
from pathlib import Path
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from os import path, system, uname, walk
|
||||
from pathlib import Path
|
||||
from shutil import move, rmtree
|
||||
from sshyp import decrypt, encrypt, optimized_edit, shm_gen
|
||||
|
||||
if __name__ == "__main__":
|
||||
directory, tmp_dir = path.expanduser('~/.local/share/sshyp'), path.expanduser('~/.config/sshyp/tmp/')
|
||||
if uname()[0] == 'Haiku': # set proper gpg command for OS
|
||||
gpg = 'gpg --pinentry-mode loopback'
|
||||
else:
|
||||
gpg = 'gpg'
|
||||
system(f"{gpg} -k")
|
||||
gpg_id = input('\ngpg id for re-encryption: ')
|
||||
if Path(f"{directory}.new").exists():
|
||||
rmtree(f"{directory}.new")
|
||||
if Path(f"{directory}.old").exists():
|
||||
rmtree(f"{directory}.old")
|
||||
if path.isdir(directory):
|
||||
print('\nconverting... please wait, do not force close this process')
|
||||
for dirPath, dirNames, filenames in walk(directory):
|
||||
for filename in sorted(filenames):
|
||||
Path(dirPath.replace(directory, directory + '.new')).mkdir(0o700, parents=True, exist_ok=True)
|
||||
_shm_folder, _shm_entry = shm_gen()
|
||||
decrypt(f"{dirPath}/{filename[:-4]}", _shm_folder, _shm_entry, gpg)
|
||||
_new_lines = optimized_edit(open(f"{tmp_dir}{_shm_folder}/{_shm_entry}", 'r').readlines(), None, -1)
|
||||
open(f"{tmp_dir}{_shm_folder}/{_shm_entry}", 'w').writelines(_new_lines)
|
||||
encrypt(f"{dirPath.replace(directory, directory + '.new', 1)}/{filename[:-4]}",
|
||||
_shm_folder, _shm_entry, gpg, gpg_id)
|
||||
move(directory, f"{directory}.old")
|
||||
move(f"{directory}.new", directory)
|
||||
else:
|
||||
print(f"\nError: no entry folder ({directory}) found\n")
|
||||
@@ -0,0 +1,5 @@
|
||||
[sshyp-mfa]
|
||||
desc = read mfa data from sshyp entries to generate and copy totp keys to the clipboard (optional Steam support)
|
||||
usage = sshyp /<entry name> copy -m
|
||||
exe = https://raw.githubusercontent.com/rwinkhart/sshyp-labs/v1.5.0.2/extensions/sshyp-mfa/sshyp-mfa.py
|
||||
ini = https://raw.githubusercontent.com/rwinkhart/sshyp-labs/v1.5.0.2/extensions/sshyp-mfa/sshyp-mfa.ini
|
||||
@@ -0,0 +1,5 @@
|
||||
[sshyp-mfa]
|
||||
desc = read mfa data from sshyp entries to generate and copy totp keys to the clipboard (optional Steam support)
|
||||
usage = sshyp /<entry name> copy -m
|
||||
exe = https://raw.githubusercontent.com/rwinkhart/sshyp-labs/v1.5.1.2/extensions/sshyp-mfa/sshyp-mfa.py
|
||||
ini = https://raw.githubusercontent.com/rwinkhart/sshyp-labs/v1.5.1.2/extensions/sshyp-mfa/sshyp-mfa.ini
|
||||
@@ -0,0 +1,5 @@
|
||||
[sshyp-mfa]
|
||||
desc = reads mfa/2fa secrets from sshyp entries to generate and copy totp/Steam otp keys to the clipboard
|
||||
usage = sshyp /<entry name> copy -m<br><br>to add mfa data to a sshyp entry, insert it into the SECOND notes line for a given entry using the following format:<br><br> otpauth://<OTP METHOD>/<ACCOUNT NAME, NOT USED>?secret=<SECRET>&issuer=<ISSUER, NOT USED>&algorithm=<ALGORITHM>&digits=<DIGITS>&period=<REFRESH PERIOD><br><br>what to put in each of the above spaces:<br><br> <OTP METHOD> is almost always 'totp', but in the case of Steam, it needs to be set to 'steam'.<br> <ACCOUNT NAME, NOT USED> is a part of the Authenticator backup format, but it is not used in sshyp-mfa. Set to anything.<br> <SECRET> refers to the secret used to generate your MFA key. This is usually directly provided by the issuer, but it is sometimes hidden and more easily retrieved by copying it from a QR-compatible MFA app (such as Aegis).<br> <ISSUER, NOT USED> is a part of the Authenticator backup format, but it is not used in sshyp-mfa. Set to anything.<br> <ALGORITHM> refers to the algorithm used to generate your MFA key based on your secret. This is almost always 'sha1'.<br> <DIGITS> refers to the intended length of your MFA key. This is almost always '6', but in the case of Steam, it needs to be set to '5'.<br> <REFRESH PERIOD> refers to the interval at which a new MFA key needs to be generated. This is almost always '30', for 30 seconds.<br><br>examples:<br><br> GitHub (standard 6-digit totp):<br> otpauth://totp/MyNameIsBob?secret=YUGBSG65SG9SDBSDF56SBFVSC86SBVD6&issuer=GitHub&algorithm=sha1&digits=6&period=30<br><br> Steam (5-character totp):<br> otpauth://steam/SteamUser?secret=VGVG34GH2GJHVCK7HGVS7&issuer=Steam&algorithm=sha1&digits=5&period=30<br><br>for more information, visit:<br><br> https://github.com/rwinkhart/sshyp-labs/wiki/sshyp-mfa
|
||||
exe = https://raw.githubusercontent.com/rwinkhart/sshyp-labs/v1.5.1.3/extensions/sshyp-mfa/sshyp-mfa.py
|
||||
ini = https://raw.githubusercontent.com/rwinkhart/sshyp-labs/v1.5.1.3/extensions/sshyp-mfa/sshyp-mfa.ini
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/bin/python3
|
||||
|
||||
from base64 import b32decode
|
||||
from hmac import new as new_mac
|
||||
from os import environ, path, system, uname
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from sshync import get_profile
|
||||
from sshyp import decrypt, entry_list_gen, shm_gen
|
||||
from struct import pack, unpack
|
||||
from sys import argv, exit as s_exit
|
||||
from time import sleep, strftime, time
|
||||
try:
|
||||
from steam.guard import generate_twofactor_code as steam_totp
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
pass
|
||||
|
||||
|
||||
def totp(_secret, _algo, _digits, _period): # uses provided information to generate a standard totp key
|
||||
_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(): # reads and extracts MFA info from the user-specified sshyp entry
|
||||
if not Path(f"{directory}{argument}.gpg").exists():
|
||||
print(f"\n\u001b[38;5;9merror: entry ({argument}) does not exist\u001b[0m\n")
|
||||
s_exit(1)
|
||||
_shm_folder, _shm_entry = shm_gen()
|
||||
decrypt(directory + argument, _shm_folder, _shm_entry, gpg)
|
||||
try:
|
||||
_mfa_data = open(f"{path.expanduser('~/.config/sshyp/tmp/')}{_shm_folder}/{_shm_entry}", 'r').readlines()
|
||||
_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])
|
||||
rmtree(f"{path.expanduser('~/.config/sshyp/tmp/')}{_shm_folder}")
|
||||
return _type, _secret, _algo, _digits, _period
|
||||
except IndexError:
|
||||
print(f"\n\u001b[38;5;9merror: entry ({argument}) does not contain valid mfa data\u001b[0m\n")
|
||||
rmtree(f"{path.expanduser('~/.config/sshyp/tmp/')}{_shm_folder}")
|
||||
s_exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# argument fetcher
|
||||
argument_list = argv
|
||||
if not len(argv) == 1 and not argv[1].strip().startswith('/'):
|
||||
print(f"\n\u001b[38;5;9merror: invalid argument - run 'man sshyp-mfa' for usage information\u001b[0m\n")
|
||||
s_exit(1)
|
||||
|
||||
# user data fetcher
|
||||
ssh_info = get_profile(path.expanduser('~/.config/sshyp/sshyp.sshync'))
|
||||
directory = str(ssh_info[3].replace('\n', ''))
|
||||
if uname()[0] == 'Haiku': # set proper gpg command for OS
|
||||
gpg = 'gpg --pinentry-mode loopback'
|
||||
else:
|
||||
gpg = 'gpg'
|
||||
|
||||
# main process; runs functions to generate MFA key, then continuously copies up-to-date MFA key to clipboard
|
||||
try:
|
||||
if len(argument_list) == 1:
|
||||
entry_list_gen()
|
||||
argument_list.append(input('entry to read: '))
|
||||
argument = ' '.join(argument_list[1:]).replace('/', '', 1)
|
||||
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':
|
||||
_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 uname()[0] == 'Haiku': # Haiku clipboard detection
|
||||
system(f"clipboard -c '{_mfa_key}'")
|
||||
elif Path("/data/data/com.termux").exists(): # Termux (Android) clipboard detection
|
||||
system(f"termux-clipboard-set '{_mfa_key}'")
|
||||
elif environ.get('WAYLAND_DISPLAY') == 'wayland-0': # Wayland clipboard detection
|
||||
system(f"wl-copy '{_mfa_key}'")
|
||||
else: # X11 clipboard detection
|
||||
system(f"echo -n '{_mfa_key}' | xclip -sel c")
|
||||
sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print('\n')
|
||||
s_exit()
|
||||
@@ -1,44 +0,0 @@
|
||||
.TH sshyp-mfa 1 "12 July 2022" "rolling" "sshyp-mfa man page"
|
||||
.SH NAME
|
||||
sshyp-mfa \- An MFA (TOTP/Steam) key generator for the sshyp password manager.
|
||||
.SH SYNOPSIS
|
||||
sshyp-mfa [/<entry name>]
|
||||
.SH DESCRIPTION
|
||||
sshyp-mfa is an extension for the sshyp password manager that reads MFA data from sshyp entries and generates generic TOTP and Steam keys.
|
||||
.SH EXAMPLES
|
||||
Viewing the entry database:
|
||||
sshyp-mfa
|
||||
|
||||
Generating and copying an MFA key for an existing entry saved as ~/.local/share/sshyp-mfa/development/github.gpg
|
||||
sshyp-mfa /development/github
|
||||
|
||||
.SH SETUP
|
||||
sshyp-mfa requires a pre-existing functional sshyp setup.
|
||||
|
||||
MFA keys are generated off of MFA data placed in the second line of a sshyp entry's notes field. This data needs to be added manually, through sshyp, and must be in Authenticator backup format.
|
||||
|
||||
It might be easiest to import all of your MFA keys into Authenticator, export them into plain text, then add all of the exported MFA data into the second notes line in their respective sshyp entries.
|
||||
.SH FORMAT
|
||||
Format: otpauth://<OTP METHOD>/<ACCOUNT NAME, NOT USED>?secret=<SECRET>&issuer=<ISSUER, NOT USED>&algorithm=<ALGORITHM>&digits=<DIGITS>&period=<REFRESH PERIOD>
|
||||
|
||||
Help! What do I put in each of those spaces?
|
||||
|
||||
<OTP METHOD> is almost always 'totp', but in the case of Steam, it needs to be set to 'steam'.
|
||||
|
||||
<ACCOUNT NAME, NOT USED> is a part of the Authenticator backup format, but it is not used in sshyp-mfa. Set to anything.
|
||||
|
||||
<SECRET> refers to the secret used to generate your MFA key. This is usually directly provided by the issuer, but it is sometimes hidden and more easily retrieved by copying it from a QR-compatible MFA app (such as Aegis).
|
||||
|
||||
<ISSUER, NOT USED> is a part of the Authenticator backup format, but it is not used in sshyp-mfa. Set to anything.
|
||||
|
||||
<ALGORITHM> refers to the algorithm used to generate your MFA key based on your secret. This is almost always 'sha1'.
|
||||
|
||||
<DIGITS> refers to the intended length of your MFA key. This is almost always '6', but in the case of Steam, it needs to be set to '5'.
|
||||
|
||||
<REFRESH PERIOD> refers to the interval at which a new MFA key needs to be generated. This is almost always '30', for 30 seconds.
|
||||
|
||||
Example configuration (for most services, see above for differences regarding Steam):
|
||||
|
||||
otpauth://totp/MyNameIsBob?secret=YUGBSG65SG9SDBSDF56SBFVSC86SBVD6&issuer=Github&algorithm=sha1&digits=6&period=30
|
||||
.SH AUTHOR
|
||||
Randall Winkhart (https://github.com/rwinkhart)
|
||||
@@ -1,231 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script packages sshyp-mfa (from source) for various UNIX(-like) environments.
|
||||
# Dependencies (Arch Linux): dpkg (packaging for Debian/Termux), freebsd-pkg (packaging for FreeBSD)
|
||||
# Dependencies (Fedora) (can only package for self): rpmdevtools
|
||||
# NOTE It is recommended to instead use the latest officially packaged and tagged release.
|
||||
|
||||
echo -e '\nOptions (please enter the number only):'
|
||||
echo -e '\nPackage Formats:\n\n1. Haiku\n2. Debian&Ubuntu Linux\n3. Fedora Linux\n4. FreeBSD\n5. Termux\n6. Generic (used for PKGBUILD/APKBUILD)'
|
||||
echo -e '\nBuild Scripts:\n\n7. Arch Linux (PKGBUILD)'
|
||||
echo -e '\nOther:\n\n8. All (generates all distribution packages (excluding Haiku and Fedora, as these must be packaged on their respective distributions) and build scripts)\n'
|
||||
read -n 1 -r -p "Distribution: " distro
|
||||
|
||||
echo -e '\n\nThe value entered in this field will only affect the version reported to the package manager. The latest source is used regardless.\n'
|
||||
read -r -p "Version number: " version
|
||||
|
||||
echo -e '\nThe value entered in this field will only affect the revision number for build scripts.\n'
|
||||
read -r -p "Revision number: " revision
|
||||
|
||||
if [ "$distro" == "7" ] || [ "$distro" == "8" ]; then
|
||||
echo -e '\nOptions (please enter the number only):'
|
||||
echo -e '\n1. GitHub Release Tag\n2. Local\n'
|
||||
read -r -p "Source (for build scripts): " source
|
||||
|
||||
if [ "$source" == "1" ]; then
|
||||
source='https://github.com/rwinkhart/sshyp-labs/releases/download/v$pkgver/sshyp-mfa-$pkgver.tar.xz'
|
||||
else
|
||||
source=local://sshyp-mfa-"$version".tar.xz
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p packages
|
||||
|
||||
if [ "$distro" == "1" ]; then
|
||||
echo -e '\nPackaging for Haiku...\n'
|
||||
mkdir -p packages/haikutemp/documentation/{man/man1,packages/sshyp-mfa}
|
||||
echo "name sshypmfa
|
||||
version "$version"-"$revision"
|
||||
architecture any
|
||||
summary \"An MFA (TOTP/Steam) key generator for the sshyp password manager\"
|
||||
description \"sshyp-mfa is an extension for the sshyp password manager that reads MFA data from sshyp entries and generates generic TOTP and Steam keys.\"
|
||||
packager \"Randall Winkhart <idgr at tutanota dot com>\"
|
||||
vendor \"Randall Winkhart\"
|
||||
licenses {
|
||||
\"GNU GPL v3\"
|
||||
}
|
||||
copyrights {
|
||||
\"2021-2022 Randall Winkhart\"
|
||||
}
|
||||
provides {
|
||||
sshypmfa = "$version"
|
||||
cmd:sshypmfa
|
||||
}
|
||||
requires {
|
||||
sshyp
|
||||
python3
|
||||
}
|
||||
urls {
|
||||
\"https://github.com/rwinkhart/sshyp-labs\"
|
||||
}
|
||||
" > packages/haikutemp/.PackageInfo
|
||||
cp -r bin packages/haikutemp/
|
||||
ln -s /bin/sshyp-mfa.py packages/haikutemp/bin/sshyp-mfa
|
||||
cp -r share/licenses/sshyp-mfa/ packages/haikutemp/documentation/packages/
|
||||
cp extra/manpage packages/haikutemp/documentation/man/man1/sshyp-mfa.1
|
||||
gzip packages/haikutemp/documentation/man/man1/sshyp-mfa.1
|
||||
cd packages/haikutemp
|
||||
package create -b sshyp-mfa-"$version"-"$revision"_all.hpkg
|
||||
package add sshyp-mfa-"$version"-"$revision"_all.hpkg bin documentation
|
||||
cd ../..
|
||||
mv packages/haikutemp/sshyp-mfa-"$version"-"$revision"_all.hpkg packages/
|
||||
rm -rf packages/haikutemp
|
||||
echo -e "\nHaiku packaging complete.\n"
|
||||
fi
|
||||
|
||||
if [ "$distro" == "2" ] || [ "$distro" == "8" ]; then
|
||||
echo -e '\nPackaging for Debian...\n'
|
||||
mkdir -p packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all/{DEBIAN,usr/share/man/man1}
|
||||
echo "Package: sshyp-mfa
|
||||
Version: $version
|
||||
Section: utils
|
||||
Architecture: all
|
||||
Maintainer: Randall Winkhart <idgr at tutanota dot com>
|
||||
Description: An MFA (TOTP/Steam) key generator for the sshyp password manager
|
||||
Depends: sshyp, python3
|
||||
Priority: optional
|
||||
Installed-Size: 100
|
||||
" > packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all/DEBIAN/control
|
||||
cp -r bin packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all/usr/
|
||||
ln -s /usr/bin/sshyp-mfa.py packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all/usr/bin/sshyp-mfa
|
||||
cp -r share packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all/usr/
|
||||
cp extra/manpage packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all/usr/share/man/man1/sshyp-mfa.1
|
||||
gzip packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all/usr/share/man/man1/sshyp-mfa.1
|
||||
dpkg-deb --build --root-owner-group packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all/
|
||||
mv packages/debiantemp/sshyp-mfa_"$version"-"$revision"_all.deb packages/
|
||||
rm -rf packages/debiantemp
|
||||
echo -e "\nDebian packaging complete.\n"
|
||||
fi
|
||||
|
||||
if [ "$distro" == "5" ] || [ "$distro" == "8" ]; then
|
||||
echo -e '\nPackaging for Termux...\n'
|
||||
mkdir -p packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/{data/data/com.termux/files/usr/share/man/man1,DEBIAN}
|
||||
echo "Package: sshyp-mfa
|
||||
Version: $version
|
||||
Section: utils
|
||||
Architecture: all
|
||||
Maintainer: Randall Winkhart <idgr at tutanota dot com>
|
||||
Description: An MFA (TOTP/Steam) key generator for the sshyp password manager
|
||||
Depends: sshyp, python3
|
||||
Priority: optional
|
||||
Installed-Size: 100
|
||||
" > packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/DEBIAN/control
|
||||
cp -r bin packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/data/data/com.termux/files/usr/
|
||||
ln -s /data/data/com.termux/files/usr/bin/sshyp-mfa.py packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/data/data/com.termux/files/usr/bin/sshyp-mfa
|
||||
cp -r share packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/data/data/com.termux/files/usr/
|
||||
cp extra/manpage packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/data/data/com.termux/files/usr/share/man/man1/sshyp-mfa.1
|
||||
gzip packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/data/data/com.termux/files/usr/share/man/man1/sshyp-mfa.1
|
||||
dpkg-deb --build --root-owner-group packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux/
|
||||
mv packages/termuxtemp/sshyp-mfa_"$version"-"$revision"_all_termux.deb packages/
|
||||
rm -rf packages/termuxtemp
|
||||
echo -e "\nTermux packaging complete.\n"
|
||||
fi
|
||||
|
||||
if [ "$distro" == "3" ] || [ "$distro" == "4" ] || [ "$distro" == "6" ] || [ "$distro" == "7" ] || [ "$distro" == "8" ]; then
|
||||
echo -e '\nPackaging as generic...\n'
|
||||
mkdir -p packages/generictemp/usr/share/man/man1
|
||||
cp -r bin packages/generictemp/usr/
|
||||
ln -s /usr/bin/sshyp-mfa.py packages/generictemp/usr/bin/sshyp-mfa
|
||||
cp -r share packages/generictemp/usr/
|
||||
cp extra/manpage packages/generictemp/usr/share/man/man1/sshyp-mfa.1
|
||||
gzip packages/generictemp/usr/share/man/man1/sshyp-mfa.1
|
||||
tar -C packages/generictemp -cvf packages/sshyp-mfa-"$version".tar.xz usr/
|
||||
rm -rf packages/generictemp
|
||||
sha512="$(sha512sum packages/sshyp-mfa-"$version".tar.xz | awk '{print $1;}')"
|
||||
echo -e "\nsha512 sum:\n$sha512"
|
||||
echo -e "\nGeneric packaging complete.\n"
|
||||
fi
|
||||
|
||||
if [ "$distro" == "3" ]; then
|
||||
echo -e '\nPackaging for Fedora...\n'
|
||||
rm -rf ~/rpmbuild
|
||||
rpmdev-setuptree
|
||||
cp packages/sshyp-mfa-"$version".tar.xz ~/rpmbuild/SOURCES
|
||||
echo "Name: sshyp-mfa
|
||||
Version: "$version"
|
||||
Release: "$revision"
|
||||
Summary: An MFA (TOTP/Steam) key generator for the sshyp password manager
|
||||
BuildArch: noarch
|
||||
|
||||
License: GPLv3
|
||||
URL: https://github.com/rwinkhart/sshyp-labs
|
||||
Source0: sshyp-mfa-"$version".tar.xz
|
||||
|
||||
Requires: sshyp python
|
||||
|
||||
%description
|
||||
sshyp-mfa is an extension for the sshyp password manager that reads MFA data from sshyp entries and generates generic TOTP and Steam keys.
|
||||
|
||||
%install
|
||||
tar xf %{_sourcedir}/sshyp-mfa-"$version".tar.xz -C %{_sourcedir}
|
||||
cp -r %{_sourcedir}/usr %{buildroot}
|
||||
|
||||
%files
|
||||
/usr/bin/sshyp-mfa
|
||||
/usr/bin/sshyp-mfa.py
|
||||
%license /usr/share/licenses/sshyp-mfa/license
|
||||
%doc /usr/share/man/man1/sshyp-mfa.1.gz
|
||||
" > ~/rpmbuild/SPECS/sshyp-mfa.spec
|
||||
rpmbuild -bb ~/rpmbuild/SPECS/sshyp-mfa.spec
|
||||
mv ~/rpmbuild/RPMS/noarch/* packages/
|
||||
rm -rf ~/rpmbuild
|
||||
echo -e "\nFedora packaging complete.\n"
|
||||
fi
|
||||
|
||||
if [ "$distro" == "4" ] || [ "$distro" == "8" ]; then
|
||||
echo -e '\nPackaging for FreeBSD...\n'
|
||||
mkdir -p packages/FreeBSDtemp/bin
|
||||
tar xf packages/sshyp-mfa-"$version".tar.xz -C packages/FreeBSDtemp
|
||||
echo "name: sshyp-mfa
|
||||
version: \""$version"\"
|
||||
abi = \"FreeBSD:13:*\";
|
||||
arch = \"freebsd:13:*\";
|
||||
origin: security/sshyp-mfa
|
||||
comment: \"a sshyp extension\"
|
||||
desc: \"an MFA (TOTP/Steam) key generator for the sshyp password manager\"
|
||||
maintainer: <idgr at tutanota dot com>
|
||||
www: https://github.com/rwinkhart/sshyp-labs
|
||||
prefix: /
|
||||
\"deps\" : {
|
||||
\"sshyp\" : {
|
||||
\"origin\" : \"security/sshyp\"
|
||||
},
|
||||
\"python\" : {
|
||||
\"origin\" : \"lang/python\"
|
||||
},
|
||||
},
|
||||
" > packages/FreeBSDtemp/+MANIFEST
|
||||
echo "/usr/bin/sshyp-mfa
|
||||
/usr/bin/sshyp-mfa.py
|
||||
/usr/share/licenses/sshyp-mfa/license
|
||||
/usr/share/man/man1/sshyp-mfa.1.gz
|
||||
" > packages/FreeBSDtemp/plist
|
||||
pkg create -m packages/FreeBSDtemp/ -r packages/FreeBSDtemp/ -p packages/FreeBSDtemp/plist -o packages/
|
||||
rm -rf packages/FreeBSDtemp
|
||||
echo -e "\nFreeBSD packaging complete.\n"
|
||||
fi
|
||||
|
||||
if [ "$distro" == "7" ] || [ "$distro" == "8" ]; then
|
||||
echo -e '\nGenerating PKGBUILD...'
|
||||
echo "# Maintainer: Randall Winkhart <idgr at tutanota dot com>
|
||||
|
||||
pkgname=sshyp-mfa
|
||||
pkgver="$version"
|
||||
pkgrel="$revision"
|
||||
pkgdesc='An MFA (TOTP/Steam) key generator for the sshyp password manager'
|
||||
url='https://github.com/rwinkhart/sshyp-labs'
|
||||
arch=('any')
|
||||
license=('GPL3')
|
||||
depends=(sshyp python)
|
||||
|
||||
source=(\""$source"\")
|
||||
sha512sums=('"$sha512"')
|
||||
|
||||
package() {
|
||||
|
||||
tar xf sshyp-mfa-"\"\$pkgver\"".tar.xz -C "\"\${pkgdir}\""
|
||||
|
||||
}
|
||||
" > packages/PKGBUILD
|
||||
echo -e "\nPKGBUILD generated.\n"
|
||||
fi
|
||||
@@ -1 +0,0 @@
|
||||
steam[client]
|
||||
@@ -1,15 +0,0 @@
|
||||
sshyp-mfa is a FOSS extension for the sshyp password manager.
|
||||
Copyright (C) 2022 Randall Winkhart idgr@tutanota.com
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
Reference in New Issue
Block a user