mirror of
https://github.com/rwinkhart/sshyp.git
synced 2026-09-03 15:47:18 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5439c899f7 | ||
|
|
0990bae310 | ||
|
|
ae21489c75 | ||
|
|
5717133168 | ||
|
|
07c0dee0a4 | ||
|
|
c85640804a | ||
|
|
8ec2b31c23 | ||
|
|
50189dd526 | ||
|
|
f13404e76f | ||
|
|
5d00f83df0 | ||
|
|
5ce28e1138 | ||
|
|
2f7b548360 | ||
|
|
6789e3e3de | ||
|
|
a345f4181c | ||
|
|
08c6f1aeef | ||
|
|
3bb2f194d4 | ||
|
|
52727e8eea | ||
|
|
cd560d6f8f | ||
|
|
18d03279b9 | ||
|
|
d4f1831486 | ||
|
|
0ecb842480 | ||
|
|
45be25e6bd | ||
|
|
acf46b6227 | ||
|
|
3418bf6170 | ||
|
|
dbf3b35237 | ||
|
|
c48edfc624 | ||
|
|
b2eb63cbe6 | ||
|
|
43f8306932 | ||
|
|
e044122d48 | ||
|
|
0e45a21a58 | ||
|
|
a119544fbf | ||
|
|
23e51ae133 | ||
|
|
27cf610a1f | ||
|
|
2edb1b9b53 | ||
|
|
675e05c14f | ||
|
|
937b1357f5 | ||
|
|
c3f8178db4 | ||
|
|
75cf927343 | ||
|
|
5a34cfbc8b | ||
|
|
c0d5d3b79f | ||
|
|
d45f4264df | ||
|
|
dbae72b926 | ||
|
|
da4a4d272d | ||
|
|
7e1961e0ec | ||
|
|
f80fd2b6e2 | ||
|
|
787a2bbfc4 | ||
|
|
e7f0618142 |
@@ -1,7 +1,7 @@
|
|||||||

|

|
||||||
|
|
||||||
[](https://github.com/rwinkhart/sshyp/releases)
|
[](https://github.com/rwinkhart/sshyp/releases)
|
||||||

|

|
||||||
[](https://github.com/rwinkhart/sshyp/releases)
|
[](https://github.com/rwinkhart/sshyp/releases)
|
||||||
|
|
||||||
[](https://github.com/rwinkhart/sshyp/actions/workflows/codeql-analysis.yml)
|
[](https://github.com/rwinkhart/sshyp/actions/workflows/codeql-analysis.yml)
|
||||||
@@ -14,7 +14,7 @@ sshyp is compatible with entries created by pass/password-store, as its original
|
|||||||
|
|
||||||
sshyp makes use of a custom sftp wrapper, called sshync (ssh+sync), to reliably sync user entries with a local or remote server.
|
sshyp makes use of a custom sftp wrapper, called sshync (ssh+sync), to reliably sync user entries with a local or remote server.
|
||||||
|
|
||||||
The name "sshyp" is a combination of its syncing library, "sshync", and "passwords".
|
The name "sshyp" is a combination of its synchronization library, "sshync", and "passwords".
|
||||||
|
|
||||||
# WARNING
|
# WARNING
|
||||||
It is your responsibility to assess the security and stability of "sshyp" before using it and ensure it meets your needs.
|
It is your responsibility to assess the security and stability of "sshyp" before using it and ensure it meets your needs.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
sed -i 's/[[:space:]]*$//' ./lib/* ./port-jobs/* ./package.sh
|
||||||
git add -f extra lib/sshyp.py lib/sshync.py lib/stweak.py lib/clipclear.py port-jobs share LICENSE README.md package.sh commit.sh .gitignore
|
git add -f extra lib/sshyp.py lib/sshync.py lib/stweak.py lib/clipclear.py port-jobs share LICENSE README.md package.sh commit.sh .gitignore
|
||||||
git commit -m "$1"
|
git commit -m "$1"
|
||||||
git push
|
git push
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
sshyp v1.5.2
|
||||||
|
01/07/2024
|
||||||
|
|
||||||
|
the fortified flock update - patch two
|
||||||
|
|
||||||
|
this release focuses on fixing various bugs, crashes, and visual oddities;
|
||||||
|
additionally, the new-user setup experience has been vastly improved
|
||||||
|
|
||||||
|
compatibility-breaking changes:
|
||||||
|
|
||||||
|
- this release is fully compatible with sshyp v1.5.0-v1.5.1
|
||||||
|
- the minimum Python version has been raised from Python 3.7 to Python 3.8
|
||||||
|
^ this was needed for the new device id changing behavior
|
||||||
|
^ Python 3.7 no longer receives security updates, so this should have minimal impact
|
||||||
|
^ please create an issue on GitHub if any Python version (3.8 or newer) does not work as intended
|
||||||
|
|
||||||
|
user-facing features:
|
||||||
|
|
||||||
|
- registered device ids can now be removed from the server tweak menu
|
||||||
|
^ all device id menus now stay open until the user manually exits
|
||||||
|
- changing a client's device id now attempts to automatically remove the previous id from the server
|
||||||
|
- the extension downloader now supports showing multiple lines of usage information
|
||||||
|
^ sshyp-mfa is taking advantage of this to display the info from the old man page
|
||||||
|
- basic tweak/init menu text wrapping and terminal-resizing has been added
|
||||||
|
^ the tweak/init radio menus now warn if the terminal needs to be resized to see all content;
|
||||||
|
this resizing can be done in real time without restarting sshyp (layout dynamically adjusts)
|
||||||
|
^ this avoids crashes on large radio menus in small terminals (crashes are still possible, but
|
||||||
|
only on unreasonably small terminals - this will probably stay the way it is now)
|
||||||
|
- ssh connections are no longer made from the init menu
|
||||||
|
^ this is to give the user time to register their ssh pubkey with the server
|
||||||
|
- the user is now warned that gpg key generation may take time and that sshyp should not be terminated
|
||||||
|
- the user is now warned about risks associated with changing or removing active device ids
|
||||||
|
- back buttons have been added to menus that were missing a safe way to exit
|
||||||
|
|
||||||
|
fixes/optimizations:
|
||||||
|
|
||||||
|
- quick-unlock has been fixed - invalid pins have been generated since v1.5.0
|
||||||
|
^ if quick-unlock is not working for you, re-generate your pin after this update!
|
||||||
|
- extension removal now works on Busybox environments (Alpine Linux)
|
||||||
|
- instead of crashing with an unhelpful error, sshyp now warns if a valid pinentry program is not found (gpg)
|
||||||
|
^ affects some Alpine Linux 3.19 base installs (pinentry does not always get installed with gpg)
|
||||||
|
- a recursive curses.wrapper() function is no longer used (fixes menu crash on Alpine Linux 3.19)
|
||||||
|
^ the new method of transitioning menus is more efficient on all platforms
|
||||||
|
- the wl-paste empty clipboard warning is no longer shown when multiple copies are done within 30 seconds (Wayland)
|
||||||
|
- quotes are no longer copied to the Termux clipboard on clear
|
||||||
|
- the entry reader and entry list should now be more visible in light terminals
|
||||||
|
^ in testing, I noticed some solarized dark themes bind color 8 to the same color as the terminal background,
|
||||||
|
leading to some invisible text - this will not be fixed (unless custom accent colors are added), as
|
||||||
|
I believe this is poor theme design and the fault of the particular solarized dark themes I tested
|
||||||
|
- entry list text wrapping has been vastly improved
|
||||||
|
^ better utilization of full terminal width
|
||||||
|
^ less prone to splitting entry names in half (should be impossible now)
|
||||||
|
^ no longer crashes on small terminals
|
||||||
|
- multi-word device ids can now verify via quick-unlock
|
||||||
|
- various language standardization changes
|
||||||
|
^ all back buttons in menus are now named "BACK"
|
||||||
|
^ all mentions of device ids are now referred to as "ids", rather than "names"
|
||||||
|
^ various non-user-facing variable renamings have also been made
|
||||||
|
- various menu helptext have been made more clear
|
||||||
|
^ this includes quick-unlock configuration, ssh configuration, device id configuration, and more
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
.TH sshyp 1 "24 October 2023" "v1.5.1" "sshyp man page"
|
.TH sshyp 1 "07 January 2024" "v1.5.2" "sshyp man page"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
\fBsshyp\fR - Simple, self-hosted, synchronized password management for UNIX(-like) systems. Alternative to (and compatible with) pass/password-store.
|
\fBsshyp\fR - Simple, self-hosted, synchronized password management for UNIX(-like) systems. Alternative to (and compatible with) pass/password-store.
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -38,7 +38,7 @@ Making a new entry saved as '~/.local/share/sshyp/school/university.gpg' using t
|
|||||||
Creating a note-only entry saved as '~/.local/share/sshyp/notes/test note.gpg':
|
Creating a note-only entry saved as '~/.local/share/sshyp/notes/test note.gpg':
|
||||||
sshyp /notes/test\ note add -n
|
sshyp /notes/test\ note add -n
|
||||||
|
|
||||||
Manually syncing entries with the server:
|
Manually synchronizing entries with the server:
|
||||||
sshyp sync
|
sshyp sync
|
||||||
.SH ARGUMENTS (CLIENT)
|
.SH ARGUMENTS (CLIENT)
|
||||||
help/-h bring up the help menu
|
help/-h bring up the help menu
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
from subprocess import PIPE, run
|
from subprocess import DEVNULL, PIPE, run
|
||||||
from sys import argv
|
from sys import argv
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ if argv[2] == 'wsl':
|
|||||||
run(('powershell.exe', '-c', 'Set-Clipboard'))
|
run(('powershell.exe', '-c', 'Set-Clipboard'))
|
||||||
|
|
||||||
elif argv[2] == 'wayland':
|
elif argv[2] == 'wayland':
|
||||||
hash_paste.update(run('wl-paste', stdout=PIPE).stdout.strip())
|
hash_paste.update(run('wl-paste', stdout=PIPE, stderr=DEVNULL).stdout.strip())
|
||||||
if argv[1] == hash_paste.hexdigest():
|
if argv[1] == hash_paste.hexdigest():
|
||||||
run(('wl-copy', '-c'))
|
run(('wl-copy', '-c'))
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ elif argv[2] == 'mac':
|
|||||||
elif argv[2] == 'termux':
|
elif argv[2] == 'termux':
|
||||||
hash_paste.update(run('termux-clipboard-get', stdout=PIPE).stdout.strip())
|
hash_paste.update(run('termux-clipboard-get', stdout=PIPE).stdout.strip())
|
||||||
if argv[1] == hash_paste.hexdigest():
|
if argv[1] == hash_paste.hexdigest():
|
||||||
run(("termux-clipboard-set", "''"))
|
run(("termux-clipboard-set", ''))
|
||||||
|
|
||||||
elif argv[2] == 'x11':
|
elif argv[2] == 'x11':
|
||||||
hash_paste.update(run(('xclip', '-o', '-sel', 'c'), stdout=PIPE).stdout.strip())
|
hash_paste.update(run(('xclip', '-o', '-sel', 'c'), stdout=PIPE).stdout.strip())
|
||||||
|
|||||||
+7
-7
@@ -8,12 +8,12 @@ home = expanduser('~')
|
|||||||
# PORT START SSHYNC-REMOTE
|
# PORT START SSHYNC-REMOTE
|
||||||
# REMOTE
|
# REMOTE
|
||||||
# prints all necessary remote data to stdout
|
# prints all necessary remote data to stdout
|
||||||
def remote_list_gen(_client_device_name, _remote_dir):
|
def remote_list_gen(_client_device_id, _remote_dir):
|
||||||
# deletions
|
# deletions
|
||||||
for _file in listdir(f"{home}/.config/sshyp/deleted"):
|
for _file in listdir(f"{home}/.config/sshyp/deleted"):
|
||||||
_file_path, _sep, _device = _file.partition('\x1f')
|
_file_path, _sep, _device = _file.partition('\x1f')
|
||||||
_file_path = _file_path.replace('\x1e', '/')
|
_file_path = _file_path.replace('\x1e', '/')
|
||||||
if _device == _client_device_name:
|
if _device == _client_device_id:
|
||||||
print(_file_path)
|
print(_file_path)
|
||||||
try:
|
try:
|
||||||
remove(f"{home}/.config/sshyp/deleted/{_file}")
|
remove(f"{home}/.config/sshyp/deleted/{_file}")
|
||||||
@@ -31,7 +31,7 @@ def remote_list_gen(_client_device_name, _remote_dir):
|
|||||||
|
|
||||||
|
|
||||||
# HYBRID
|
# HYBRID
|
||||||
# deletes a file or folder and/or marks it for deletion upon syncing
|
# deletes a file or folder and/or marks it for deletion upon synchronizing
|
||||||
def delete(_file_path, _target_database, _silent):
|
def delete(_file_path, _target_database, _silent):
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
_directory = f"{home}/.local/share/sshyp/"
|
_directory = f"{home}/.local/share/sshyp/"
|
||||||
@@ -44,8 +44,8 @@ def delete(_file_path, _target_database, _silent):
|
|||||||
if not _silent:
|
if not _silent:
|
||||||
print(f"location does not exist {_target_database}")
|
print(f"location does not exist {_target_database}")
|
||||||
if _target_database == 'remotely':
|
if _target_database == 'remotely':
|
||||||
for _device_name in listdir(f"{home}/.config/sshyp/devices"):
|
for _device_id in listdir(f"{home}/.config/sshyp/devices"):
|
||||||
open(f"{home}/.config/sshyp/deleted/" + _file_path.replace('/', '\x1e') + '\x1f' + _device_name, 'w')
|
open(f"{home}/.config/sshyp/deleted/" + _file_path.replace('/', '\x1e') + '\x1f' + _device_id, 'w')
|
||||||
|
|
||||||
|
|
||||||
# retrieves and returns titles and mod times from the local device
|
# retrieves and returns titles and mod times from the local device
|
||||||
@@ -78,8 +78,8 @@ def remote_list_fetch(_user_data):
|
|||||||
_deletion_database = _remote_data[0].strip().splitlines()
|
_deletion_database = _remote_data[0].strip().splitlines()
|
||||||
_folder_database = _remote_data[1].strip().splitlines()
|
_folder_database = _remote_data[1].strip().splitlines()
|
||||||
_titles_mods = _remote_data[2].strip().splitlines()
|
_titles_mods = _remote_data[2].strip().splitlines()
|
||||||
return _deletion_database, _folder_database, _titles_mods[:len(_titles_mods)//2], \
|
return _deletion_database, _folder_database, _titles_mods[:len(_titles_mods) // 2], \
|
||||||
_titles_mods[len(_titles_mods)//2:]
|
_titles_mods[len(_titles_mods) // 2:]
|
||||||
|
|
||||||
|
|
||||||
# checks for and acts upon files and folders marked for deletion
|
# checks for and acts upon files and folders marked for deletion
|
||||||
|
|||||||
+99
-97
@@ -18,29 +18,30 @@ home = expanduser('~')
|
|||||||
# generates and prints full entry list
|
# generates and prints full entry list
|
||||||
def entry_list_gen(_directory=f"{home}/.local/share/sshyp/"):
|
def entry_list_gen(_directory=f"{home}/.local/share/sshyp/"):
|
||||||
from shutil import get_terminal_size
|
from shutil import get_terminal_size
|
||||||
from textwrap import fill
|
_ran, _width = False, get_terminal_size().columns
|
||||||
_ran = False
|
print("\nfor a list of usable commands, run 'sshyp help'\n\n\u001b[38;5;0;48;5;15msshyp entries:\u001b[0m", end='')
|
||||||
print("\nfor a list of usable commands, run 'sshyp help'\n\n\u001b[38;5;0;48;5;15msshyp entries:\u001b[0m\n")
|
|
||||||
for _root, _dirs, _files in sorted(walk(_directory, topdown=True)):
|
for _root, _dirs, _files in sorted(walk(_directory, topdown=True)):
|
||||||
_entry_list, _color_alternator = [], 1
|
_color_alternator = 1
|
||||||
if _ran:
|
if _ran:
|
||||||
print(f"\u001b[38;5;15;48;5;238m{_root.replace(f'{home}/.local/share/sshyp', '', 1)}/\u001b[0m")
|
print(f"\n\n\u001b[38;5;7;48;5;8m{_root.replace(f'{home}/.local/share/sshyp', '', 1)}/\u001b[0m")
|
||||||
for filename in sorted(_files):
|
_char_counter = 0
|
||||||
|
for _filename in sorted(_files):
|
||||||
if _color_alternator > 0:
|
if _color_alternator > 0:
|
||||||
_entry_list.append(filename[:-4])
|
_print_string = _filename[:-4]
|
||||||
else:
|
else:
|
||||||
_entry_list.append(f"\u001b[38;5;8m{filename[:-4]}\u001b[0m")
|
_print_string = f"\u001b[38;5;8m{_filename[:-4]}\u001b[0m"
|
||||||
|
# -3 instead of -4 to account for trailing space character
|
||||||
|
_char_counter += len(_filename) - 3
|
||||||
|
if _char_counter >= _width:
|
||||||
|
# reset _char_counter to length of first entry in new line
|
||||||
|
_char_counter = len(_filename) - 3
|
||||||
|
print()
|
||||||
|
print(_print_string + ' ', end='')
|
||||||
_color_alternator = _color_alternator * -1
|
_color_alternator = _color_alternator * -1
|
||||||
_real = len(' '.join(_entry_list)) - (5.5 * len(_entry_list))
|
if _ran and _char_counter < 1:
|
||||||
if _real <= get_terminal_size()[0]:
|
print('\u001b[38;5;9m-empty directory-\u001b[0m', end='')
|
||||||
_width = len(' '.join(_entry_list))
|
|
||||||
else:
|
|
||||||
_width = (len(' '.join(_entry_list)) / (_real / get_terminal_size()[0]) - 25)
|
|
||||||
if len(_entry_list) > 0:
|
|
||||||
print(fill(' '.join(_entry_list), width=_width) + '\n')
|
|
||||||
elif _ran:
|
|
||||||
print('\u001b[38;5;9m-empty directory-\u001b[0m\n')
|
|
||||||
_ran = True
|
_ran = True
|
||||||
|
print('\n')
|
||||||
|
|
||||||
|
|
||||||
# displays the contents of an entry in a readable format
|
# displays the contents of an entry in a readable format
|
||||||
@@ -54,16 +55,16 @@ def entry_reader(_decrypted_entry):
|
|||||||
for _num in range(len(_decrypted_entry)):
|
for _num in range(len(_decrypted_entry)):
|
||||||
try:
|
try:
|
||||||
if _num == 0 and _decrypted_entry[1] != '':
|
if _num == 0 and _decrypted_entry[1] != '':
|
||||||
print(f"\u001b[38;5;15;48;5;238musername:\u001b[0m\n{_decrypted_entry[1]}\n")
|
print(f"\u001b[38;5;7;48;5;8musername:\u001b[0m\n{_decrypted_entry[1]}\n")
|
||||||
elif _num == 1 and _decrypted_entry[0] != '':
|
elif _num == 1 and _decrypted_entry[0] != '':
|
||||||
print(f"\u001b[38;5;15;48;5;238mpassword:\u001b[0m\n{_entry_password}\n")
|
print(f"\u001b[38;5;7;48;5;8mpassword:\u001b[0m\n{_entry_password}\n")
|
||||||
elif _num == 2 and _decrypted_entry[2] != '':
|
elif _num == 2 and _decrypted_entry[2] != '':
|
||||||
print(f"\u001b[38;5;15;48;5;238murl:\u001b[0m\n{_decrypted_entry[_num]}\n")
|
print(f"\u001b[38;5;7;48;5;8murl:\u001b[0m\n{_decrypted_entry[_num]}\n")
|
||||||
elif _num >= 3 and _decrypted_entry[_num] != '' and _notes_flag != 1:
|
elif _notes_flag == 1 and _num >= 3:
|
||||||
_notes_flag = 1
|
|
||||||
print('\u001b[38;5;15;48;5;238mnotes:\u001b[0m\n' + _decrypted_entry[_num])
|
|
||||||
elif _num >= 3 and _notes_flag == 1:
|
|
||||||
print(_decrypted_entry[_num])
|
print(_decrypted_entry[_num])
|
||||||
|
elif _notes_flag != 1 and _num >= 3 and _decrypted_entry[_num] != '':
|
||||||
|
_notes_flag = 1
|
||||||
|
print('\u001b[38;5;7;48;5;8mnotes:\u001b[0m\n' + _decrypted_entry[_num])
|
||||||
if _notes_flag == 1:
|
if _notes_flag == 1:
|
||||||
try:
|
try:
|
||||||
_line_test = _decrypted_entry[_num + 1]
|
_line_test = _decrypted_entry[_num + 1]
|
||||||
@@ -71,7 +72,7 @@ def entry_reader(_decrypted_entry):
|
|||||||
print()
|
print()
|
||||||
except IndexError:
|
except IndexError:
|
||||||
if _num == 0 and _decrypted_entry[0] != '':
|
if _num == 0 and _decrypted_entry[0] != '':
|
||||||
print(f"\u001b[38;5;15;48;5;238mpassword:\u001b[0m\n{_entry_password}\n")
|
print(f"\u001b[38;5;7;48;5;8mpassword:\u001b[0m\n{_entry_password}\n")
|
||||||
|
|
||||||
|
|
||||||
# generates and returns a random string based on input
|
# generates and returns a random string based on input
|
||||||
@@ -81,7 +82,7 @@ def string_gen(_complexity, _length):
|
|||||||
if _complexity == 's':
|
if _complexity == 's':
|
||||||
_character_pool = string.ascii_letters + string.digits
|
_character_pool = string.ascii_letters + string.digits
|
||||||
elif _complexity == 'f':
|
elif _complexity == 'f':
|
||||||
_character_pool = string.digits + string.ascii_letters + string.punctuation.replace('/', '').replace('\\', '')\
|
_character_pool = string.digits + string.ascii_letters + string.punctuation.replace('/', '').replace('\\', '') \
|
||||||
.replace("'", '').replace('"', '').replace('`', '').replace('~', '')
|
.replace("'", '').replace('"', '').replace('`', '').replace('~', '')
|
||||||
else:
|
else:
|
||||||
_character_pool = string.digits + string.ascii_letters + string.punctuation
|
_character_pool = string.digits + string.ascii_letters + string.punctuation
|
||||||
@@ -192,14 +193,15 @@ def whitelist_verify(_port, _username_ssh, _ip, _client_device_id, _identity):
|
|||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
_i, _full_password = 0, ''
|
_i, _full_password = 0, ''
|
||||||
_server_whitelist = run(('ssh', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
|
_server_whitelist = run(('ssh', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
|
||||||
f'python3 -c \'from os import listdir; print(*listdir("/home/{_username_ssh}'
|
'python3 -c \'from os import listdir; [print(_id, end="\x1f") for _id in '
|
||||||
f'/.config/sshyp/whitelist"))\''), stdout=PIPE, text=True).stdout.rstrip().split()
|
f'listdir("/home/{_username_ssh}/.config/sshyp/whitelist")]\''),
|
||||||
|
stdout=PIPE, text=True).stdout.rstrip().split('\x1f')
|
||||||
for _device_id in _server_whitelist:
|
for _device_id in _server_whitelist:
|
||||||
if _device_id == _client_device_id:
|
if _device_id == _client_device_id:
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
_quick_unlock_password = getpass(prompt='\nquick-unlock pin: ')
|
_quick_unlock_password = getpass(prompt='\nquick-unlock pin: ')
|
||||||
_quick_unlock_password_excluded = \
|
_quick_unlock_password_excluded = \
|
||||||
run(('ssh', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
|
run(('ssh', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
|
||||||
f"gpg --pinentry-mode loopback --passphrase '{_quick_unlock_password}' "
|
f"gpg --pinentry-mode loopback --passphrase '{_quick_unlock_password}' "
|
||||||
f"-qd ~/.config/sshyp/excluded.gpg"), stdout=PIPE, text=True).stdout.rstrip()
|
f"-qd ~/.config/sshyp/excluded.gpg"), stdout=PIPE, text=True).stdout.rstrip()
|
||||||
while _i < len(_quick_unlock_password_excluded):
|
while _i < len(_quick_unlock_password_excluded):
|
||||||
@@ -259,7 +261,7 @@ def line_edit(_lines, _edit_data, _edit_line):
|
|||||||
return _lines
|
return _lines
|
||||||
|
|
||||||
|
|
||||||
# attempts to connect to the user's server via ssh to register the device for syncing
|
# attempts to connect to the user's server via ssh to register the device for synchronization
|
||||||
def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshyp_data):
|
def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshyp_data):
|
||||||
from stweak import write_config
|
from stweak import write_config
|
||||||
if not _sshyp_data.has_section('CLIENT-ONLINE'):
|
if not _sshyp_data.has_section('CLIENT-ONLINE'):
|
||||||
@@ -270,8 +272,8 @@ def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshy
|
|||||||
f'{_client_device_id}").touch(mode=0o400, exist_ok=True)\''), stderr=DEVNULL, check=True)
|
f'{_client_device_id}").touch(mode=0o400, exist_ok=True)\''), stderr=DEVNULL, check=True)
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
print(f'\n\u001b[38;5;9mwarning: ssh connection could not be made - ensure the public key ({_identity}) is '
|
print(f'\n\u001b[38;5;9mwarning: ssh connection could not be made - ensure the public key ({_identity}) is '
|
||||||
'registered on the remote server and that the entered ip, port, and username are correct\n\nsyncing '
|
'registered on the remote server and that the entered ip, port, and username are correct\n\n'
|
||||||
'functionality will be disabled until this is addressed\u001b[0m\n')
|
'synchronization functionality will be disabled until this is addressed\u001b[0m\n')
|
||||||
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'true')
|
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'true')
|
||||||
write_config(_sshyp_data)
|
write_config(_sshyp_data)
|
||||||
return True
|
return True
|
||||||
@@ -285,101 +287,101 @@ def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshy
|
|||||||
# prints help text based on argument
|
# prints help text based on argument
|
||||||
def print_info():
|
def print_info():
|
||||||
if arguments[0] in ('version', '-v'):
|
if arguments[0] in ('version', '-v'):
|
||||||
_blank = '\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m' + 55*' ' + '\u001b[38;5;7;48;5;8m/\u001b[0m'
|
_blank = '\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m' + 55 * ' ' + '\u001b[38;5;7;48;5;8m/\u001b[0m'
|
||||||
_border = '\u001b[38;5;7;48;5;8m' + 14*'<>' + '-' + 14*'<>' + '\u001b[0m\n'
|
_border = '\u001b[38;5;7;48;5;8m' + 14 * '<>' + '-' + 14 * '<>' + '\u001b[0m\n'
|
||||||
print(f"""\nsshyp is a simple, self-hosted, sftp-synchronized\npassword manager for unix(-like) systems\n
|
print(f"""\nsshyp is a simple, self-hosted, sftp-synchronized\npassword manager for unix(-like) systems\n
|
||||||
{9*' '}..{15*' '}\u001b[38;5;12m♥♥ \u001b[38;5;9m♥♥\u001b[0m{15*' '}..
|
{9 * ' '}..{15 * ' '}\u001b[38;5;12m♥♥ \u001b[38;5;9m♥♥\u001b[0m{15 * ' '}..
|
||||||
{8*' '}/()\\''.''.{7*' '}\u001b[38;5;12m♥♥♥\u001b[0m♥♥♥♥\u001b[0m{7*' '}.''.''/()\\{3*' '}_)
|
{8 * ' '}/()\\''.''.{7 * ' '}\u001b[38;5;12m♥♥♥\u001b[0m♥♥♥♥\u001b[0m{7 * ' '}.''.''/()\\{3 * ' '}_)
|
||||||
{5*' '}_.{3*' '}:{7*' '}*{7*' '}\u001b[38;5;9m♥♥♥♥♥\u001b[0m{7*' '}*{7*' '}:{3*' '}<[◎]|_|=
|
{5 * ' '}_.{3 * ' '}:{7 * ' '}*{7 * ' '}\u001b[38;5;9m♥♥♥♥♥\u001b[0m{7 * ' '}*{7 * ' '}:{3 * ' '}<[◎]|_|=
|
||||||
}}-}}-*]{4*' '}`..'..'{9*' '}\u001b[0m♥♥♥\u001b[0m{9*' '}`..'..'{6*' '}|
|
}}-}}-*]{4 * ' '}`..'..'{9 * ' '}\u001b[0m♥♥♥\u001b[0m{9 * ' '}`..'..'{6 * ' '}|
|
||||||
{4*' '}◎-◎{4*' '}//{3*' '}\\\\{10*' '}\u001b[38;5;9m♥\u001b[0m{10*' '}//{3*' '}\\\\{5*' '}/|\\""")
|
{4 * ' '}◎-◎{4 * ' '}//{3 * ' '}\\\\{10 * ' '}\u001b[38;5;9m♥\u001b[0m{10 * ' '}//{3 * ' '}\\\\{5 * ' '}/|\\""")
|
||||||
print(f"{_border}{_blank}\n\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{18*' '}\u001b[38;5;15;48;5;8msshyp "
|
print(f"{_border}{_blank}\n\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{18 * ' '}\u001b[38;5;15;48;5;8msshyp "
|
||||||
f"version 1.5.1\u001b[38;5;15;48;5;15m{18*' '}\u001b[38;5;7;48;5;8m/\u001b[0m")
|
f"version 1.5.2\u001b[38;5;15;48;5;15m{18 * ' '}\u001b[38;5;7;48;5;8m/\u001b[0m")
|
||||||
print(f"\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{14*' '}\u001b[38;5;15;48;5;8mthe fortified flock"
|
print(f"\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{14 * ' '}\u001b[38;5;15;48;5;8mthe fortified flock"
|
||||||
f" update\u001b[38;5;15;48;5;15m{15*' '}\u001b[38;5;7;48;5;8m/\u001b[0m\n{_blank}")
|
f" update\u001b[38;5;15;48;5;15m{15 * ' '}\u001b[38;5;7;48;5;8m/\u001b[0m\n{_blank}")
|
||||||
print(f"\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{9*' '}\u001b[38;5;15;48;5;8mcopyright 2021-2023 ",
|
print(f"\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{9 * ' '}\u001b[38;5;15;48;5;8mcopyright 2021-2024 ",
|
||||||
f"randall winkhart\u001b[38;5;15;48;5;15m{9*' '}\u001b[38;5;7;48;5;8m/\u001b[0m")
|
f"randall winkhart\u001b[38;5;15;48;5;15m{9 * ' '}\u001b[38;5;7;48;5;8m/\u001b[0m")
|
||||||
print(f"{_blank}\n{_border}\nsee https://github.com/rwinkhart/sshyp for more information\n")
|
print(f"{_blank}\n{_border}\nsee https://github.com/rwinkhart/sshyp for more information\n")
|
||||||
elif arguments[0] == 'license':
|
elif arguments[0] == 'license':
|
||||||
print('\nThis program is free software: you can redistribute it and/or modify it under the terms\nof version 3 '
|
print('\nThis program is free software: you can redistribute it and/or modify it under the terms of\nversion 3 '
|
||||||
'(only) of the GNU General Public License as published by the Free Software Foundation.\n\nThis program '
|
'(only) of the GNU General Public License as published by the Free Software Foundation.\n\nThis program '
|
||||||
'is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\nwithout even the implied '
|
'is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\nwithout even the implied '
|
||||||
'warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\nSee the GNU General Public License for'
|
'warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\nSee the GNU General Public License '
|
||||||
' more details.\n\nhttps://opensource.org/licenses/GPL-3.0\n')
|
'for more details:\nhttps://opensource.org/licenses/GPL-3.0\n')
|
||||||
elif arguments[0] == 'add' and device_type == 'client':
|
elif arguments[0] == 'add' and device_type == 'client':
|
||||||
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> add <option>\u001b[0m\n
|
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> add <option>\u001b[0m\n
|
||||||
\u001b[1moptions:\u001b[0m
|
\u001b[1moptions:\u001b[0m
|
||||||
add:
|
add:
|
||||||
password/-p{12*' '}add a password entry
|
password/-p{12 * ' '}add a password entry
|
||||||
note/-n{16*' '}add a note entry
|
note/-n{16 * ' '}add a note entry
|
||||||
folder/-f{14*' '}add a new folder for entries\n""")
|
folder/-f{14 * ' '}add a new folder for entries\n""")
|
||||||
elif arguments[0] == 'edit' and device_type == 'client':
|
elif arguments[0] == 'edit' and device_type == 'client':
|
||||||
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> edit <option>\u001b[0m\n
|
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> edit <option>\u001b[0m\n
|
||||||
\u001b[1moptions:\u001b[0m
|
\u001b[1moptions:\u001b[0m
|
||||||
edit:
|
edit:
|
||||||
rename/relocate/-r{5*' '}rename or relocate an entry
|
rename/relocate/-r{5 * ' '}rename or relocate an entry
|
||||||
username/-u{12*' '}change the username of an entry
|
username/-u{12 * ' '}change the username of an entry
|
||||||
password/-p{12*' '}change the password of an entry
|
password/-p{12 * ' '}change the password of an entry
|
||||||
url/-l{17*' '}change the url attached to an entry
|
url/-l{17 * ' '}change the url attached to an entry
|
||||||
note/-n{16*' '}change the note attached to an entry\n""")
|
note/-n{16 * ' '}change the note attached to an entry\n""")
|
||||||
elif arguments[0] == 'copy' and device_type == 'client':
|
elif arguments[0] == 'copy' and device_type == 'client':
|
||||||
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> copy <option>\u001b[0m\n
|
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> copy <option>\u001b[0m\n
|
||||||
\u001b[1moptions:\u001b[0m
|
\u001b[1moptions:\u001b[0m
|
||||||
copy:
|
copy:
|
||||||
username/-u{12*' '}copy the username of an entry to your clipboard
|
username/-u{12 * ' '}copy the username of an entry to your clipboard
|
||||||
password/-p{12*' '}copy the password of an entry to your clipboard
|
password/-p{12 * ' '}copy the password of an entry to your clipboard
|
||||||
url/-l{17*' '}copy the url of an entry to your clipboard
|
url/-l{17 * ' '}copy the url of an entry to your clipboard
|
||||||
note/-n{16*' '}copy the note of an entry to your clipboard\n""")
|
note/-n{16 * ' '}copy the note of an entry to your clipboard\n""")
|
||||||
elif arguments[0] == 'gen' and device_type == 'client':
|
elif arguments[0] == 'gen' and device_type == 'client':
|
||||||
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> gen [option]\u001b[0m\n
|
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> gen [option]\u001b[0m\n
|
||||||
\u001b[1moptions:\u001b[0m
|
\u001b[1moptions:\u001b[0m
|
||||||
gen:
|
gen:
|
||||||
update/-u{14*' '}generate a password for an existing entry\n""")
|
update/-u{14 * ' '}generate a password for an existing entry\n""")
|
||||||
else:
|
else:
|
||||||
print("\n\u001b[1msshyp ", "copyright (c) 2021-2023 ", """randall winkhart\u001b[0m
|
print("\n\u001b[1msshyp ", "copyright (c) 2021-2024 ", """randall winkhart\u001b[0m
|
||||||
this is free software, and you are welcome to redistribute it under certain conditions;
|
this is free software, and you are welcome to redistribute it under certain conditions;
|
||||||
this program comes with absolutely no warranty; type 'sshyp license' for details""")
|
this program comes with absolutely no warranty; type 'sshyp license' for details""")
|
||||||
if device_type == 'client':
|
if device_type == 'client':
|
||||||
print(f"""\n\u001b[1musage:\u001b[0m sshyp [/<entry name> [argument] [option]] | [argument]\n
|
print(f"""\n\u001b[1musage:\u001b[0m sshyp [/<entry name> [argument] [option]] | [argument]\n
|
||||||
\u001b[1marguments:\u001b[0m
|
\u001b[1marguments:\u001b[0m
|
||||||
help/-h{17*' '}bring up this menu
|
help/-h{17 * ' '}bring up this menu
|
||||||
version/-v{14*' '}display sshyp version info
|
version/-v{14 * ' '}display sshyp version info
|
||||||
init{20*' '}set up sshyp
|
init{20 * ' '}set up sshyp
|
||||||
tweak{19*' '}change configuration options/manage extensions and updates
|
tweak{19 * ' '}change configuration options/manage extensions and updates
|
||||||
add{21*' '}add an entry
|
add{21 * ' '}add an entry
|
||||||
gen{21*' '}generate a new password
|
gen{21 * ' '}generate a new password
|
||||||
edit{20*' '}edit an existing entry
|
edit{20 * ' '}edit an existing entry
|
||||||
copy{20*' '}copy details of an entry to your clipboard
|
copy{20 * ' '}copy details of an entry to your clipboard
|
||||||
shear{19*' '}delete an existing entry
|
shear{19 * ' '}delete an existing entry
|
||||||
sync{20*' '}manually sync the entry directory via sshync
|
sync{20 * ' '}manually sync the entry directory via sshync
|
||||||
\n\u001b[1moptions:\u001b[0m
|
\n\u001b[1moptions:\u001b[0m
|
||||||
add:
|
add:
|
||||||
password/-p{12*' '}add a password entry
|
password/-p{12 * ' '}add a password entry
|
||||||
note/-n{16*' '}add a note entry
|
note/-n{16 * ' '}add a note entry
|
||||||
folder/-f{14*' '}add a new folder for entries
|
folder/-f{14 * ' '}add a new folder for entries
|
||||||
edit:
|
edit:
|
||||||
rename/relocate/-r{5*' '}rename or relocate an entry
|
rename/relocate/-r{5 * ' '}rename or relocate an entry
|
||||||
username/-u{12*' '}change the username of an entry
|
username/-u{12 * ' '}change the username of an entry
|
||||||
password/-p{12*' '}change the password of an entry
|
password/-p{12 * ' '}change the password of an entry
|
||||||
url/-l{17*' '}change the url attached to an entry
|
url/-l{17 * ' '}change the url attached to an entry
|
||||||
note/-n{16*' '}change the note attached to an entry
|
note/-n{16 * ' '}change the note attached to an entry
|
||||||
copy:
|
copy:
|
||||||
username/-u{12*' '}copy the username of an entry to your clipboard
|
username/-u{12 * ' '}copy the username of an entry to your clipboard
|
||||||
password/-p{12*' '}copy the password of an entry to your clipboard
|
password/-p{12 * ' '}copy the password of an entry to your clipboard
|
||||||
url/-l{17*' '}copy the url of an entry to your clipboard
|
url/-l{17 * ' '}copy the url of an entry to your clipboard
|
||||||
note/-n{16*' '}copy the note of an entry to your clipboard
|
note/-n{16 * ' '}copy the note of an entry to your clipboard
|
||||||
gen:
|
gen:
|
||||||
update/-u{14*' '}generate a password for an existing entry
|
update/-u{14 * ' '}generate a password for an existing entry
|
||||||
\n\u001b[1mtip 1:\u001b[0m you can quickly read an entry with 'sshyp /<entry name>'
|
\n\u001b[1mtip 1:\u001b[0m you can quickly read an entry with 'sshyp /<entry name>'
|
||||||
\u001b[1mtip 2:\u001b[0m type 'sshyp' to view a list of saved entries\n""")
|
\u001b[1mtip 2:\u001b[0m type 'sshyp' to view a list of saved entries\n""")
|
||||||
# PORT START HELP-SERVER
|
# PORT START HELP-SERVER
|
||||||
else:
|
else:
|
||||||
print(f"""\n\u001b[1musage:\u001b[0m sshyp <argument>\n
|
print(f"""\n\u001b[1musage:\u001b[0m sshyp <argument>\n
|
||||||
\u001b[1marguments:\u001b[0m
|
\u001b[1marguments:\u001b[0m
|
||||||
help/-h{17*' '}bring up this menu
|
help/-h{17 * ' '}bring up this menu
|
||||||
version/-v{14*' '}display sshyp version info
|
version/-v{14 * ' '}display sshyp version info
|
||||||
init{20*' '}set up sshyp
|
init{20 * ' '}set up sshyp
|
||||||
tweak{19*' '}change configuration options/manage extensions and updates\n""")
|
tweak{19 * ' '}change configuration options/manage extensions and updates\n""")
|
||||||
# PORT END HELP-SERVER
|
# PORT END HELP-SERVER
|
||||||
|
|
||||||
|
|
||||||
@@ -391,7 +393,7 @@ def read_shortcut():
|
|||||||
|
|
||||||
# calls sshync to sync changes to the user's server
|
# calls sshync to sync changes to the user's server
|
||||||
def sync(_start_text=''):
|
def sync(_start_text=''):
|
||||||
print(f"{_start_text}syncing entries with the server device...\n")
|
print(f"{_start_text}synchronizing entries with the server device...\n")
|
||||||
# set permissions before uploading
|
# set permissions before uploading
|
||||||
for _root, _dirs, _files in walk(f"{home}/.local/share/sshyp"):
|
for _root, _dirs, _files in walk(f"{home}/.local/share/sshyp"):
|
||||||
for _path in _root.splitlines():
|
for _path in _root.splitlines():
|
||||||
@@ -420,7 +422,7 @@ def add_entry():
|
|||||||
else:
|
else:
|
||||||
_note = ''
|
_note = ''
|
||||||
print('\n\u001b[1mentry preview:\u001b[0m')
|
print('\n\u001b[1mentry preview:\u001b[0m')
|
||||||
entry_reader([_password, _username, _url, _note])
|
entry_reader([_password, _username, _url] + _note.split('\n'))
|
||||||
encrypt([_password, _username, _url, _note], directory + entry_name, gpg_id)
|
encrypt([_password, _username, _url, _note], directory + entry_name, gpg_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -513,7 +515,7 @@ def gen():
|
|||||||
_note = edit_note([])
|
_note = edit_note([])
|
||||||
else:
|
else:
|
||||||
_note = ''
|
_note = ''
|
||||||
_new_lines = [_password, _username, _url, _note]
|
_new_lines = [_password, _username, _url] + _note.split('\n')
|
||||||
print('\n\u001b[1mentry preview:\u001b[0m')
|
print('\n\u001b[1mentry preview:\u001b[0m')
|
||||||
entry_reader(_new_lines)
|
entry_reader(_new_lines)
|
||||||
encrypt(_new_lines, directory + entry_name, gpg_id)
|
encrypt(_new_lines, directory + entry_name, gpg_id)
|
||||||
@@ -625,8 +627,8 @@ if __name__ == "__main__":
|
|||||||
arg_start = 1
|
arg_start = 1
|
||||||
entry_name = arguments[0].strip('/')
|
entry_name = arguments[0].strip('/')
|
||||||
# determine whether to show passwords in entry previews
|
# determine whether to show passwords in entry previews
|
||||||
if arg_count > 1 and arguments[arg_count-1] in ('--show', '-s'):
|
if arg_count > 1 and arguments[arg_count - 1] in ('--show', '-s'):
|
||||||
arguments.pop()
|
del arguments[-1]
|
||||||
arg_count -= 1
|
arg_count -= 1
|
||||||
pass_show = True
|
pass_show = True
|
||||||
else:
|
else:
|
||||||
@@ -657,13 +659,13 @@ if __name__ == "__main__":
|
|||||||
if ssh_error:
|
if ssh_error:
|
||||||
ssh_error = copy_id_check(port, username_ssh, ip, client_device_id, identity, sshyp_data)
|
ssh_error = copy_id_check(port, username_ssh, ip, client_device_id, identity, sshyp_data)
|
||||||
except (FileNotFoundError, NoSectionError, NoOptionError):
|
except (FileNotFoundError, NoSectionError, NoOptionError):
|
||||||
print(f"\n{73*'!'}")
|
print(f"\n{73 * '!'}")
|
||||||
print("not all necessary configurations have been made - please run 'sshyp init'")
|
print("not all necessary configurations have been made - please run 'sshyp init'")
|
||||||
print(f"{73*'!'}\n")
|
print(f"{73 * '!'}\n")
|
||||||
s_exit(1)
|
s_exit(1)
|
||||||
else:
|
else:
|
||||||
from stweak import wrapped_entry
|
from stweak import wrapped_entry
|
||||||
wrapped_entry(False)
|
wrapped_entry(False, 'additional configuration options:')
|
||||||
s_exit()
|
s_exit()
|
||||||
|
|
||||||
# run function based on arguments
|
# run function based on arguments
|
||||||
|
|||||||
+228
-115
@@ -5,12 +5,12 @@ from os import environ, listdir, remove
|
|||||||
from os.path import exists, expanduser, isfile
|
from os.path import exists, expanduser, isfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from random import randint
|
from random import randint
|
||||||
from shutil import get_terminal_size, which
|
from shutil import which
|
||||||
from subprocess import PIPE, run
|
from subprocess import CalledProcessError, DEVNULL, PIPE, run
|
||||||
# PORT START UNAME-IMPORT-STWEAK
|
# PORT START UNAME-IMPORT-STWEAK
|
||||||
from os import uname
|
from os import uname
|
||||||
# PORT END UNAME-IMPORT-STWEAK
|
# PORT END UNAME-IMPORT-STWEAK
|
||||||
home, sshyp_data, stdscr = expanduser('~'), ConfigParser(interpolation=None), None
|
home, sshyp_data, stdscr, gm_device_type = expanduser('~'), ConfigParser(interpolation=None), None, None
|
||||||
if isfile(f"{home}/.config/sshyp/sshyp.ini"):
|
if isfile(f"{home}/.config/sshyp/sshyp.ini"):
|
||||||
_exists_flag = True
|
_exists_flag = True
|
||||||
sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini")
|
sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini")
|
||||||
@@ -28,25 +28,62 @@ def write_config(_sshyp_data=sshyp_data):
|
|||||||
def curses_radio(_options, _pretext):
|
def curses_radio(_options, _pretext):
|
||||||
curs_set(0)
|
curs_set(0)
|
||||||
_selected = 0
|
_selected = 0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# clear curses window
|
||||||
stdscr.clear()
|
stdscr.clear()
|
||||||
stdscr.addstr(0, 0, _pretext)
|
# get terminal size
|
||||||
for _i, _option in enumerate(_options):
|
_height, _width = stdscr.getmaxyx()
|
||||||
_y = _i + _pretext.count('\n') + 2
|
|
||||||
if _i == _selected:
|
# track whether to generate interactive buttons (depending on terminal size)
|
||||||
stdscr.addstr(_y, 0, "[*] " + _option, A_REVERSE)
|
_button_gen = False
|
||||||
|
|
||||||
|
# split text based on new lines
|
||||||
|
_pretext_lines = _pretext.split('\n')
|
||||||
|
|
||||||
|
# iterate through lines, splitting further (wrapping) as needed, to add to curses window
|
||||||
|
_current_line = 0
|
||||||
|
for _line in _pretext_lines:
|
||||||
|
if _current_line <= _height - 4:
|
||||||
|
_button_gen = True
|
||||||
|
while len(_line) > _width:
|
||||||
|
stdscr.addstr(_current_line, 0, _line[:_width])
|
||||||
|
_line = _line[_width:]
|
||||||
|
_current_line += 1
|
||||||
|
stdscr.addstr(_current_line, 0, _line)
|
||||||
|
_current_line += 1
|
||||||
else:
|
else:
|
||||||
stdscr.addstr(_y, 0, "[ ] " + _option)
|
_button_gen = False
|
||||||
stdscr.refresh()
|
stdscr.addstr(_current_line, 0, '# warning: re-size terminal to see more information'[:_width - 1])
|
||||||
_key = stdscr.getch()
|
_current_line += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
# create user-interactive options if all information has been displayed to the user
|
||||||
|
if _button_gen:
|
||||||
|
for _i, _option in enumerate(_options):
|
||||||
|
_y = _i + _current_line + 1
|
||||||
|
if _y < _height:
|
||||||
|
if _i == _selected:
|
||||||
|
stdscr.addstr(_y, 0, "[*] " + _option, A_REVERSE)
|
||||||
|
else:
|
||||||
|
stdscr.addstr(_y, 0, "[ ] " + _option)
|
||||||
|
else:
|
||||||
|
stdscr.addstr(_y - 1, 0, '# warning: re-size terminal to see more information')
|
||||||
|
break
|
||||||
|
|
||||||
# update _selected based on user input
|
# update _selected based on user input
|
||||||
if _key == KEY_UP:
|
_key = stdscr.getch()
|
||||||
_selected = (_selected-1) % len(_options)
|
if _button_gen:
|
||||||
elif _key == KEY_DOWN:
|
if _key == KEY_UP:
|
||||||
_selected = (_selected+1) % len(_options)
|
_selected = (_selected - 1) % len(_options)
|
||||||
elif _key == ord('\n'):
|
elif _key == KEY_DOWN:
|
||||||
break
|
_selected = (_selected + 1) % len(_options)
|
||||||
|
# ord('\n') == 10
|
||||||
|
elif _key == 10:
|
||||||
|
break
|
||||||
|
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
|
|
||||||
curs_set(1)
|
curs_set(1)
|
||||||
return _selected
|
return _selected
|
||||||
|
|
||||||
@@ -55,9 +92,9 @@ def curses_radio(_options, _pretext):
|
|||||||
def curses_text(_pretext):
|
def curses_text(_pretext):
|
||||||
stdscr.clear()
|
stdscr.clear()
|
||||||
stdscr.addstr(0, 0, _pretext)
|
stdscr.addstr(0, 0, _pretext)
|
||||||
_term_columns = get_terminal_size()[0]
|
_width = stdscr.getmaxyx()[1]
|
||||||
_editwin = newwin(1, _term_columns-2, 3, 1)
|
_editwin = newwin(1, _width - 2, 3, 1)
|
||||||
rectangle(stdscr, 2, 0, 4, _term_columns-1)
|
rectangle(stdscr, 2, 0, 4, _width - 1)
|
||||||
stdscr.refresh()
|
stdscr.refresh()
|
||||||
_box = Textbox(_editwin)
|
_box = Textbox(_editwin)
|
||||||
# let the user edit until ctrl+g/enter is struck
|
# let the user edit until ctrl+g/enter is struck
|
||||||
@@ -102,13 +139,22 @@ def gpg_config():
|
|||||||
_named_uid_list.append(_uid.split(':')[9].replace('\\x3a', ':').replace('\\x5c', '\\'))
|
_named_uid_list.append(_uid.split(':')[9].replace('\\x3a', ':').replace('\\x5c', '\\'))
|
||||||
_named_uid_list.append('auto-generate')
|
_named_uid_list.append('auto-generate')
|
||||||
_gpg_id_sel = curses_radio(_named_uid_list, 'gpg key selection')
|
_gpg_id_sel = curses_radio(_named_uid_list, 'gpg key selection')
|
||||||
if _gpg_id_sel == len(_named_uid_list)-1:
|
if _gpg_id_sel == len(_named_uid_list) - 1:
|
||||||
if not isfile(f"{home}/.config/sshyp/gpg-gen"):
|
if not isfile(f"{home}/.config/sshyp/gpg-gen"):
|
||||||
open(f"{home}/.config/sshyp/gpg-gen", 'w').writelines([
|
open(f"{home}/.config/sshyp/gpg-gen", 'w').writelines([
|
||||||
'Key-Type: 1\n', 'Key-Length: 4096\n', 'Key-Usage: sign encrypt\n', 'Name-Real: sshyp\n',
|
'Key-Type: 1\n', 'Key-Length: 4096\n', 'Key-Usage: sign encrypt\n', 'Name-Real: sshyp\n',
|
||||||
'Name-Comment: gpg-sshyp\n', 'Name-Email: github.com/rwinkhart/sshyp\n',
|
'Name-Comment: gpg-sshyp\n', 'Name-Email: github.com/rwinkhart/sshyp\n',
|
||||||
'Expire-Date: 0'])
|
'Expire-Date: 0'])
|
||||||
run(('gpg', '-q', '--batch', '--generate-key', f"{home}/.config/sshyp/gpg-gen"))
|
curses_radio(['okay'], 'gpg key generation may take some time (especially on slower devices)\n\nselect "okay" '
|
||||||
|
'to start\n\ndo not terminate this process!')
|
||||||
|
try:
|
||||||
|
run(('gpg', '-q', '--batch', '--generate-key', f"{home}/.config/sshyp/gpg-gen"), stderr=PIPE, check=True)
|
||||||
|
except CalledProcessError as e:
|
||||||
|
if 'No pinentry' in e.stderr.decode("utf-8"):
|
||||||
|
curses_radio(['okay'], 'either a valid pinentry program is missing or gpg is not configured to use an '
|
||||||
|
'available pinentry program\n\nsshyp will now exit')
|
||||||
|
from sys import exit as s_exit
|
||||||
|
s_exit(6)
|
||||||
remove(f"{home}/.config/sshyp/gpg-gen")
|
remove(f"{home}/.config/sshyp/gpg-gen")
|
||||||
_gpg_id = run(('gpg', '-k', '--with-colons'), stdout=PIPE, text=True).stdout.splitlines()[-1].split(':')[9]
|
_gpg_id = run(('gpg', '-k', '--with-colons'), stdout=PIPE, text=True).stdout.splitlines()[-1].split(':')[9]
|
||||||
else:
|
else:
|
||||||
@@ -145,7 +191,7 @@ def editor_config(_env_mode):
|
|||||||
|
|
||||||
|
|
||||||
# ssh+sshync configuration
|
# ssh+sshync configuration
|
||||||
def ssh_config():
|
def ssh_config(_reconfig=False):
|
||||||
# private key selection/generation
|
# private key selection/generation
|
||||||
_keys = []
|
_keys = []
|
||||||
# ensure ~/.ssh directory exists
|
# ensure ~/.ssh directory exists
|
||||||
@@ -155,14 +201,20 @@ def ssh_config():
|
|||||||
and not _file.endswith('.pub') and isfile(f"{home}/.ssh/{_file}"):
|
and not _file.endswith('.pub') and isfile(f"{home}/.ssh/{_file}"):
|
||||||
_keys.append(f"{home}/.ssh/{_file}")
|
_keys.append(f"{home}/.ssh/{_file}")
|
||||||
_keys.extend(['auto-generate', 'other (type the location)'])
|
_keys.extend(['auto-generate', 'other (type the location)'])
|
||||||
|
# append a back button if launched optionally
|
||||||
|
if _reconfig:
|
||||||
|
_keys.append('BACK')
|
||||||
_key_selected_num = curses_radio(_keys, 'which private ssh key would you like to use for sshyp?')
|
_key_selected_num = curses_radio(_keys, 'which private ssh key would you like to use for sshyp?')
|
||||||
_gen_index = len(_keys)-2
|
if _reconfig and _key_selected_num == len(_keys) - 1:
|
||||||
|
return
|
||||||
|
_gen_index = len(_keys) - 2
|
||||||
if _key_selected_num >= _gen_index:
|
if _key_selected_num >= _gen_index:
|
||||||
_ssh_key = expanduser(curses_text('enter the location for your private ssh key:\n\n\n\n\n(ctrl+g/enter to '
|
_ssh_key = expanduser(curses_text('enter the location for your private ssh key:\n\n\n\n\n(ctrl+g/enter to '
|
||||||
'confirm)\n\nexample input:\n\n~/.ssh/privkey'))
|
'confirm)\n\nexample input:\n\n~/.ssh/privkey'))
|
||||||
if _key_selected_num == _gen_index:
|
if _key_selected_num == _gen_index:
|
||||||
_passphrase = curses_text('enter your desired ssh keyfile passphrase:\n\n\n\n\n(ctrl+g/enter to confirm)'
|
_passphrase = curses_text('enter your desired ssh keyfile passphrase:\n\n\n\n\n(ctrl+g/enter to confirm)'
|
||||||
'\n\ntip: you can leave this blank to use the keyfile without a passphrase')
|
'\n\nnote: it is recommended to leave this blank and to use the keyfile without '
|
||||||
|
'a passphrase (the keyfile itself serves as your identity)')
|
||||||
run(('ssh-keygen', '-q', '-t', 'ed25519', '-N', _passphrase, '-f', _ssh_key))
|
run(('ssh-keygen', '-q', '-t', 'ed25519', '-N', _passphrase, '-f', _ssh_key))
|
||||||
else:
|
else:
|
||||||
_ssh_key = _keys[_key_selected_num]
|
_ssh_key = _keys[_key_selected_num]
|
||||||
@@ -188,19 +240,41 @@ def ssh_config():
|
|||||||
|
|
||||||
|
|
||||||
# device id configuration
|
# device id configuration
|
||||||
def dev_id_config(_ip, _username_ssh, _port, _identity):
|
def dev_id_config(_port, _username_ssh, _ip, _identity, _reconfig=False):
|
||||||
|
if _reconfig:
|
||||||
|
_sure = curses_radio(('no', 'yes'), "WARNING: ensure this sshyp client is synchronized (up-to-date) before "
|
||||||
|
"changing the device id\n\nfailure to do so may result in sync"
|
||||||
|
"hronization issues\n\nare you sure you wish to change the device id?")
|
||||||
|
if _sure != 1:
|
||||||
|
return
|
||||||
from sshyp import copy_id_check, string_gen
|
from sshyp import copy_id_check, string_gen
|
||||||
_device_id_prefix = curses_text('name this device:\n\n\n\n\n(ctrl+g/enter to confirm)\n\nimportant: this '
|
_device_id_prefix = curses_text('set this device\'s id:\n\n\n\n\n(ctrl+g/enter to confirm)\n\nimportant: this '
|
||||||
'id must be unique amongst your client devices\n\nthis is used to keep track of '
|
'id must be unique amongst your client devices\n\nthis is used to keep track of '
|
||||||
'database syncing and quick-unlock permissions\n')
|
'database synchronization and quick-unlock permissions\n')
|
||||||
_device_id_suffix = string_gen('f', randint(24, 48))
|
_device_id_suffix = string_gen('f', randint(24, 48))
|
||||||
_device_id = _device_id_prefix + '-' + _device_id_suffix
|
_device_id = _device_id_prefix + '-' + _device_id_suffix
|
||||||
# remove existing device ids
|
# remove existing device ids
|
||||||
for _id in listdir(f"{home}/.config/sshyp/devices"):
|
_device_id_list = listdir(f"{home}/.config/sshyp/devices")
|
||||||
|
for _id in _device_id_list:
|
||||||
remove(f"{home}/.config/sshyp/devices/{_id}")
|
remove(f"{home}/.config/sshyp/devices/{_id}")
|
||||||
open(f"{home}/.config/sshyp/devices/{_device_id}", 'w')
|
open(f"{home}/.config/sshyp/devices/{_device_id}", 'w')
|
||||||
# test server connection and attempt to register device id
|
# test server connection and attempt to register device id - only run if _reconfig is True, since the keyfile
|
||||||
copy_id_check(_ip, _username_ssh, _port, _device_id, _identity, sshyp_data)
|
# needs to be registered with the server before a successful connection can be made
|
||||||
|
if _reconfig:
|
||||||
|
# copy_id_check() returns false if successful
|
||||||
|
if not copy_id_check(_port, _username_ssh, _ip, _device_id, _identity, sshyp_data):
|
||||||
|
# remove old device id from registered pool and whitelist
|
||||||
|
run(('ssh', '-o', 'ConnectTimeout=3', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
|
||||||
|
'python3 -c \'from pathlib import Path; '
|
||||||
|
f'Path("/home/{_username_ssh}/.config/sshyp/devices/{_device_id_list[0]}").unlink(missing_ok=True); '
|
||||||
|
f'Path("/home/{_username_ssh}/.config/sshyp/whitelist/{_device_id_list[0]}")'
|
||||||
|
f'.unlink(missing_ok=True)\''), stderr=DEVNULL, stdout=DEVNULL)
|
||||||
|
else:
|
||||||
|
sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'true')
|
||||||
|
write_config()
|
||||||
|
curses_radio(['okay'], 'this device will be registered with the server upon the first successful sync\n\n'
|
||||||
|
'you can force this now by running "sshyp sync"\n\nif you have not done so already, '
|
||||||
|
'ensure your ssh pubkey has been appended to the server\'s authorized_keys file!')
|
||||||
|
|
||||||
|
|
||||||
# quick-unlock configuration
|
# quick-unlock configuration
|
||||||
@@ -232,10 +306,10 @@ def refresh_encryption():
|
|||||||
_directory = f"{home}/.local/share/sshyp"
|
_directory = f"{home}/.local/share/sshyp"
|
||||||
|
|
||||||
# warn the user of potential data loss and prompt to continue
|
# warn the user of potential data loss and prompt to continue
|
||||||
_proceed = curses_radio(('no', 'yes'), "WARNING: proceeding with this action will remove/overwrite any directories"
|
_sure = curses_radio(('no', 'yes'), "WARNING: proceeding with this action will remove/overwrite any directories"
|
||||||
f" matching the following:\n\n{home}/.local/share/sshyp.old\n{home}/.local/"
|
f" matching the following:\n\n{home}/.local/share/sshyp.old\n{home}/.local/"
|
||||||
"share/sshyp.new\n\nare you sure you wish to re-encrypt all entries?")
|
"share/sshyp.new\n\nare you sure you wish to re-encrypt all entries?")
|
||||||
if _proceed != 1:
|
if _sure != 1:
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
# set new gpg key
|
# set new gpg key
|
||||||
@@ -248,8 +322,8 @@ def refresh_encryption():
|
|||||||
|
|
||||||
# prompt for unlock and display do not close warning
|
# prompt for unlock and display do not close warning
|
||||||
decrypt(None)
|
decrypt(None)
|
||||||
curses_radio(['okay'], 'entry optimization may take some time - select "okay" to start - '
|
curses_radio(['okay'], 'entry optimization may take some time (especially on slower devices)\n\nselect "okay" '
|
||||||
'do not terminate this process!')
|
'to start\n\ndo not terminate this process!')
|
||||||
|
|
||||||
# remove existing conflicts
|
# remove existing conflicts
|
||||||
for _extension in ('.new', '.old'):
|
for _extension in ('.new', '.old'):
|
||||||
@@ -274,21 +348,43 @@ def refresh_encryption():
|
|||||||
|
|
||||||
|
|
||||||
# PORT START WHITELIST-SERVER
|
# PORT START WHITELIST-SERVER
|
||||||
|
# removes a registered device id from the server-side pool and prunes the quick-unlock whitelist
|
||||||
|
def registered_dev_id_remover(_back=False):
|
||||||
|
_device_ids = listdir(f"{home}/.config/sshyp/devices") + ['BACK']
|
||||||
|
_whitelisted_ids = listdir(f"{home}/.config/sshyp/whitelist")
|
||||||
|
while not _back:
|
||||||
|
_del_id = curses_radio(_device_ids, 'WARNING: only remove registered device ids if they are no longer in use'
|
||||||
|
'\n\nthe removal of an active device id will result in the malfunction of '
|
||||||
|
'the device still using the removed id\n\nregistered device id to remove:')
|
||||||
|
if _del_id == len(_device_ids) - 1:
|
||||||
|
_back = True
|
||||||
|
else:
|
||||||
|
# remove deleted device id from whitelist
|
||||||
|
if _device_ids[_del_id] in _whitelisted_ids:
|
||||||
|
remove(f"{home}/.config/sshyp/whitelist/{_whitelisted_ids[_del_id]}")
|
||||||
|
_whitelisted_ids = [_id for _id in _whitelisted_ids if _id != _device_ids[_del_id]]
|
||||||
|
# remove deleted device id from device pool
|
||||||
|
remove(f"{home}/.config/sshyp/devices/{_device_ids[_del_id]}")
|
||||||
|
del _device_ids[_del_id]
|
||||||
|
|
||||||
|
|
||||||
# takes input from the user to set up quick-unlock pin
|
# takes input from the user to set up quick-unlock pin
|
||||||
def whitelist_setup():
|
def whitelist_setup():
|
||||||
_gpg_password_temp = str(curses_text('full gpg passphrase:\n\n\n\n\n(ctrl+g/enter to confirm)'))
|
_gpg_password_temp = str(curses_text('full gpg passphrase:\n\n\n\n\n(ctrl+g/enter to confirm)'))
|
||||||
_half_length = int(len(_gpg_password_temp)/2)
|
_half_length = len(_gpg_password_temp) // 2
|
||||||
try:
|
try:
|
||||||
_short_password_length = int(curses_text(f"quick unlock pin length ({_half_length}):\n\n\n\n\n(ctrl+g/enter "
|
_short_password_length = int(curses_text(f"quick unlock pin length ({_half_length}):\n\n\n\n\n(ctrl+g/enter "
|
||||||
"to confirm)\n\npin must be half the length of the gpg passphrase "
|
"to confirm)\n\nnote: do NOT enter your desired pin - this is simply "
|
||||||
"or less\n\ncannot be a negative number"))
|
"an integer used to determine the length of the auto-generated pin"
|
||||||
if not 0 <= _short_password_length <= _half_length:
|
"\n\npin must be half the length of the gpg passphrase or less and "
|
||||||
|
"must be greater than 0"))
|
||||||
|
if not 0 < _short_password_length <= _half_length:
|
||||||
_short_password_length = _half_length
|
_short_password_length = _half_length
|
||||||
except ValueError:
|
except ValueError:
|
||||||
_short_password_length = _half_length
|
_short_password_length = _half_length
|
||||||
_i, _quick_unlock_password, _quick_unlock_password_excluded = 0, '', ''
|
_i, _quick_unlock_password, _quick_unlock_password_excluded = 0, '', ''
|
||||||
for _char in _gpg_password_temp:
|
for _char in _gpg_password_temp:
|
||||||
if _i % 2 == 1 and _i < _short_password_length*2:
|
if _i % 2 == 1 and _i < _short_password_length * 2:
|
||||||
_quick_unlock_password += _char
|
_quick_unlock_password += _char
|
||||||
else:
|
else:
|
||||||
_quick_unlock_password_excluded += _char
|
_quick_unlock_password_excluded += _char
|
||||||
@@ -298,6 +394,8 @@ def whitelist_setup():
|
|||||||
open(f"{home}/.config/sshyp/gpg-gen", 'w').writelines([
|
open(f"{home}/.config/sshyp/gpg-gen", 'w').writelines([
|
||||||
'Key-Type: 1\n', 'Key-Length: 4096\n', 'Key-Usage: sign encrypt\n', 'Name-Real: sshyp\n',
|
'Key-Type: 1\n', 'Key-Length: 4096\n', 'Key-Usage: sign encrypt\n', 'Name-Real: sshyp\n',
|
||||||
'Name-Comment: gpg-sshyp-whitelist\n', 'Name-Email: github.com/rwinkhart/sshyp\n', 'Expire-Date: 0'])
|
'Name-Comment: gpg-sshyp-whitelist\n', 'Name-Email: github.com/rwinkhart/sshyp\n', 'Expire-Date: 0'])
|
||||||
|
curses_radio(['okay'], 'gpg key generation may take some time (especially on slower devices)\n\nselect "okay" '
|
||||||
|
'to start\n\ndo not terminate this process!')
|
||||||
run(('gpg', '-q', '--pinentry-mode', 'loopback', '--batch', '--generate-key', '--passphrase',
|
run(('gpg', '-q', '--pinentry-mode', 'loopback', '--batch', '--generate-key', '--passphrase',
|
||||||
_quick_unlock_password, f"{home}/.config/sshyp/gpg-gen"))
|
_quick_unlock_password, f"{home}/.config/sshyp/gpg-gen"))
|
||||||
remove(f"{home}/.config/sshyp/gpg-gen")
|
remove(f"{home}/.config/sshyp/gpg-gen")
|
||||||
@@ -305,37 +403,38 @@ def whitelist_setup():
|
|||||||
|
|
||||||
# encrypt excluded with the assembly key
|
# encrypt excluded with the assembly key
|
||||||
from sshyp import encrypt
|
from sshyp import encrypt
|
||||||
encrypt(_quick_unlock_password_excluded, f"{home}/.config/sshyp/excluded", _gpg_id)
|
encrypt([_quick_unlock_password_excluded], f"{home}/.config/sshyp/excluded", _gpg_id)
|
||||||
curses_radio(['okay, I have it memorized'], f"your quick-unlock pin: {_quick_unlock_password}")
|
curses_radio(['okay, I have it memorized'], f"your quick-unlock pin: {_quick_unlock_password}")
|
||||||
|
|
||||||
|
|
||||||
# adds or removes quick-unlock whitelisted device ids
|
# adds or removes quick-unlock whitelisted device ids
|
||||||
def whitelist_manage(_action):
|
def whitelist_manage(_action, _back=False):
|
||||||
_whitelisted_ids = listdir(f"{home}/.config/sshyp/whitelist")
|
_whitelisted_ids = listdir(f"{home}/.config/sshyp/whitelist") + ['BACK']
|
||||||
_device_ids = listdir(f"{home}/.config/sshyp/devices")
|
_device_ids = listdir(f"{home}/.config/sshyp/devices")
|
||||||
|
|
||||||
# a value of True indicates adding
|
_unwhitelisted_ids = []
|
||||||
if _action:
|
for _id in _device_ids:
|
||||||
_unwhitelisted_ids = []
|
if _id not in _whitelisted_ids:
|
||||||
for _id in _device_ids:
|
_unwhitelisted_ids.append(_id)
|
||||||
if _id not in _whitelisted_ids:
|
_unwhitelisted_ids.append('BACK')
|
||||||
_unwhitelisted_ids.append(_id)
|
|
||||||
_unwhitelisted_ids.append('cancel')
|
|
||||||
_add_id = curses_radio(_unwhitelisted_ids, 'id to add to whitelist:')
|
|
||||||
if _add_id == len(_unwhitelisted_ids)-1:
|
|
||||||
return
|
|
||||||
open(f"{home}/.config/sshyp/whitelist/{_unwhitelisted_ids[_add_id]}", 'w').write('')
|
|
||||||
else:
|
|
||||||
_whitelisted_choices = _whitelisted_ids + ['cancel']
|
|
||||||
_del_id = curses_radio(_whitelisted_choices, 'id to remove from whitelist:')
|
|
||||||
if _del_id == len(_whitelisted_choices)-1:
|
|
||||||
return
|
|
||||||
remove(f"{home}/.config/sshyp/whitelist/{_whitelisted_ids[_del_id]}")
|
|
||||||
|
|
||||||
# prune deleted device ids from whitelist
|
while not _back:
|
||||||
for _id in _whitelisted_ids:
|
# _action == True indicates adding
|
||||||
if _id not in _device_ids:
|
if _action:
|
||||||
remove(f"{home}/.config/sshyp/whitelist/{_id}")
|
_add_id = curses_radio(_unwhitelisted_ids, 'id to add to whitelist:')
|
||||||
|
if _add_id == len(_unwhitelisted_ids) - 1:
|
||||||
|
_back = True
|
||||||
|
else:
|
||||||
|
open(f"{home}/.config/sshyp/whitelist/{_unwhitelisted_ids[_add_id]}", 'w').write('')
|
||||||
|
del _unwhitelisted_ids[_add_id]
|
||||||
|
# _action == False indicates removing
|
||||||
|
else:
|
||||||
|
_del_id = curses_radio(_whitelisted_ids, 'id to remove from whitelist:')
|
||||||
|
if _del_id == len(_whitelisted_ids) - 1:
|
||||||
|
_back = True
|
||||||
|
else:
|
||||||
|
remove(f"{home}/.config/sshyp/whitelist/{_whitelisted_ids[_del_id]}")
|
||||||
|
del _whitelisted_ids[_del_id]
|
||||||
|
|
||||||
|
|
||||||
# runs quick-unlock configuration menu
|
# runs quick-unlock configuration menu
|
||||||
@@ -360,17 +459,21 @@ def extension_downloader():
|
|||||||
from os import chmod
|
from os import chmod
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
from urllib.request import urlopen, urlretrieve
|
from urllib.request import urlopen, urlretrieve
|
||||||
_file_data = urlopen("https://raw.githubusercontent.com/rwinkhart/sshyp-labs/main/pointers/v1.5.1").read()
|
# the version listed below will NOT always match the version of sshyp being used
|
||||||
|
# it is only updated if new extensions are incompatible with previous sshyp versions
|
||||||
|
_file_data = urlopen("https://raw.githubusercontent.com/rwinkhart/sshyp-labs/main/pointers/v1.5.2").read()
|
||||||
_pointer = ConfigParser(interpolation=None)
|
_pointer = ConfigParser(interpolation=None)
|
||||||
_pointer.read_string(_file_data.decode('utf-8'))
|
_pointer.read_string(_file_data.decode('utf-8'))
|
||||||
_extensions = _pointer.sections()
|
_extensions = _pointer.sections()
|
||||||
_extensions.append('CANCEL')
|
_extensions.append('BACK')
|
||||||
_choice = curses_radio(_extensions, 'select an extension for more info')
|
_choice = curses_radio(_extensions, 'select an extension for more info')
|
||||||
if _choice == len(_extensions)-1:
|
if _choice == len(_extensions) - 1:
|
||||||
return False
|
return False
|
||||||
_selected = _extensions[_choice]
|
_selected = _extensions[_choice]
|
||||||
_choice = curses_radio(('no', 'yes'), f"description: {_pointer.get(_selected, 'desc')}\n\nusage: "
|
_divider = (stdscr.getmaxyx()[1]) * '-'
|
||||||
f"{_pointer.get(_selected, 'usage')}\n\ninstall {_selected}?")
|
_choice = curses_radio(('no', 'yes'), '# description\n' + _divider + '\n\n' + _pointer.get(_selected, 'desc') +
|
||||||
|
'\n\n# usage\n' + _divider + '\n\n' + _pointer.get(_selected, 'usage').replace('<br>', '\n')
|
||||||
|
+ '\n\n' + _divider + '\n\ninstall ' + _selected + '?')
|
||||||
# if installing the extension...
|
# if installing the extension...
|
||||||
if _choice == 1:
|
if _choice == 1:
|
||||||
# download extension files to temporary directory
|
# download extension files to temporary directory
|
||||||
@@ -389,9 +492,9 @@ def extension_remover():
|
|||||||
_installed = []
|
_installed = []
|
||||||
for _extension in listdir('/usr/lib/sshyp/extensions'):
|
for _extension in listdir('/usr/lib/sshyp/extensions'):
|
||||||
_installed.append(_extension[:-4])
|
_installed.append(_extension[:-4])
|
||||||
_installed.append('CANCEL')
|
_installed.append('BACK')
|
||||||
_choice = curses_radio(_installed, 'select an extension to uninstall')
|
_choice = curses_radio(_installed, 'select an extension to uninstall')
|
||||||
if _choice == len(_installed)-1:
|
if _choice == len(_installed) - 1:
|
||||||
return False
|
return False
|
||||||
_sure = curses_radio(('no', 'yes'), f"are you sure you want to remove {_installed[_choice]}?")
|
_sure = curses_radio(('no', 'yes'), f"are you sure you want to remove {_installed[_choice]}?")
|
||||||
if _sure == 0:
|
if _sure == 0:
|
||||||
@@ -438,14 +541,14 @@ def global_menu(_scr, _device_type, _top_message):
|
|||||||
while True:
|
while True:
|
||||||
_options, _choice, _exit_signal = ['change device/synchronization types'], 0, False
|
_options, _choice, _exit_signal = ['change device/synchronization types'], 0, False
|
||||||
if _device_type == 'client':
|
if _device_type == 'client':
|
||||||
_options.extend(['change gpg key', 're-configure ssh(ync)', 'change device name',
|
_options.extend(['change gpg key', 're-configure ssh(ync)', 'change device id',
|
||||||
'[OPTIONAL, RECOMMENDED] set custom text editor',
|
'[OPTIONAL, RECOMMENDED] set custom text editor',
|
||||||
'[OPTIONAL] enable/disable quick-unlock',
|
'[OPTIONAL] enable/disable quick-unlock',
|
||||||
'[OPTIONAL] re-encrypt/optimize entries',
|
'[OPTIONAL] re-encrypt/optimize entries',
|
||||||
'[OPTIONAL] extension management'])
|
'[OPTIONAL] extension management'])
|
||||||
else:
|
else:
|
||||||
_options.extend(['manage quick-unlock/whitelist'])
|
_options.extend(['remove registered device ids', 'manage quick-unlock/whitelist'])
|
||||||
_options.extend(['EXIT/DONE'])
|
_options.append('EXIT/DONE')
|
||||||
_choice += curses_radio(_options, _top_message)
|
_choice += curses_radio(_options, _top_message)
|
||||||
|
|
||||||
if _choice == 0:
|
if _choice == 0:
|
||||||
@@ -458,7 +561,7 @@ def global_menu(_scr, _device_type, _top_message):
|
|||||||
# ...and text editor settings are missing
|
# ...and text editor settings are missing
|
||||||
if not sshyp_data.has_option('CLIENT-GENERAL', 'text_editor'):
|
if not sshyp_data.has_option('CLIENT-GENERAL', 'text_editor'):
|
||||||
editor_config(True)
|
editor_config(True)
|
||||||
# ...and online (synced) mode is enabled...
|
# ...and online (synchronized) mode is enabled...
|
||||||
if _dev_sync_types[1] == 'false':
|
if _dev_sync_types[1] == 'false':
|
||||||
# ...and quick-unlock settings are missing
|
# ...and quick-unlock settings are missing
|
||||||
if not sshyp_data.has_option('CLIENT-ONLINE', 'quick_unlock_enabled'):
|
if not sshyp_data.has_option('CLIENT-ONLINE', 'quick_unlock_enabled'):
|
||||||
@@ -472,7 +575,7 @@ def global_menu(_scr, _device_type, _top_message):
|
|||||||
if not listdir(f"{home}/.config/sshyp/devices"):
|
if not listdir(f"{home}/.config/sshyp/devices"):
|
||||||
if None in (_ip, _username_ssh, _port):
|
if None in (_ip, _username_ssh, _port):
|
||||||
_ip, _username_ssh, _port, _identity = ssh_config()
|
_ip, _username_ssh, _port, _identity = ssh_config()
|
||||||
dev_id_config(_ip, _username_ssh, _port, _identity)
|
dev_id_config(_port, _username_ssh, _ip, _identity)
|
||||||
# ...or ssh_error is missing
|
# ...or ssh_error is missing
|
||||||
elif not sshyp_data.has_option('CLIENT-ONLINE', 'ssh_error'):
|
elif not sshyp_data.has_option('CLIENT-ONLINE', 'ssh_error'):
|
||||||
sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1')
|
sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1')
|
||||||
@@ -482,17 +585,20 @@ def global_menu(_scr, _device_type, _top_message):
|
|||||||
if _device_type == 'client':
|
if _device_type == 'client':
|
||||||
gpg_config()
|
gpg_config()
|
||||||
else:
|
else:
|
||||||
whitelist_menu()
|
registered_dev_id_remover()
|
||||||
elif _choice == 2:
|
elif _choice == 2:
|
||||||
if _device_type == 'client':
|
if _device_type == 'client':
|
||||||
ssh_config()
|
ssh_config(True)
|
||||||
|
else:
|
||||||
|
whitelist_menu()
|
||||||
|
elif _choice == 3:
|
||||||
|
if _device_type == 'client':
|
||||||
|
if not sshyp_data.has_section('SSHYNC'):
|
||||||
|
ssh_config()
|
||||||
|
dev_id_config(sshyp_data.get('SSHYNC', 'port'), sshyp_data.get('SSHYNC', 'user'),
|
||||||
|
sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'identity_file'), True)
|
||||||
else:
|
else:
|
||||||
_exit_signal = True
|
_exit_signal = True
|
||||||
elif _choice == 3:
|
|
||||||
if not sshyp_data.has_section('SSHYNC'):
|
|
||||||
ssh_config()
|
|
||||||
dev_id_config(sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'user'),
|
|
||||||
sshyp_data.get('SSHYNC', 'port'), sshyp_data.get('SSHYNC', 'identity_file'))
|
|
||||||
elif _choice == 4:
|
elif _choice == 4:
|
||||||
editor_config(False)
|
editor_config(False)
|
||||||
elif _choice == 5:
|
elif _choice == 5:
|
||||||
@@ -553,12 +659,11 @@ def initial_setup(_scr):
|
|||||||
|
|
||||||
# online (synchronized mode) configuration
|
# online (synchronized mode) configuration
|
||||||
if _dev_sync_types[1] != 'true':
|
if _dev_sync_types[1] != 'true':
|
||||||
|
|
||||||
# ssh+sshync configuration
|
# ssh+sshync configuration
|
||||||
_ip, _username_ssh, _port, _identity = ssh_config()
|
_ip, _username_ssh, _port, _identity = ssh_config()
|
||||||
|
|
||||||
# device id configuration
|
# device id configuration
|
||||||
dev_id_config(_ip, _username_ssh, _port, _identity)
|
dev_id_config(_port, _username_ssh, _ip, _identity)
|
||||||
|
|
||||||
# PORT START CLIPTOOL
|
# PORT START CLIPTOOL
|
||||||
# check for clipboard tool and display warning if missing
|
# check for clipboard tool and display warning if missing
|
||||||
@@ -578,39 +683,47 @@ def initial_setup(_scr):
|
|||||||
# run optional configuration menu
|
# run optional configuration menu
|
||||||
curses_radio(['okay'], 'required configuration complete\n\na menu for additional (optional) configuration will be '
|
curses_radio(['okay'], 'required configuration complete\n\na menu for additional (optional) configuration will be '
|
||||||
'displayed\n\nthis menu can be safely exited at any time')
|
'displayed\n\nthis menu can be safely exited at any time')
|
||||||
wrapped_entry(_dev_sync_types[0], 'additional configuration options:')
|
|
||||||
|
# set gm_device_type so that after the init menu is terminated the global menu knows the device type
|
||||||
|
global gm_device_type
|
||||||
|
gm_device_type = _dev_sync_types[0]
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
# runs the specified entry function (menu start point) within a curses wrapper
|
# runs the specified entry function (menu start point) within a curses wrapper
|
||||||
def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'):
|
def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'):
|
||||||
from curses import wrapper, use_default_colors
|
from curses import wrapper, use_default_colors
|
||||||
|
|
||||||
|
global gm_device_type
|
||||||
|
gm_device_type = _gm_device_type
|
||||||
|
|
||||||
# a boolean value represents init
|
# a boolean value represents init
|
||||||
if isinstance(_gm_device_type, bool):
|
if isinstance(gm_device_type, bool):
|
||||||
wrapper(lambda _wrap_stdscr: (use_default_colors(), initial_setup(_wrap_stdscr)))
|
wrapper(lambda _wrap_stdscr: (use_default_colors(), initial_setup(_wrap_stdscr)))
|
||||||
# any other value will be provided as the global menu device type
|
# any other value will be interpreted as the global menu device type
|
||||||
else:
|
# this code still runs when called for init once the init menu terminates
|
||||||
_repeat = True
|
_repeat = True
|
||||||
while _repeat:
|
while _repeat:
|
||||||
try:
|
try:
|
||||||
_ext_name, _escalator, _action = \
|
_ext_name, _escalator, _action = \
|
||||||
wrapper(lambda _wrap_stdscr: (use_default_colors(),
|
wrapper(lambda _wrap_stdscr: (use_default_colors(),
|
||||||
global_menu(_wrap_stdscr, _gm_device_type, _gm_top_message)))[1]
|
global_menu(_wrap_stdscr, gm_device_type, _gm_top_message)))[1]
|
||||||
except ChildProcessError:
|
except ChildProcessError:
|
||||||
print("\n\u001b[38;5;9merror: privilege escalation required\n\nneither 'doas' nor 'sudo' were found in "
|
print("\n\u001b[38;5;9merror: privilege escalation required\n\nneither 'doas' nor 'sudo' were found in "
|
||||||
"the system's $PATH\u001b[0m\n")
|
"the system's $PATH\u001b[0m\n")
|
||||||
return
|
return
|
||||||
# only run if privilege escalation is needed
|
# only run if privilege escalation is needed
|
||||||
if _action is not None:
|
if _action is not None:
|
||||||
if _action:
|
if _action:
|
||||||
# install with privilege escalation (outside of curses)
|
# install with privilege escalation (outside of curses)
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
|
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
|
||||||
run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir))
|
run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir))
|
||||||
run((_escalator, 'mv', _exe_dir, f"/usr/lib/sshyp/{_ext_name}"))
|
run((_escalator, 'mv', _exe_dir, f"/usr/lib/sshyp/{_ext_name}"))
|
||||||
run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
|
run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
|
||||||
else:
|
|
||||||
# uninstall with privilege escalation (outside of curses)
|
|
||||||
run((_escalator, 'rm', '-I', f"/usr/lib/sshyp/{_ext_name}",
|
|
||||||
f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
|
|
||||||
else:
|
else:
|
||||||
_repeat = False
|
# uninstall with privilege escalation (outside of curses)
|
||||||
|
run((_escalator, 'rm', f"/usr/lib/sshyp/{_ext_name}",
|
||||||
|
f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
|
||||||
|
else:
|
||||||
|
_repeat = False
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
version=$(head -n1 extra/changelog-archive/2023 | cut -c8-)
|
version=$(head -n1 extra/changelog-archive/2024 | cut -c8-)
|
||||||
if [ -z "$2" ]; then
|
if [ -z "$2" ]; then
|
||||||
revision=1
|
revision=1
|
||||||
else
|
else
|
||||||
@@ -135,7 +135,7 @@ licenses {
|
|||||||
\"GNU GPL v3\"
|
\"GNU GPL v3\"
|
||||||
}
|
}
|
||||||
copyrights {
|
copyrights {
|
||||||
\"2021-2023 Randall Winkhart\"
|
\"2021-2024 Randall Winkhart\"
|
||||||
}
|
}
|
||||||
provides {
|
provides {
|
||||||
sshyp_client = "$version"
|
sshyp_client = "$version"
|
||||||
@@ -313,6 +313,7 @@ cp -r %%{_sourcedir}/usr %%{buildroot}
|
|||||||
/usr/lib/sshyp/sshyp.py
|
/usr/lib/sshyp/sshyp.py
|
||||||
/usr/lib/sshyp/sshync.py
|
/usr/lib/sshyp/sshync.py
|
||||||
/usr/lib/sshyp/stweak.py
|
/usr/lib/sshyp/stweak.py
|
||||||
|
/usr/lib/sshyp/clipclear.py
|
||||||
/usr/lib/sshyp/extensions/
|
/usr/lib/sshyp/extensions/
|
||||||
/usr/share/bash-completion/completions/sshyp
|
/usr/share/bash-completion/completions/sshyp
|
||||||
/usr/share/zsh/site-functions/_sshyp
|
/usr/share/zsh/site-functions/_sshyp
|
||||||
@@ -377,6 +378,7 @@ printf "/usr/bin/sshyp
|
|||||||
/usr/lib/sshyp/sshyp.py
|
/usr/lib/sshyp/sshyp.py
|
||||||
/usr/lib/sshyp/sshync.py
|
/usr/lib/sshyp/sshync.py
|
||||||
/usr/lib/sshyp/stweak.py
|
/usr/lib/sshyp/stweak.py
|
||||||
|
/usr/lib/sshyp/clipclear.py
|
||||||
/usr/local/share/bash-completion/completions/sshyp
|
/usr/local/share/bash-completion/completions/sshyp
|
||||||
/usr/local/share/zsh/site-functions/_sshyp
|
/usr/local/share/zsh/site-functions/_sshyp
|
||||||
/usr/share/licenses/sshyp/license
|
/usr/share/licenses/sshyp/license
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ if argv[1] == hash_paste.hexdigest():
|
|||||||
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout)
|
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout)
|
||||||
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'x11'))"""
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'x11'))"""
|
||||||
clear_replacement = """if argv[2] == 'wayland':
|
clear_replacement = """if argv[2] == 'wayland':
|
||||||
hash_paste.update(run('wl-paste', stdout=PIPE).stdout.strip())
|
hash_paste.update(run('wl-paste', stdout=PIPE, stderr=DEVNULL).stdout.strip())
|
||||||
if argv[1] == hash_paste.hexdigest():
|
if argv[1] == hash_paste.hexdigest():
|
||||||
run(('wl-copy', '-c'))
|
run(('wl-copy', '-c'))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
sshyp is a FOSS password manager that uses an sftp-based syncing back-end.
|
sshyp is a FOSS password manager that uses an sftp-based syncing back-end.
|
||||||
Copyright (C) 2021-2023 Randall Winkhart idgr@tutanota.com
|
Copyright (C) 2021-2024 Randall Winkhart idgr@tutanota.com
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of version 3 (only) of the GNU General Public License
|
it under the terms of version 3 (only) of the GNU General Public License
|
||||||
|
|||||||
Reference in New Issue
Block a user