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())
|
||||||
|
|||||||
+5
-5
@@ -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
|
||||||
|
|||||||
+43
-41
@@ -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
|
||||||
@@ -192,8 +193,9 @@ 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
|
||||||
@@ -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
|
||||||
@@ -294,18 +296,18 @@ def print_info():
|
|||||||
}}-}}-*]{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
|
||||||
@@ -336,7 +338,7 @@ def print_info():
|
|||||||
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':
|
||||||
@@ -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)
|
||||||
@@ -626,7 +628,7 @@ if __name__ == "__main__":
|
|||||||
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:
|
||||||
@@ -663,7 +665,7 @@ if __name__ == "__main__":
|
|||||||
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
|
||||||
|
|||||||
+180
-67
@@ -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
|
||||||
|
_height, _width = stdscr.getmaxyx()
|
||||||
|
|
||||||
|
# track whether to generate interactive buttons (depending on terminal size)
|
||||||
|
_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:
|
||||||
|
_button_gen = False
|
||||||
|
stdscr.addstr(_current_line, 0, '# warning: re-size terminal to see more information'[:_width - 1])
|
||||||
|
_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):
|
for _i, _option in enumerate(_options):
|
||||||
_y = _i + _pretext.count('\n') + 2
|
_y = _i + _current_line + 1
|
||||||
|
if _y < _height:
|
||||||
if _i == _selected:
|
if _i == _selected:
|
||||||
stdscr.addstr(_y, 0, "[*] " + _option, A_REVERSE)
|
stdscr.addstr(_y, 0, "[*] " + _option, A_REVERSE)
|
||||||
else:
|
else:
|
||||||
stdscr.addstr(_y, 0, "[ ] " + _option)
|
stdscr.addstr(_y, 0, "[ ] " + _option)
|
||||||
stdscr.refresh()
|
else:
|
||||||
_key = stdscr.getch()
|
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
|
||||||
|
_key = stdscr.getch()
|
||||||
|
if _button_gen:
|
||||||
if _key == KEY_UP:
|
if _key == KEY_UP:
|
||||||
_selected = (_selected - 1) % len(_options)
|
_selected = (_selected - 1) % len(_options)
|
||||||
elif _key == KEY_DOWN:
|
elif _key == KEY_DOWN:
|
||||||
_selected = (_selected + 1) % len(_options)
|
_selected = (_selected + 1) % len(_options)
|
||||||
elif _key == ord('\n'):
|
# ord('\n') == 10
|
||||||
|
elif _key == 10:
|
||||||
break
|
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
|
||||||
@@ -108,7 +145,16 @@ def gpg_config():
|
|||||||
'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?')
|
||||||
|
if _reconfig and _key_selected_num == len(_keys) - 1:
|
||||||
|
return
|
||||||
_gen_index = len(_keys) - 2
|
_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,15 +348,37 @@ 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
|
||||||
@@ -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
|
|
||||||
if _action:
|
|
||||||
_unwhitelisted_ids = []
|
_unwhitelisted_ids = []
|
||||||
for _id in _device_ids:
|
for _id in _device_ids:
|
||||||
if _id not in _whitelisted_ids:
|
if _id not in _whitelisted_ids:
|
||||||
_unwhitelisted_ids.append(_id)
|
_unwhitelisted_ids.append(_id)
|
||||||
_unwhitelisted_ids.append('cancel')
|
_unwhitelisted_ids.append('BACK')
|
||||||
|
|
||||||
|
while not _back:
|
||||||
|
# _action == True indicates adding
|
||||||
|
if _action:
|
||||||
_add_id = curses_radio(_unwhitelisted_ids, 'id to add to whitelist:')
|
_add_id = curses_radio(_unwhitelisted_ids, 'id to add to whitelist:')
|
||||||
if _add_id == len(_unwhitelisted_ids) - 1:
|
if _add_id == len(_unwhitelisted_ids) - 1:
|
||||||
return
|
_back = True
|
||||||
|
else:
|
||||||
open(f"{home}/.config/sshyp/whitelist/{_unwhitelisted_ids[_add_id]}", 'w').write('')
|
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:
|
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]}")
|
remove(f"{home}/.config/sshyp/whitelist/{_whitelisted_ids[_del_id]}")
|
||||||
|
del _whitelisted_ids[_del_id]
|
||||||
# prune deleted device ids from whitelist
|
|
||||||
for _id in _whitelisted_ids:
|
|
||||||
if _id not in _device_ids:
|
|
||||||
remove(f"{home}/.config/sshyp/whitelist/{_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,7 +492,7 @@ 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
|
||||||
@@ -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:
|
else:
|
||||||
_exit_signal = True
|
whitelist_menu()
|
||||||
elif _choice == 3:
|
elif _choice == 3:
|
||||||
|
if _device_type == 'client':
|
||||||
if not sshyp_data.has_section('SSHYNC'):
|
if not sshyp_data.has_section('SSHYNC'):
|
||||||
ssh_config()
|
ssh_config()
|
||||||
dev_id_config(sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'user'),
|
dev_id_config(sshyp_data.get('SSHYNC', 'port'), sshyp_data.get('SSHYNC', 'user'),
|
||||||
sshyp_data.get('SSHYNC', 'port'), sshyp_data.get('SSHYNC', 'identity_file'))
|
sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'identity_file'), True)
|
||||||
|
else:
|
||||||
|
_exit_signal = True
|
||||||
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,23 +683,31 @@ 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")
|
||||||
@@ -610,7 +723,7 @@ def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'):
|
|||||||
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:
|
else:
|
||||||
# uninstall with privilege escalation (outside of curses)
|
# uninstall with privilege escalation (outside of curses)
|
||||||
run((_escalator, 'rm', '-I', f"/usr/lib/sshyp/{_ext_name}",
|
run((_escalator, 'rm', f"/usr/lib/sshyp/{_ext_name}",
|
||||||
f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
|
f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
|
||||||
else:
|
else:
|
||||||
_repeat = False
|
_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