mirror of
https://github.com/rwinkhart/sshyp.git
synced 2026-09-03 15:47:18 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bcf5fc0d1 | ||
|
|
ec3ee297b8 | ||
|
|
5f5ede7349 | ||
|
|
cc3fa30865 | ||
|
|
3613d3dd0c | ||
|
|
249269a3e3 | ||
|
|
c441802058 | ||
|
|
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 | ||
|
|
1f5a2c8f3c | ||
|
|
386044d081 | ||
|
|
c651c41a41 | ||
|
|
7cb245edde | ||
|
|
edf31f9a7c | ||
|
|
362b1aa025 | ||
|
|
5633520d82 | ||
|
|
a487bd561a | ||
|
|
cbc0538e82 | ||
|
|
5c589de4f2 | ||
|
|
44392467e5 | ||
|
|
18aacf648b | ||
|
|
ac621a8a73 | ||
|
|
98f76ac1ab | ||
|
|
bdff85f024 | ||
|
|
2bcabe8597 | ||
|
|
7259a8732c | ||
|
|
20cdc27b88 | ||
|
|
2f6bdb8d2e | ||
|
|
eb4fae3e9e | ||
|
|
30afbafcbf | ||
|
|
e010180219 | ||
|
|
1f4e7d5cae | ||
|
|
67983da22e | ||
|
|
aa0bd017c9 | ||
|
|
5a4e952d4d | ||
|
|
db05e9b05e | ||
|
|
397ad7d409 | ||
|
|
5af02866b4 | ||
|
|
e8267f838a | ||
|
|
e4fbcd6825 | ||
|
|
9ba4b8d724 | ||
|
|
0b73d4016f |
@@ -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.
|
||||||
@@ -77,9 +77,10 @@ All available options can be found with `sshyp help`, or alternatively, in the m
|
|||||||
# Roadmap
|
# Roadmap
|
||||||
Short-term Goals:
|
Short-term Goals:
|
||||||
|
|
||||||
- migrate from gpg to a better-suited utility focused on symmetric cipher encryption
|
- implement a method for tracking how long it has been since a password has been changed (password aging)
|
||||||
|
- find and fix more bugs
|
||||||
|
|
||||||
Long-term Goals:
|
Long-term Goals:
|
||||||
|
|
||||||
- improve OS compatibility
|
- migrate from gpg to a better-suited utility focused on symmetric cipher encryption
|
||||||
- seize the thrones, shear the humans
|
- seize the thrones, shear the humans
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
git add -f extra lib/sshyp.py lib/sshync.py lib/stweak.py port-jobs share LICENSE README.md package.sh commit.sh .gitignore
|
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 commit -m "$1"
|
git commit -m "$1"
|
||||||
git push
|
git push
|
||||||
|
|||||||
@@ -1,3 +1,34 @@
|
|||||||
|
sshyp v1.5.1
|
||||||
|
10/24/2023
|
||||||
|
|
||||||
|
the fortified flock update - patch one
|
||||||
|
|
||||||
|
this release fixes both long and short-standing bugs that managed to escape discovery in testing;
|
||||||
|
it also addresses some issues that first-time users may be put-off by upon encountering
|
||||||
|
|
||||||
|
user-facing features:
|
||||||
|
|
||||||
|
- the clipboard is now only cleared if the contents of the clipboard still match what was placed onto it by sshyp
|
||||||
|
^ this means that sshyp will no longer remove data from the clipboard that it did not place there
|
||||||
|
- a warning/explanation is now provided when the user attempts to sync in offline mode
|
||||||
|
- a readable error is now provided if the user attempts to edit a note and the specified text editor cannot be found
|
||||||
|
- the optional configuration menu is now explained when encountered during "sshyp init"
|
||||||
|
- the man page now includes common troubleshooting information
|
||||||
|
|
||||||
|
fixes/optimizations:
|
||||||
|
|
||||||
|
- custom, user-provided ssh keyfiles are now properly utilized in all cases
|
||||||
|
^ previously, using custom ssh keyfiles would break various features
|
||||||
|
- clipboard contents are no longer leaked via the process list on Wayland
|
||||||
|
- interpolation has been disabled for ConfigParser, allowing "%" to be stored in configuration files
|
||||||
|
- gpg keys with ":" or "\" in their names are now properly written to the configuration file
|
||||||
|
- copy_id_check() and whitelist_verify() are now safely callable from extensions
|
||||||
|
- the ssh key selection will no longer see "known_hosts.old" as a valid keyfile
|
||||||
|
- when possible, data from the configuration file is now read as boolean values
|
||||||
|
- the Haiku package has been updated to use Python 3.11
|
||||||
|
|
||||||
|
<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
|
||||||
|
|
||||||
sshyp v1.5.0
|
sshyp v1.5.0
|
||||||
07/12/2023
|
07/12/2023
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
sshyp v1.5.3
|
||||||
|
01/08/2024
|
||||||
|
|
||||||
|
the fortified flock update - patch three
|
||||||
|
|
||||||
|
this release is a quick hot-fix for some bugs that were noticed after the release of v1.5.2
|
||||||
|
|
||||||
|
fixes/optimizations:
|
||||||
|
|
||||||
|
- fixed a regression with the entry list causing entries not part of a subdirectory to appear
|
||||||
|
in-line with the entry list header
|
||||||
|
- fixed extension group ownership under FreeBSD
|
||||||
|
^ the "wheel" group is now used where previously the non-existent "root" group was used
|
||||||
|
- fixed notes sometimes being appended in the wrong position after editing an incomplete entry
|
||||||
|
- addressed flake8 suggestions (slight optimization)
|
||||||
|
- removed unnecessary empty variable declarations
|
||||||
|
^ these were previously included to silence PyCharm's PEP8 warnings
|
||||||
|
^ the project is now primarily developed in VSCodium with the flake8 linter
|
||||||
|
^ the only change to flake8 defaults in the line length limit (79->120)
|
||||||
|
|
||||||
|
<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>
|
||||||
|
|
||||||
|
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
|
||||||
+28
-2
@@ -1,4 +1,4 @@
|
|||||||
.TH sshyp 1 "12 July 2023" "v1.5.0" "sshyp man page"
|
.TH sshyp 1 "08 January 2024" "v1.5.3" "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
|
||||||
@@ -99,6 +99,32 @@ Client setup:
|
|||||||
Done!
|
Done!
|
||||||
|
|
||||||
Please note that the server setup intentionally does not allow the reading of entries (it does not allow adding a gpg decryption key). For security purposes, only clients can read entries.
|
Please note that the server setup intentionally does not allow the reading of entries (it does not allow adding a gpg decryption key). For security purposes, only clients can read entries.
|
||||||
|
.SH TROUBLESHOOTING
|
||||||
|
.B Clipboard not clearing:
|
||||||
|
|
||||||
|
The intended way for sshyp to interact with the system clipboard is for it to clear it 30 seconds after copying a field. Unfortunately, this does not work by default on all systems due to the prevalence of clipboard managers.
|
||||||
|
|
||||||
|
Clipboard managers save a history of what has been copied to the clipboard, which is already a big enough issue on its own for people who copy sensitive information to their clipboard. Some clipboard managers simply will not allow the clipboard to be empty and will replace its contents with the last copied item if you attempt to clear it. One such naughty clipboard manager is KDE Klipper, which comes packaged into KDE Plasma and is typically enabled by default on most distributions. Due to this behavior, KDE Klipper breaks sshyp's clipboard clearing functionality and should not be left enabled.
|
||||||
|
|
||||||
|
It is likely other popular clipboard managers exhibit this behavior as well (Klipper is just the one I first noticed it with). Clipboard managers should not be enabled by default in any environment or distribution due to their potential security implications.
|
||||||
|
|
||||||
|
.B Shell completions not working:
|
||||||
|
ZSH:
|
||||||
|
Make sure your ~/.zshrc contains the following:
|
||||||
|
autoload -Uz compinit && compinit
|
||||||
|
Restart your shell
|
||||||
|
|
||||||
|
Bash:
|
||||||
|
Install your distribution's 'bash-completion' package and restart your shell
|
||||||
|
OR
|
||||||
|
Source the completion script manually by adding the following to your ~/.bashrc (then restart your shell):
|
||||||
|
Linux:
|
||||||
|
source /usr/share/bash-completion/completions/sshyp
|
||||||
|
FreeBSD:
|
||||||
|
source /usr/local/share/bash-completion/completions/sshyp
|
||||||
|
Haiku:
|
||||||
|
source /system/data/bash-completion/completions/sshyp
|
||||||
|
|
||||||
.SH EXIT CODES
|
.SH EXIT CODES
|
||||||
0 - no error
|
0 - no error
|
||||||
|
|
||||||
|
|||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from hashlib import sha512
|
||||||
|
from subprocess import DEVNULL, PIPE, run
|
||||||
|
from sys import argv
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
# set period of time to wait before attempting to clear clipboard
|
||||||
|
sleep(30)
|
||||||
|
# enable the storing of clipboard contents hash for later comparison
|
||||||
|
hash_paste = sha512()
|
||||||
|
|
||||||
|
# PORT START CLIPCLEAR
|
||||||
|
if argv[2] == 'wsl':
|
||||||
|
hash_paste.update(run(('powershell.exe', '-c', 'Get-Clipboard'), stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(('powershell.exe', '-c', 'Set-Clipboard'))
|
||||||
|
|
||||||
|
elif argv[2] == 'wayland':
|
||||||
|
hash_paste.update(run('wl-paste', stdout=PIPE, stderr=DEVNULL).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(('wl-copy', '-c'))
|
||||||
|
|
||||||
|
elif argv[2] == 'haiku':
|
||||||
|
hash_paste.update(run(('clipboard', '-p'), stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(('clipboard', '-r'))
|
||||||
|
|
||||||
|
elif argv[2] == 'mac':
|
||||||
|
hash_paste.update(run('pbpaste', stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run('pbcopy', input=b'')
|
||||||
|
|
||||||
|
elif argv[2] == 'termux':
|
||||||
|
hash_paste.update(run('termux-clipboard-get', stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(("termux-clipboard-set", ''))
|
||||||
|
|
||||||
|
elif argv[2] == 'x11':
|
||||||
|
hash_paste.update(run(('xclip', '-o', '-sel', 'c'), stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(('xclip', '-i', '/dev/null', '-sel', 'c'))
|
||||||
|
# PORT END CLIPCLEAR
|
||||||
+8
-8
@@ -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
|
||||||
@@ -124,7 +124,7 @@ def sort_titles_mods(_list_1, _list_2):
|
|||||||
# returns a list of data read from a sshync job profile
|
# returns a list of data read from a sshync job profile
|
||||||
def get_profile(_profile_dir):
|
def get_profile(_profile_dir):
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
_profile_data = ConfigParser()
|
_profile_data = ConfigParser(interpolation=None)
|
||||||
if isfile(_profile_dir):
|
if isfile(_profile_dir):
|
||||||
_profile_data.read(_profile_dir)
|
_profile_data.read(_profile_dir)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+162
-153
@@ -18,29 +18,32 @@ 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):
|
elif _root == f"{home}/.local/share/sshyp/" and len(_files) > 0:
|
||||||
|
print("\n\n\u001b[38;5;7;48;5;8m/\u001b[0m")
|
||||||
|
_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
|
||||||
@@ -49,29 +52,29 @@ def entry_reader(_decrypted_entry):
|
|||||||
if pass_show:
|
if pass_show:
|
||||||
_entry_password = f'\u001b[38;5;10m{_decrypted_entry[0]}\u001b[0m'
|
_entry_password = f'\u001b[38;5;10m{_decrypted_entry[0]}\u001b[0m'
|
||||||
else:
|
else:
|
||||||
_entry_password = f'\u001b[38;5;3mend command in "--show" or "-s" to view\u001b[0m'
|
_entry_password = '\u001b[38;5;3mend command in "--show" or "-s" to view\u001b[0m'
|
||||||
print()
|
print()
|
||||||
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]
|
_ = _decrypted_entry[_num + 1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
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 +84,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
|
||||||
@@ -123,7 +126,11 @@ def edit_note(_note_lines, _exit_on_match=False):
|
|||||||
with NamedTemporaryFile(mode='w+') as _tmp:
|
with NamedTemporaryFile(mode='w+') as _tmp:
|
||||||
_tmp.write(_joined_note_lines)
|
_tmp.write(_joined_note_lines)
|
||||||
_tmp.seek(0)
|
_tmp.seek(0)
|
||||||
run((editor, _tmp.name))
|
try:
|
||||||
|
run((editor, _tmp.name))
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"\n\u001b[38;5;9merror: the configured text editor ({editor}) cannot be found on this system\n\n"
|
||||||
|
f"please either install the editor or re-configure the active editor using 'sshyp tweak'\u001b[0m\n")
|
||||||
_tmp.seek(0)
|
_tmp.seek(0)
|
||||||
_new_note = _tmp.read().rstrip()
|
_new_note = _tmp.read().rstrip()
|
||||||
if _exit_on_match and _joined_note_lines == _new_note:
|
if _exit_on_match and _joined_note_lines == _new_note:
|
||||||
@@ -131,20 +138,18 @@ def edit_note(_note_lines, _exit_on_match=False):
|
|||||||
return _new_note
|
return _new_note
|
||||||
|
|
||||||
|
|
||||||
# encrypts an entry and cleans up the temporary files
|
# encrypts an entry
|
||||||
def encrypt(_entry_data, _entry_dir, _gpg_id):
|
def encrypt(_entry_data, _entry_dir, _gpg_id):
|
||||||
_bytes_data = '\n'.join(_entry_data).rstrip().encode()
|
_bytes_data = '\n'.join(_entry_data).rstrip().encode()
|
||||||
_encrypted_data = run(('gpg', '-qr', str(_gpg_id), '-e'), input=_bytes_data, stdout=PIPE).stdout
|
_encrypted_data = run(('gpg', '-qr', str(_gpg_id), '-e'), input=_bytes_data, stdout=PIPE).stdout
|
||||||
open(_entry_dir + '.gpg', 'wb').write(_encrypted_data)
|
open(_entry_dir + '.gpg', 'wb').write(_encrypted_data)
|
||||||
|
|
||||||
|
|
||||||
# decrypts an entry to a temporary directory
|
# decrypts an entry and returns its contents
|
||||||
def decrypt(_entry_dir, _quick_verify=None, _quick_pass=None):
|
def decrypt(_entry_dir, _quick_verify=None, _quick_pass=None):
|
||||||
_contents = None
|
|
||||||
|
|
||||||
# check quick-unlock status, fetch passphrase
|
# check quick-unlock status, fetch passphrase
|
||||||
if _quick_verify == 'true':
|
if _quick_verify:
|
||||||
_quick_pass = whitelist_verify(port, username_ssh, ip, client_device_id)
|
_quick_pass = whitelist_verify(port, username_ssh, ip, client_device_id, identity)
|
||||||
else:
|
else:
|
||||||
if _quick_pass is None:
|
if _quick_pass is None:
|
||||||
_quick_pass = False
|
_quick_pass = False
|
||||||
@@ -180,22 +185,23 @@ def decrypt(_entry_dir, _quick_verify=None, _quick_pass=None):
|
|||||||
|
|
||||||
|
|
||||||
# checks the user's whitelist status and fetches the full gpg key password if possible
|
# checks the user's whitelist status and fetches the full gpg key password if possible
|
||||||
def whitelist_verify(_port, _username_ssh, _ip, _client_device_id):
|
def whitelist_verify(_port, _username_ssh, _ip, _client_device_id, _identity):
|
||||||
try:
|
try:
|
||||||
run(('gpg', '--pinentry-mode', 'cancel', '-qd', '--output', '/dev/null',
|
run(('gpg', '--pinentry-mode', 'cancel', '-qd', '--output', '/dev/null',
|
||||||
f"{home}/.config/sshyp/lock.gpg"), stderr=DEVNULL, check=True)
|
f"{home}/.config/sshyp/lock.gpg"), stderr=DEVNULL, check=True)
|
||||||
return False
|
return False
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
_i, _full_password = 0, ''
|
_i, _full_password = 0, ''
|
||||||
_server_whitelist = run(('ssh', '-i', f"{home}/.ssh/sshyp", '-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', f"{home}/.ssh/sshyp", '-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):
|
||||||
@@ -245,33 +251,34 @@ def target_type_check(_target_name, _expected_type=True, _error=False):
|
|||||||
s_exit(2)
|
s_exit(2)
|
||||||
|
|
||||||
|
|
||||||
# ensures an edited entry is optimized for best compatibility
|
# ensures an entry has enough lines to complete an edit and optionally carries out the edit
|
||||||
def line_edit(_lines, _edit_data, _edit_line):
|
def line_edit(_lines, _edit_line, _edit_data=None):
|
||||||
# ensure enough lines are present for edited field
|
# ensure enough lines are present for edited field
|
||||||
while len(_lines) < _edit_line + 1:
|
while len(_lines) < _edit_line + 1:
|
||||||
_lines.append('')
|
_lines.append('')
|
||||||
# write the edited field
|
if _edit_data is not None:
|
||||||
_lines[_edit_line] = _edit_data.rstrip()
|
# write the edited field
|
||||||
|
_lines[_edit_line] = _edit_data.rstrip()
|
||||||
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, _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'):
|
||||||
_sshyp_data.add_section('CLIENT-ONLINE')
|
_sshyp_data.add_section('CLIENT-ONLINE')
|
||||||
try:
|
try:
|
||||||
run(('ssh', '-o', 'ConnectTimeout=3', '-i', f"{home}/.ssh/sshyp", '-p', _port, f"{_username_ssh}@{_ip}",
|
run(('ssh', '-o', 'ConnectTimeout=3', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
|
||||||
f'python3 -c \'from pathlib import Path; Path("/home/{_username_ssh}/.config/sshyp/devices/'
|
f'python3 -c \'from pathlib import Path; Path("/home/{_username_ssh}/.config/sshyp/devices/'
|
||||||
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('\n\u001b[38;5;9mwarning: ssh connection could not be made - ensure the public key (~/.ssh/sshyp.pub) 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', '1')
|
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'true')
|
||||||
write_config(_sshyp_data)
|
write_config(_sshyp_data)
|
||||||
return True
|
return True
|
||||||
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '0')
|
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'false')
|
||||||
write_config(_sshyp_data)
|
write_config(_sshyp_data)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -281,101 +288,101 @@ def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _sshyp_data):
|
|||||||
# 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.0\u001b[38;5;15;48;5;15m{18*' '}\u001b[38;5;7;48;5;8m/\u001b[0m")
|
f"version 1.5.3\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
|
||||||
|
|
||||||
|
|
||||||
@@ -387,7 +394,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():
|
||||||
@@ -416,7 +423,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -424,7 +431,7 @@ def add_entry():
|
|||||||
def add_folder():
|
def add_folder():
|
||||||
Path(directory + entry_name).mkdir(mode=0o700, parents=True, exist_ok=True)
|
Path(directory + entry_name).mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
if not ssh_error:
|
if not ssh_error:
|
||||||
run(('ssh', '-i', f"{home}/.ssh/sshyp", '-p', port, f"{username_ssh}@{ip}",
|
run(('ssh', '-i', identity, '-p', port, f"{username_ssh}@{ip}",
|
||||||
f'python3 -c \'from pathlib import Path; Path("{directory_ssh}{entry_name}")'
|
f'python3 -c \'from pathlib import Path; Path("{directory_ssh}{entry_name}")'
|
||||||
f'.mkdir(mode=0o700, parents=True, exist_ok=True)\''))
|
f'.mkdir(mode=0o700, parents=True, exist_ok=True)\''))
|
||||||
|
|
||||||
@@ -453,21 +460,18 @@ def rename():
|
|||||||
# if renaming a folder
|
# if renaming a folder
|
||||||
if not ssh_error:
|
if not ssh_error:
|
||||||
Path(f"{directory}{_new_name}").mkdir(mode=0o700, parents=True, exist_ok=True)
|
Path(f"{directory}{_new_name}").mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
run(('ssh', '-i', f"{home}/.ssh/sshyp", '-p', port, f"{username_ssh}@{ip}",
|
run(('ssh', '-i', identity, '-p', port, f"{username_ssh}@{ip}",
|
||||||
f'python3 -c \'from pathlib import Path; Path("{directory_ssh}{entry_name}")'
|
f'python3 -c \'from pathlib import Path; Path("{directory_ssh}{entry_name}")'
|
||||||
f'.rename(Path("{directory_ssh}{_new_name}"))\''))
|
f'.rename(Path("{directory_ssh}{_new_name}"))\''))
|
||||||
else:
|
else:
|
||||||
move(f"{directory}{entry_name}", f"{directory}{_new_name}")
|
move(f"{directory}{entry_name}", f"{directory}{_new_name}")
|
||||||
if not ssh_error:
|
if not ssh_error:
|
||||||
run(('ssh', '-i', f"{home}/.ssh/sshyp", '-p', port, f"{username_ssh}@{ip}",
|
run(('ssh', '-i', identity, '-p', port, f"{username_ssh}@{ip}",
|
||||||
f'cd /usr/lib/sshyp; python3 -c \'from sshync import delete; delete("{entry_name}", "remotely", True)\''))
|
f'cd /usr/lib/sshyp; python3 -c \'from sshync import delete; delete("{entry_name}", "remotely", True)\''))
|
||||||
|
|
||||||
|
|
||||||
# edits the contents of an entry
|
# edits the contents of an entry
|
||||||
def edit():
|
def edit():
|
||||||
# set to avoid PEP8 warnings
|
|
||||||
_detail, _edit_line = None, None
|
|
||||||
|
|
||||||
# ensure the edit target is an entry
|
# ensure the edit target is an entry
|
||||||
target_type_check(entry_name, True, True)
|
target_type_check(entry_name, True, True)
|
||||||
|
|
||||||
@@ -480,10 +484,10 @@ def edit():
|
|||||||
_detail, _edit_line = str(input('\nurl: ')), 2
|
_detail, _edit_line = str(input('\nurl: ')), 2
|
||||||
if arguments[2] in ('note', '-n'):
|
if arguments[2] in ('note', '-n'):
|
||||||
_old_lines = decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled)
|
_old_lines = decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled)
|
||||||
_new_lines = _old_lines[0:3] + edit_note(_old_lines[3:], True).split('\n')
|
# pass 2 as _edit_line to simulate a full entry minus notes so that notes are appended correctly
|
||||||
|
_new_lines = line_edit(_old_lines[0:3], 2) + edit_note(_old_lines[3:], True).split('\n')
|
||||||
else:
|
else:
|
||||||
_new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), _detail,
|
_new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), _edit_line, _detail)
|
||||||
_edit_line)
|
|
||||||
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)
|
||||||
@@ -491,13 +495,11 @@ def edit():
|
|||||||
|
|
||||||
# generates a password for a new or an existing entry
|
# generates a password for a new or an existing entry
|
||||||
def gen():
|
def gen():
|
||||||
# set to avoid PEP8 warnings
|
|
||||||
_username, _url, _notes = None, None, None
|
|
||||||
# gen update
|
# gen update
|
||||||
if arg_count == 3 and arguments[2] in ('update', '-u'):
|
if arg_count == 3 and arguments[2] in ('update', '-u'):
|
||||||
# ensure the gen update target is an entry
|
# ensure the gen update target is an entry
|
||||||
target_type_check(entry_name, True, True)
|
target_type_check(entry_name, True, True)
|
||||||
_new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), pass_gen(), 0)
|
_new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), 0, pass_gen())
|
||||||
# gen
|
# gen
|
||||||
else:
|
else:
|
||||||
# make sure the gen target does not already exist
|
# make sure the gen target does not already exist
|
||||||
@@ -509,7 +511,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)
|
||||||
@@ -517,6 +519,7 @@ def gen():
|
|||||||
|
|
||||||
# copies a specified field of an entry to the clipboard
|
# copies a specified field of an entry to the clipboard
|
||||||
def copy_data():
|
def copy_data():
|
||||||
|
from hashlib import sha512
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
# ensure the copy target is an entry
|
# ensure the copy target is an entry
|
||||||
target_type_check(entry_name, True, True)
|
target_type_check(entry_name, True, True)
|
||||||
@@ -533,33 +536,38 @@ def copy_data():
|
|||||||
# ensure field is not blank
|
# ensure field is not blank
|
||||||
if _copy_subject == '':
|
if _copy_subject == '':
|
||||||
raise IndexError
|
raise IndexError
|
||||||
|
|
||||||
|
# store hashed _copy_subject for later comparison
|
||||||
|
_hash = sha512()
|
||||||
|
_hash.update(_copy_subject.encode('utf-8'))
|
||||||
|
|
||||||
# PORT START CLIPBOARD
|
# PORT START CLIPBOARD
|
||||||
# WSL clipboard detection
|
# WSL clipboard detection
|
||||||
if 'WSL_DISTRO_NAME' in environ:
|
if 'WSL_DISTRO_NAME' in environ:
|
||||||
run(('powershell.exe', '-c', "Set-Clipboard '" + _copy_subject.replace("'", "''") + "'"))
|
run(('powershell.exe', '-c', "Set-Clipboard '" + _copy_subject.replace("'", "''") + "'"))
|
||||||
Popen("sleep 30; powershell.exe -c Set-Clipboard ''", shell=True, stdout=DEVNULL, stderr=DEVNULL)
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'wsl'), stdout=DEVNULL,
|
||||||
|
stderr=DEVNULL)
|
||||||
# Wayland clipboard detection
|
# Wayland clipboard detection
|
||||||
elif 'WAYLAND_DISPLAY' in environ:
|
elif 'WAYLAND_DISPLAY' in environ:
|
||||||
run(('wl-copy', _copy_subject))
|
run('wl-copy', stdin=Popen(('printf', '%b', _copy_subject.replace('\\', '\\\\')), stdout=PIPE).stdout)
|
||||||
Popen('sleep 30; wl-copy -c', shell=True)
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'wayland'))
|
||||||
# Haiku clipboard detection
|
# Haiku clipboard detection
|
||||||
elif uname()[0] == 'Haiku':
|
elif uname()[0] == 'Haiku':
|
||||||
run(('clipboard', '-c', _copy_subject))
|
run(('clipboard', '-c', _copy_subject))
|
||||||
Popen('sleep 30; clipboard -r', shell=True)
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'haiku'))
|
||||||
# MacOS clipboard detection
|
# MacOS clipboard detection
|
||||||
elif uname()[0] == 'Darwin':
|
elif uname()[0] == 'Darwin':
|
||||||
run('pbcopy', stdin=Popen(('printf', _copy_subject.replace('\\', '\\\\').replace('%', '%%')), stdout=PIPE)
|
run('pbcopy', stdin=Popen(('printf', '%b', _copy_subject.replace('\\', '\\\\')), stdout=PIPE).stdout)
|
||||||
.stdout)
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'mac'))
|
||||||
Popen("sleep 30; printf '' | pbcopy", shell=True)
|
|
||||||
# Termux (Android) clipboard detection
|
# Termux (Android) clipboard detection
|
||||||
elif isdir("/data/data/com.termux"):
|
elif isdir("/data/data/com.termux"):
|
||||||
run(('termux-clipboard-set', _copy_subject))
|
run(('termux-clipboard-set', _copy_subject))
|
||||||
Popen("sleep 30; termux-clipboard-set ''", shell=True)
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'termux'))
|
||||||
# X11 clipboard detection
|
# X11 clipboard detection
|
||||||
elif 'DISPLAY' in environ:
|
elif 'DISPLAY' in environ:
|
||||||
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', _copy_subject.replace('\\', '\\\\')
|
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', '%b', _copy_subject.replace('\\', '\\\\')), stdout=PIPE)
|
||||||
.replace('%', '%%')), stdout=PIPE).stdout)
|
.stdout)
|
||||||
Popen("sleep 30; printf '' | xclip -sel c", shell=True)
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'x11'))
|
||||||
else:
|
else:
|
||||||
print('\n\u001b[38;5;9merror: clipboard tool could not be determined\n\nnote that the clipboard does not '
|
print('\n\u001b[38;5;9merror: clipboard tool could not be determined\n\nnote that the clipboard does not '
|
||||||
'function in a raw tty\u001b[0m\n')
|
'function in a raw tty\u001b[0m\n')
|
||||||
@@ -570,7 +578,7 @@ def copy_data():
|
|||||||
def remove_data():
|
def remove_data():
|
||||||
decrypt(None, _quick_verify=quick_unlock_enabled)
|
decrypt(None, _quick_verify=quick_unlock_enabled)
|
||||||
if not ssh_error:
|
if not ssh_error:
|
||||||
run(('ssh', '-i', f"{home}/.ssh/sshyp", '-p', port, f"{username_ssh}@{ip}",
|
run(('ssh', '-i', identity, '-p', port, f"{username_ssh}@{ip}",
|
||||||
f'cd /usr/lib/sshyp; python3 -c \'from sshync import delete; delete("{entry_name}", "remotely", True)\''))
|
f'cd /usr/lib/sshyp; python3 -c \'from sshync import delete; delete("{entry_name}", "remotely", True)\''))
|
||||||
else:
|
else:
|
||||||
offline_delete(entry_name, 'locally', silent_sync)
|
offline_delete(entry_name, 'locally', silent_sync)
|
||||||
@@ -581,7 +589,7 @@ def extension_runner():
|
|||||||
_output_com, _extension_dir = None, realpath(__file__).rsplit('/', 1)[0] + '/extensions/'
|
_output_com, _extension_dir = None, realpath(__file__).rsplit('/', 1)[0] + '/extensions/'
|
||||||
if isdir(_extension_dir):
|
if isdir(_extension_dir):
|
||||||
for _extension in listdir(_extension_dir):
|
for _extension in listdir(_extension_dir):
|
||||||
_extension_config = ConfigParser()
|
_extension_config = ConfigParser(interpolation=None)
|
||||||
_extension_config.read(_extension_dir + _extension)
|
_extension_config.read(_extension_dir + _extension)
|
||||||
_input_com = _extension_config.get('config', 'input').split()
|
_input_com = _extension_config.get('config', 'input').split()
|
||||||
if _input_com == arguments[arg_start:]:
|
if _input_com == arguments[arg_start:]:
|
||||||
@@ -602,9 +610,6 @@ if __name__ == "__main__":
|
|||||||
# set default states
|
# set default states
|
||||||
ssh_error, success_flag, sync_flag, silent_sync, pass_show = False, False, False, False, False
|
ssh_error, success_flag, sync_flag, silent_sync, pass_show = False, False, False, False, False
|
||||||
|
|
||||||
# set to avoid PEP8 warnings
|
|
||||||
arg_start, device_type = None, None
|
|
||||||
|
|
||||||
# retrieve typed argument
|
# retrieve typed argument
|
||||||
arguments = argv[1:]
|
arguments = argv[1:]
|
||||||
arg_count = len(arguments)
|
arg_count = len(arguments)
|
||||||
@@ -615,8 +620,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:
|
||||||
@@ -624,35 +629,36 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# import saved userdata
|
# import saved userdata
|
||||||
try:
|
try:
|
||||||
sshyp_data = ConfigParser()
|
sshyp_data = ConfigParser(interpolation=None)
|
||||||
sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini")
|
sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini")
|
||||||
device_type = sshyp_data.get('GENERAL', 'device_type')
|
device_type = sshyp_data.get('GENERAL', 'device_type')
|
||||||
if device_type == 'client':
|
if device_type == 'client':
|
||||||
directory = f"{home}/.local/share/sshyp/"
|
directory = f"{home}/.local/share/sshyp/"
|
||||||
gpg_id = sshyp_data.get('CLIENT-GENERAL', 'gpg_id')
|
gpg_id = sshyp_data.get('CLIENT-GENERAL', 'gpg_id')
|
||||||
editor = sshyp_data.get('CLIENT-GENERAL', 'text_editor')
|
editor = sshyp_data.get('CLIENT-GENERAL', 'text_editor')
|
||||||
offline_mode_enabled = sshyp_data.get('CLIENT-GENERAL', 'offline_mode_enabled')
|
offline_mode_enabled = sshyp_data.getboolean('CLIENT-GENERAL', 'offline_mode_enabled')
|
||||||
if offline_mode_enabled == 'true':
|
if offline_mode_enabled:
|
||||||
ssh_error = True
|
ssh_error = True
|
||||||
quick_unlock_enabled = 'false'
|
quick_unlock_enabled = False
|
||||||
else:
|
else:
|
||||||
quick_unlock_enabled = sshyp_data.get('CLIENT-ONLINE', 'quick_unlock_enabled')
|
quick_unlock_enabled = sshyp_data.getboolean('CLIENT-ONLINE', 'quick_unlock_enabled')
|
||||||
username_ssh = sshyp_data.get('SSHYNC', 'user')
|
username_ssh = sshyp_data.get('SSHYNC', 'user')
|
||||||
ip = sshyp_data.get('SSHYNC', 'ip')
|
ip = sshyp_data.get('SSHYNC', 'ip')
|
||||||
port = sshyp_data.get('SSHYNC', 'port')
|
port = sshyp_data.get('SSHYNC', 'port')
|
||||||
directory_ssh = sshyp_data.get('SSHYNC', 'remote_dir')
|
directory_ssh = sshyp_data.get('SSHYNC', 'remote_dir')
|
||||||
|
identity = sshyp_data.get('SSHYNC', 'identity_file')
|
||||||
client_device_id = listdir(f"{home}/.config/sshyp/devices")[0]
|
client_device_id = listdir(f"{home}/.config/sshyp/devices")[0]
|
||||||
ssh_error = int(sshyp_data.get('CLIENT-ONLINE', 'ssh_error'))
|
ssh_error = sshyp_data.getboolean('CLIENT-ONLINE', 'ssh_error')
|
||||||
if ssh_error == 1:
|
if ssh_error:
|
||||||
ssh_error = copy_id_check(port, username_ssh, ip, client_device_id, 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
|
||||||
@@ -714,6 +720,9 @@ if __name__ == "__main__":
|
|||||||
sync()
|
sync()
|
||||||
elif (arg_count > 0 and arguments[0] == 'sync') or (arg_count > 1 and arguments[1] == 'shear'):
|
elif (arg_count > 0 and arguments[0] == 'sync') or (arg_count > 1 and arguments[1] == 'shear'):
|
||||||
sync('\n')
|
sync('\n')
|
||||||
|
elif arg_count > 0 and arguments[0] == 'sync' and offline_mode_enabled:
|
||||||
|
print("\n\u001b[38;5;9mwarning: sshyp is currently configured in offline mode - ssh synchronization is "
|
||||||
|
"disabled\u001b[0m\n")
|
||||||
|
|
||||||
# PORT START ARGS-SERVER
|
# PORT START ARGS-SERVER
|
||||||
# server arguments
|
# server arguments
|
||||||
|
|||||||
+244
-124
@@ -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(), 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
|
||||||
@@ -99,16 +136,25 @@ def gpg_config():
|
|||||||
stdout=PIPE, text=True).stdout.splitlines() if _item.startswith('uid')]
|
stdout=PIPE, text=True).stdout.splitlines() if _item.startswith('uid')]
|
||||||
_named_uid_list = []
|
_named_uid_list = []
|
||||||
for _uid in _uid_list:
|
for _uid in _uid_list:
|
||||||
_named_uid_list.append(_uid.split(':')[9])
|
_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,24 +191,30 @@ 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
|
||||||
Path(f"{home}/.ssh").mkdir(mode=0o700, exist_ok=True)
|
Path(f"{home}/.ssh").mkdir(mode=0o700, exist_ok=True)
|
||||||
for _file in listdir(f"{home}/.ssh"):
|
for _file in listdir(f"{home}/.ssh"):
|
||||||
if not _file.startswith('.') and _file not in ('known_hosts', 'authorized_keys') \
|
if not _file.startswith('.') and _file not in ('known_hosts', 'known_hosts.old', 'authorized_keys') \
|
||||||
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]
|
||||||
@@ -184,23 +236,45 @@ def ssh_config():
|
|||||||
sshyp_data.set('SSHYNC', 'remote_dir', f"/home/{_username_ssh}/.local/share/sshyp/")
|
sshyp_data.set('SSHYNC', 'remote_dir', f"/home/{_username_ssh}/.local/share/sshyp/")
|
||||||
sshyp_data.set('SSHYNC', 'identity_file', _ssh_key)
|
sshyp_data.set('SSHYNC', 'identity_file', _ssh_key)
|
||||||
write_config()
|
write_config()
|
||||||
return _iport[1], _username_ssh, _iport[0]
|
return _iport[1], _username_ssh, _iport[0], _ssh_key
|
||||||
|
|
||||||
|
|
||||||
# device id configuration
|
# device id configuration
|
||||||
def dev_id_config(_ip, _username_ssh, _port):
|
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, 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,23 +459,27 @@ 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.0").read()
|
# the version listed below will NOT always match the version of sshyp being used
|
||||||
_pointer = ConfigParser()
|
# 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.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
|
||||||
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
|
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
|
||||||
_ext_exe = urlretrieve(_pointer.get(_selected, 'exe'), _exe_dir)
|
urlretrieve(_pointer.get(_selected, 'exe'), _exe_dir)
|
||||||
_ext_ini = urlretrieve(_pointer.get(_selected, 'ini'), _ini_dir)
|
urlretrieve(_pointer.get(_selected, 'ini'), _ini_dir)
|
||||||
# set permissions under active user
|
# set permissions under active user
|
||||||
chmod(_exe_dir, 0o755)
|
chmod(_exe_dir, 0o755)
|
||||||
chmod(_ini_dir, 0o644)
|
chmod(_ini_dir, 0o644)
|
||||||
@@ -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,21 +561,21 @@ 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'):
|
||||||
quick_unlock_config(True)
|
quick_unlock_config(True)
|
||||||
# set to None to check if modified later
|
# set to None to check if modified later
|
||||||
_ip, _username_ssh, _port = None, None, None
|
_ip, _username_ssh, _port, _identity = None, None, None, None
|
||||||
# ...and there is no sshync config present
|
# ...and there is no sshync config present
|
||||||
if not sshyp_data.has_section('SSHYNC'):
|
if not sshyp_data.has_section('SSHYNC'):
|
||||||
_ip, _username_ssh, _port = ssh_config()
|
_ip, _username_ssh, _port, _identity = ssh_config()
|
||||||
# ...and there is no device ID present
|
# ...and there is no device ID present
|
||||||
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 = ssh_config()
|
_ip, _username_ssh, _port, _identity = ssh_config()
|
||||||
dev_id_config(_ip, _username_ssh, _port)
|
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'))
|
|
||||||
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 = ssh_config()
|
_ip, _username_ssh, _port, _identity = ssh_config()
|
||||||
|
|
||||||
# device id configuration
|
# device id configuration
|
||||||
dev_id_config(_ip, _username_ssh, _port)
|
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
|
||||||
@@ -576,39 +681,54 @@ def initial_setup(_scr):
|
|||||||
# PORT END CLIPTOOL
|
# PORT END CLIPTOOL
|
||||||
|
|
||||||
# run optional configuration menu
|
# run optional configuration menu
|
||||||
wrapped_entry(_dev_sync_types[0], 'additional configuration options:')
|
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')
|
||||||
|
|
||||||
|
# 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))
|
# PORT START TWEAK-EXT-CHOWN
|
||||||
run((_escalator, 'mv', _exe_dir, f"/usr/lib/sshyp/{_ext_name}"))
|
if uname()[0] == 'FreeBSD':
|
||||||
run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
|
run((_escalator, 'chown', 'root:wheel', _exe_dir, _ini_dir))
|
||||||
else:
|
else:
|
||||||
# uninstall with privilege escalation (outside of curses)
|
run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir))
|
||||||
run((_escalator, 'rm', '-I', f"/usr/lib/sshyp/{_ext_name}",
|
# PORT END TWEAK-EXT-CHOWN
|
||||||
f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
|
run((_escalator, 'mv', _exe_dir, f"/usr/lib/sshyp/{_ext_name}"))
|
||||||
|
run((_escalator, 'mv', _ini_dir, 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
|
||||||
|
|||||||
+21
-7
@@ -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
|
||||||
@@ -21,6 +21,7 @@ _create_generic_linux() {
|
|||||||
cd port-jobs
|
cd port-jobs
|
||||||
./CLIPTOOL.py LINUX
|
./CLIPTOOL.py LINUX
|
||||||
./CLIPBOARD.py LINUX
|
./CLIPBOARD.py LINUX
|
||||||
|
./CHOWN.py
|
||||||
./UNAME.py LINUX
|
./UNAME.py LINUX
|
||||||
./COMMENTS.py ALL
|
./COMMENTS.py ALL
|
||||||
./BLANKS.py
|
./BLANKS.py
|
||||||
@@ -135,7 +136,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"
|
||||||
@@ -144,7 +145,7 @@ provides {
|
|||||||
requires {
|
requires {
|
||||||
gnupg
|
gnupg
|
||||||
openssh
|
openssh
|
||||||
python310
|
python3.11
|
||||||
}
|
}
|
||||||
urls {
|
urls {
|
||||||
\"https://github.com/rwinkhart/sshyp\"
|
\"https://github.com/rwinkhart/sshyp\"
|
||||||
@@ -158,6 +159,7 @@ urls {
|
|||||||
./RMEXTMAN.py
|
./RMEXTMAN.py
|
||||||
./CLIPTOOL.py
|
./CLIPTOOL.py
|
||||||
./CLIPBOARD.py HAIKU
|
./CLIPBOARD.py HAIKU
|
||||||
|
./CHOWN.py
|
||||||
./UNAME.py TMP
|
./UNAME.py TMP
|
||||||
./COMMENTS.py ALL
|
./COMMENTS.py ALL
|
||||||
./BLANKS.py
|
./BLANKS.py
|
||||||
@@ -200,6 +202,7 @@ _create_deb() {
|
|||||||
./CLIPBOARD.py WSL
|
./CLIPBOARD.py WSL
|
||||||
special=WSL-ONLY-DEBIAN
|
special=WSL-ONLY-DEBIAN
|
||||||
fi
|
fi
|
||||||
|
./CHOWN.py
|
||||||
./UNAME.py LINUX
|
./UNAME.py LINUX
|
||||||
./COMMENTS.py ALL
|
./COMMENTS.py ALL
|
||||||
./BLANKS.py
|
./BLANKS.py
|
||||||
@@ -220,11 +223,17 @@ Section: utils
|
|||||||
Architecture: all
|
Architecture: all
|
||||||
Maintainer: Randall Winkhart <idgr at tutanota dot com>
|
Maintainer: Randall Winkhart <idgr at tutanota dot com>
|
||||||
Description: A light-weight, self-hosted, synchronized password manager
|
Description: A light-weight, self-hosted, synchronized password manager
|
||||||
Depends: python3, gnupg, openssh-client
|
|
||||||
Suggests: wl-clipboard, xclip, bash-completion
|
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Installed-Size: $size
|
Installed-Size: $size
|
||||||
|
Depends: python3, gnupg, openssh-client
|
||||||
" > output/debiantemp/sshyp_"$version"-"$revision"_all/DEBIAN/control
|
" > output/debiantemp/sshyp_"$version"-"$revision"_all/DEBIAN/control
|
||||||
|
if [ "$1" = 'Debian' ]; then
|
||||||
|
printf "Suggests: wl-clipboard, xclip, bash-completion, openssh-server
|
||||||
|
" >> output/debiantemp/sshyp_"$version"-"$revision"_all/DEBIAN/control
|
||||||
|
else
|
||||||
|
printf "Suggests: bash-completion, openssh-server
|
||||||
|
" >> output/debiantemp/sshyp_"$version"-"$revision"_all/DEBIAN/control
|
||||||
|
fi
|
||||||
dpkg-deb --build --root-owner-group -z6 -Sextreme -Zxz output/debiantemp/sshyp_"$version"-"$revision"_all/
|
dpkg-deb --build --root-owner-group -z6 -Sextreme -Zxz output/debiantemp/sshyp_"$version"-"$revision"_all/
|
||||||
mv output/debiantemp/sshyp_"$version"-"$revision"_all.deb output/"$special"-sshyp_"$version"-"$revision"_all.deb
|
mv output/debiantemp/sshyp_"$version"-"$revision"_all.deb output/"$special"-sshyp_"$version"-"$revision"_all.deb
|
||||||
rm -rf output/debiantemp
|
rm -rf output/debiantemp
|
||||||
@@ -247,6 +256,7 @@ _create_termux() {
|
|||||||
./RMEXTMAN.py
|
./RMEXTMAN.py
|
||||||
./CLIPTOOL.py
|
./CLIPTOOL.py
|
||||||
./CLIPBOARD.py TERMUX
|
./CLIPBOARD.py TERMUX
|
||||||
|
./CHOWN.py
|
||||||
./UNAME.py TERMUX
|
./UNAME.py TERMUX
|
||||||
./COMMENTS.py ALL
|
./COMMENTS.py ALL
|
||||||
./BLANKS.py
|
./BLANKS.py
|
||||||
@@ -296,7 +306,7 @@ License: GPL-3.0-only
|
|||||||
URL: https://github.com/rwinkhart/sshyp
|
URL: https://github.com/rwinkhart/sshyp
|
||||||
Source0: GENERIC-FEDORA-sshyp-"$version".tar.xz
|
Source0: GENERIC-FEDORA-sshyp-"$version".tar.xz
|
||||||
Requires: python gnupg openssh-clients
|
Requires: python gnupg openssh-clients
|
||||||
Recommends: wl-clipboard xclip bash-completion
|
Recommends: wl-clipboard xclip bash-completion openssh-server
|
||||||
%%description
|
%%description
|
||||||
sshyp is a password-store compatible CLI password manager available for UNIX(-like) systems - its primary goal is to make syncing passwords and notes across devices as easy as possible via CLI.
|
sshyp is a password-store compatible CLI password manager available for UNIX(-like) systems - its primary goal is to make syncing passwords and notes across devices as easy as possible via CLI.
|
||||||
%%install
|
%%install
|
||||||
@@ -307,6 +317,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
|
||||||
@@ -319,6 +330,7 @@ cp -r %%{_sourcedir}/usr %%{buildroot}
|
|||||||
cd port-jobs
|
cd port-jobs
|
||||||
./CLIPTOOL.py LINUX
|
./CLIPTOOL.py LINUX
|
||||||
./CLIPBOARD.py LINUX
|
./CLIPBOARD.py LINUX
|
||||||
|
./CHOWN.py
|
||||||
./UNAME.py LINUX
|
./UNAME.py LINUX
|
||||||
./COMMENTS.py ALL
|
./COMMENTS.py ALL
|
||||||
./BLANKS.py
|
./BLANKS.py
|
||||||
@@ -371,6 +383,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
|
||||||
@@ -381,7 +394,8 @@ printf "/usr/bin/sshyp
|
|||||||
cp lib/* port-jobs/working/
|
cp lib/* port-jobs/working/
|
||||||
cd port-jobs
|
cd port-jobs
|
||||||
./CLIPTOOL.py LINUX
|
./CLIPTOOL.py LINUX
|
||||||
./CLIPBOARD.py LINUX
|
./CLIPBOARD.py BSD
|
||||||
|
./CHOWN.py BSD
|
||||||
./UNAME.py TMP
|
./UNAME.py TMP
|
||||||
./COMMENTS.py ALL
|
./COMMENTS.py ALL
|
||||||
./BLANKS.py
|
./BLANKS.py
|
||||||
|
|||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
from sys import argv
|
||||||
|
|
||||||
|
# read arguments
|
||||||
|
arguments = argv[1:]
|
||||||
|
|
||||||
|
# define PORT target
|
||||||
|
string1 = '# PORT START TWEAK-EXT-CHOWN'
|
||||||
|
string2 = '# PORT END TWEAK-EXT-CHOWN'
|
||||||
|
|
||||||
|
# define replacement text depending on arguments
|
||||||
|
if len(arguments) > 0 and arguments[0] == 'BSD':
|
||||||
|
replacement = "run((_escalator, 'chown', 'root:wheel', _exe_dir, _ini_dir))"
|
||||||
|
else:
|
||||||
|
replacement = "run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir))"
|
||||||
|
|
||||||
|
# read input file
|
||||||
|
text = open('working/stweak.py', 'r').read()
|
||||||
|
|
||||||
|
# compile regex and modify text
|
||||||
|
regex = re.compile(f"{string1}.*?{string2}", re.DOTALL)
|
||||||
|
|
||||||
|
# find and replace the defined PORT target
|
||||||
|
new_text = re.sub(regex, replacement, text)
|
||||||
|
|
||||||
|
# write updated text
|
||||||
|
open('working/stweak.py', 'w').write(new_text)
|
||||||
+45
-29
@@ -3,44 +3,60 @@ import re
|
|||||||
from sys import argv, exit as s_exit
|
from sys import argv, exit as s_exit
|
||||||
|
|
||||||
# read arguments
|
# read arguments
|
||||||
arguments, replacement = argv[1:], None
|
arguments, clip_replacement, clear_replacement = argv[1:], None, None
|
||||||
|
|
||||||
# define replacement text depending on arguments
|
# define replacement text depending on arguments
|
||||||
if len(arguments) > 0:
|
if len(arguments) > 0:
|
||||||
if arguments[0] == 'WSL':
|
if arguments[0] == 'WSL':
|
||||||
replacement = """run(('powershell.exe', '-c', "Set-Clipboard '" + _copy_subject.replace("'", "''") + "'"))
|
clip_replacement = """run(('powershell.exe', '-c', "Set-Clipboard '" + _copy_subject.replace("'", "''") + "'"))
|
||||||
Popen("sleep 30; powershell.exe -c Set-Clipboard ''", shell=True, stdout=DEVNULL, stderr=DEVNULL)"""
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'wsl'), stdout=DEVNULL, stderr=DEVNULL)"""
|
||||||
|
clear_replacement = """hash_paste.update(run(('powershell.exe', '-c', 'Get-Clipboard'), stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(('powershell.exe', '-c', 'Set-Clipboard'))"""
|
||||||
elif arguments[0] == 'MAC':
|
elif arguments[0] == 'MAC':
|
||||||
replacement = """run('pbcopy', stdin=Popen(('printf', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')
|
clip_replacement = """run('pbcopy', stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout)
|
||||||
.replace('%', '%%')), stdout=PIPE).stdout)
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'mac'))"""
|
||||||
Popen("sleep 30; printf '' | pbcopy", shell=True)"""
|
clear_replacement = """hash_paste.update(run('pbpaste', stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run('pbcopy', input=b'')"""
|
||||||
elif arguments[0] == 'HAIKU':
|
elif arguments[0] == 'HAIKU':
|
||||||
replacement = """run(('clipboard', '-c', _copy_subject))
|
clip_replacement = """run(('clipboard', '-c', _copy_subject))
|
||||||
Popen('sleep 30; clipboard -r', shell=True)"""
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'haiku'))"""
|
||||||
|
clear_replacement = """hash_paste.update(run(('clipboard', '-p'), stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(('clipboard', '-r'))"""
|
||||||
elif arguments[0] == 'TERMUX':
|
elif arguments[0] == 'TERMUX':
|
||||||
replacement = """run(('termux-clipboard-set', _copy_subject))
|
clip_replacement = """run(('termux-clipboard-set', _copy_subject))
|
||||||
Popen("sleep 30; termux-clipboard-set ''", shell=True)"""
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'termux'))"""
|
||||||
elif arguments[0] == 'LINUX':
|
clear_replacement = """hash_paste.update(run('termux-clipboard-get', stdout=PIPE).stdout.strip())
|
||||||
replacement = """if 'WAYLAND_DISPLAY' in environ:
|
if argv[1] == hash_paste.hexdigest():
|
||||||
run(('wl-copy', _copy_subject))
|
run(("termux-clipboard-set", "''"))"""
|
||||||
Popen('sleep 30; wl-copy -c', shell=True)
|
elif arguments[0] in ('LINUX', 'BSD'):
|
||||||
|
clip_replacement = """if 'WAYLAND_DISPLAY' in environ:
|
||||||
|
run('wl-copy', stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout)
|
||||||
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'wayland'))
|
||||||
else:
|
else:
|
||||||
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', _copy_subject
|
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout)
|
||||||
.replace('\\\\\\', '\\\\\\\\\\\\\\').replace('%', '%%')), stdout=PIPE).stdout)
|
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'x11'))"""
|
||||||
Popen("sleep 30; printf '' | xclip -sel c", shell=True)"""
|
clear_replacement = """if argv[2] == 'wayland':
|
||||||
|
hash_paste.update(run('wl-paste', stdout=PIPE, stderr=DEVNULL).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(('wl-copy', '-c'))
|
||||||
|
else:
|
||||||
|
hash_paste.update(run(('xclip', '-o', '-sel', 'c'), stdout=PIPE).stdout.strip())
|
||||||
|
if argv[1] == hash_paste.hexdigest():
|
||||||
|
run(('xclip', '-i', '/dev/null', '-sel', 'c'))"""
|
||||||
else:
|
else:
|
||||||
s_exit()
|
s_exit()
|
||||||
|
|
||||||
# define PORT target
|
targets = (('CLIPBOARD', 'sshyp.py', '', '', clip_replacement),
|
||||||
string1 = '# PORT START CLIPBOARD'
|
('CLIPCLEAR', 'clipclear.py', '\n', '', clear_replacement))
|
||||||
string2 = '# PORT END CLIPBOARD'
|
|
||||||
|
|
||||||
# read input file
|
for target in targets:
|
||||||
text = open('working/sshyp.py', 'r').read()
|
# read text from target file
|
||||||
|
text = open(f"working/{target[1]}", 'r').read()
|
||||||
# find and replace the defined PORT target
|
# compile regex and modify text
|
||||||
regex = re.compile(f"{string1}.*?{string2}", re.DOTALL)
|
regex = re.compile(f"{target[2]}# PORT START {target[0]}.*?# PORT END {target[0]}{target[3]}", re.DOTALL)
|
||||||
new_text = re.sub(regex, replacement, text)
|
new_text = re.sub(regex, target[4], text)
|
||||||
|
# write updated text to target file
|
||||||
# write updated text
|
open(f"working/{target[1]}", 'w').write(new_text)
|
||||||
open('working/sshyp.py', 'w').write(new_text)
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import re
|
import re
|
||||||
|
|
||||||
devtype_replacement = """_install_type = curses_radio(('client (ssh-synchronized)', 'client (offline)'),
|
|
||||||
'device + sync type configuration')"""
|
|
||||||
|
|
||||||
targets = (('TWEAK-EXTEND-FUNCTIONS', 'stweak.py', '\n', '\n\n', ''),
|
targets = (('TWEAK-EXTEND-FUNCTIONS', 'stweak.py', '\n', '\n\n', ''),
|
||||||
('TWEAK-EXTEND-OPTION', 'stweak.py', '', '', "curses_radio(['okay'], "
|
('TWEAK-EXTEND-OPTION', 'stweak.py', '', '', "curses_radio(['okay'], "
|
||||||
"'extension management is not supported on this platform\\\\n\\\\ninstead, you may install and manage "
|
"'extension management is not supported on this platform\\\\n\\\\ninstead, you may install and manage "
|
||||||
"extensions through your system package manager\\\\n\\\\nofficial extension packages are available "
|
"extensions through your system package manager\\\\n\\\\nofficial extension packages are available "
|
||||||
"at https://github.com/rwinkhart/sshyp-labs/releases')"))
|
"at https://github.com/rwinkhart/sshyp-labs/releases')"))
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
# read text from target file
|
# read text from target file
|
||||||
text = open(f"working/{target[1]}", 'r').read()
|
text = open(f"working/{target[1]}", 'r').read()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
if [ "$1" = 'PREP' ]; then
|
if [ "$1" = 'PREP' ]; then
|
||||||
sed -i '1 s/.*/#!\/bin\/env\ python3.10/' ./*.py
|
sed -i '1 s/.*/#!\/bin\/env\ python3.11/' ./*.py
|
||||||
else
|
else
|
||||||
sed -i '1 s/.*/#!\/bin\/env\ python3.10/' ./working/sshyp.py
|
sed -i '1 s/.*/#!\/bin\/env\ python3.11/' ./working/sshyp.py ./working/clipclear.py
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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