Compare commits

..
87 Commits
Author SHA1 Message Date
RandyTheSilly 0bcf5fc0d1 Update documentation for release v1.5.3 2024-01-08 22:16:48 -05:00
RandyTheSilly ec3ee297b8 Remove unnecessary empty variable declarations (these were only included to silence PyCharm warnings) 2024-01-08 22:00:05 -05:00
RandyTheSilly 5f5ede7349 Fix notes sometimes being appended incorrectly when editing incomplete entries 2024-01-08 21:32:01 -05:00
RandyTheSilly cc3fa30865 Add CHOWN.py port-job to package.sh 2024-01-08 20:15:33 -05:00
RandyTheSilly 3613d3dd0c Fix installed extension ownership on FreeBSD 2024-01-08 20:11:26 -05:00
RandyTheSilly 249269a3e3 Address flake8 suggestions 2024-01-08 19:39:44 -05:00
RandyTheSilly c441802058 Fix root-level entries being displayed incorrectly in the entry list 2024-01-08 10:51:59 -05:00
RandyTheSilly 5439c899f7 Update documentation for release v1.5.2 - again 2024-01-07 20:51:41 -05:00
RandyTheSilly 0990bae310 Fix notes starting with blank lines differing in entry readout VS entry preview 2024-01-07 20:20:27 -05:00
RandyTheSilly ae21489c75 Optimize multi-line note readout 2024-01-07 20:00:57 -05:00
RandyTheSilly 5717133168 Do not connect to server from "sshyp init" menu, as the user may not yet have copied their ssh pubkey to the server 2024-01-07 19:42:44 -05:00
RandyTheSilly 07c0dee0a4 Fix quick-unlock: newly generated quick-unlock pins have not been valid since v1.5.0 - this is now fixed - re-generate your quick-unlock pin if it was created after v1.5.0 2024-01-07 19:08:02 -05:00
RandyTheSilly c85640804a Make quick-unlock pin-length configuration helptext more clear 2024-01-07 18:00:11 -05:00
RandyTheSilly 8ec2b31c23 Recommend using ssh keyfile without a password 2024-01-07 17:48:14 -05:00
RandyTheSilly 50189dd526 Warn against removing active device IDs 2024-01-07 17:14:36 -05:00
RandyTheSilly f13404e76f Apply consistent code formatting, especially regarding additive and multiplicative operators 2024-01-07 17:05:18 -05:00
RandyTheSilly 5d00f83df0 Remove trailing spaces upon commit 2024-01-07 16:44:39 -05:00
RandyTheSilly 5ce28e1138 Replace misuses of ".extend()" 2024-01-07 16:33:45 -05:00
RandyTheSilly 2f7b548360 Keep the user in device ID add/remove menus until they select "BACK", optimize menu logic 2024-01-07 16:25:43 -05:00
RandyTheSilly 6789e3e3de Improve method of serving whitelist contents to clients 2024-01-04 14:08:02 -05:00
RandyTheSilly a345f4181c Fix multi-word device IDs being unable to verify via quick-unlock 2024-01-04 13:56:26 -05:00
RandyTheSilly 08c6f1aeef Attempt to remove old device ID registration/whitelist entry from server when changing client device ID - bumps minimum supported Python version to 3.8 2024-01-04 13:41:35 -05:00
RandyTheSilly 3bb2f194d4 Standardize language (vars): _proceed/_sure -> _sure 2024-01-04 03:31:48 -05:00
RandyTheSilly 52727e8eea Extend 18d03279b9 to server tweak menu 2024-01-04 03:23:06 -05:00
RandyTheSilly cd560d6f8f Fix incorrect order of parameters when calling copy_id_check() from dev_id_config(), fixed inconsistency between order of parameters that caused the readability issue leading to the typo 2024-01-04 03:06:02 -05:00
RandyTheSilly 18d03279b9 Standardize language: device name/id -> device id, syncing -> synchronization/synchronizing 2024-01-03 22:56:13 -05:00
RandyTheSilly d4f1831486 Add a warning before changing an existing device id 2024-01-03 22:14:39 -05:00
RandyTheSilly 0ecb842480 Add "BACK" button in optional SSH configuration 2024-01-03 14:58:29 -05:00
RandyTheSilly 45be25e6bd Rename all back/cancel buttons to "BACK" 2024-01-03 14:43:14 -05:00
RandyTheSilly acf46b6227 Move registered_remover() to be covered by RMSERVER port-job 2024-01-03 14:32:46 -05:00
RandyTheSilly 3418bf6170 Replace unnecessary use of pop() 2024-01-03 14:20:15 -05:00
RandyTheSilly dbf3b35237 Add option to remove registered devices in server tweak menu 2024-01-03 14:10:09 -05:00
RandyTheSilly c48edfc624 Add 2024 to copyright duration, split changelog archive for 2024 2024-01-02 14:14:10 -05:00
RandyTheSilly b2eb63cbe6 Update documentation for release v1.5.2 2023-12-11 14:37:01 -05:00
RandyTheSilly 43f8306932 Add processing warnings during gpg key generation 2023-12-11 13:56:56 -05:00
RandyTheSilly e044122d48 Allow displaying partial options in curses menus in smaller terminals 2023-12-11 13:09:21 -05:00
RandyTheSilly 0e45a21a58 Re-implement "additional configuration options" header after init menu 2023-12-11 12:54:01 -05:00
RandyTheSilly a119544fbf Re-implement empty directory notification in entry list 2023-12-11 12:40:10 -05:00
RandyTheSilly 23e51ae133 Improve text wrapping for entry list - list now uses full width of terminal and can be displayed in smaller terminals without crashing 2023-12-11 02:58:04 -05:00
RandyTheSilly 27cf610a1f Fix crash when terminal is not tall enough to display curses radio pretext 2023-12-10 21:49:40 -05:00
RandyTheSilly 2edb1b9b53 Remove "/example/" heading format from extension downloader (this format is reserved entry paths) 2023-12-10 20:10:08 -05:00
RandyTheSilly 675e05c14f Improve text wrapping for curses radio pretext 2023-12-10 19:03:14 -05:00
RandyTheSilly 937b1357f5 Improve entry reader and entry list readability in light terminals 2023-12-10 15:28:57 -05:00
RandyTheSilly c3f8178db4 Fix needless function recursion when calling the global menu from the init menu (fixes errors on Alpine Linux 3.19) 2023-12-09 01:30:19 -05:00
RandyTheSilly 75cf927343 Properly handle error for missing pinentry program/configuration 2023-12-08 20:55:45 -05:00
RandyTheSilly 5a34cfbc8b Bump version to 1.5.2
Former-commit-id: 4344a965a805f5c92a707ed7ad37048f8eda93b6
Former-commit-id: 42325bca647366a6593272694daff9f9af390f10
2023-12-08 19:03:30 -05:00
RandyTheSilly c0d5d3b79f Address 7e1961e0ec [formerly c1521a73d443c173cd2b23f18f71f99fc54b0eb6] [formerly ef5415a793eaf8088f857331872181b40ef4d78f] in port-job
Former-commit-id: fe1d754b62dca302700dd8ab05f01a31117a9d0f
Former-commit-id: 223d95ecba109bad3c0724793c29574600f5bffe
2023-11-27 16:15:25 -05:00
RandyTheSilly d45f4264df Improve license information readability
Former-commit-id: c6c1f1b32544cd43e338c92fed8f8a168690be13
Former-commit-id: 7545104ced2e65a78734184261e104a34b4ec63b
2023-11-22 14:40:58 -05:00
RandyTheSilly dbae72b926 More clearly present extension information in the install screen
Former-commit-id: d75086ed21f45966b6c2b0074aea6e288d1ce650
Former-commit-id: fcf66ffac17cfc97868b0dc6dd4f4c4077f90fd3
2023-10-26 20:30:49 -04:00
RandyTheSilly da4a4d272d Add support for multi-line extension usage information
Former-commit-id: f3dfe5815a3ffcf453b430f12b63235a88469b3f
Former-commit-id: 9c3fec49417dfca2c1494f3edacd123b25dbe27a
2023-10-26 19:11:52 -04:00
RandyTheSilly 7e1961e0ec Fix empty clipboard warnings being shown to the user on Wayland if the same entry field is copied multiple times within 30 seconds
Former-commit-id: ef5415a793eaf8088f857331872181b40ef4d78f
Former-commit-id: c1521a73d443c173cd2b23f18f71f99fc54b0eb6
2023-10-26 18:32:47 -04:00
RandyTheSilly f80fd2b6e2 Fix quotes being copied when clearing Termux clipboard
Former-commit-id: 028b6bdcf891aa017f1d21b6b083591322695b69
Former-commit-id: 9ef4a8e58fc44068efdfab43b6f33e4de76d8efe
2023-10-26 17:48:12 -04:00
RandyTheSilly 787a2bbfc4 Fix extension removal on Busybox environments (Alpine)
Former-commit-id: 47909194ae390b1ace732f19da7a5a72592824c8
Former-commit-id: 165ea2af5edfc99d1094a4aeb1a341ac815d3af8
2023-10-25 21:16:17 -04:00
RandyTheSilly e7f0618142 Fix Fedora and FreeBSD packaging excluding clipclear.py (official v1.5.1 packages are unaffected by this)
Former-commit-id: d8d0d5a4cd14e3aac159ebd9755c82c3f0e058e2
Former-commit-id: 7bc639d5628c27988f88cb691345eb8680c07266
2023-10-24 11:50:26 -04:00
RandyTheSilly 1f5a2c8f3c Update documentation for release v1.5.1
Former-commit-id: d9721cfcc36cc53f2719f0c57c3c5c58578e3eb1
Former-commit-id: 9e3bba69e4d4e8d074b3882b56f2221aa7edd6ad
2023-10-23 23:19:03 -04:00
RandyTheSilly 386044d081 Fix colons and backslashes in gpg key names being interpreted as their hexadecimal equivalents
Former-commit-id: 98a19896d872423ff771e4b7bb5014397b63e889
Former-commit-id: 940178d578742aa457b985df4d7b231324aba0d9
2023-10-23 22:32:56 -04:00
RandyTheSilly c651c41a41 Disable ConfigParser interpolation to allow for "%" to be stored in configuration files
Former-commit-id: 4ac84f3ceef3407bd9e8e2c2f2191f7ea2ee2a55
Former-commit-id: 2fb4bf9d2630520702e99fa0913a3db5e1a0d0bb
2023-10-23 21:08:01 -04:00
RandyTheSilly 7cb245edde Fix CLIPCLEAR port-job for generic Linux (Wayland/X11)
Former-commit-id: 3690a41cbfce5eff13388fa7c9fe9f39d6e36865
Former-commit-id: a9a991dc141d1eb7d7973f8ecb08188dda27953b
2023-10-23 20:37:41 -04:00
RandyTheSilly edf31f9a7c Add port-job for making clipclear.py platform-specific
Former-commit-id: 6aafd4dfcddefc31bf61d3018b63ef48c156d8a8
Former-commit-id: c016e6872d71bc4bb347d85cf964dd9e7b7fe149
2023-10-23 13:51:11 -04:00
RandyTheSilly 362b1aa025 Remove unused variable from RMEXTMAN.py
Former-commit-id: ea907d093c1ffdccffc9db55f573d08650cfb7e2
Former-commit-id: e2178912cec4b4327c576af625fe8d1e6e498665
2023-10-23 13:05:46 -04:00
RandyTheSilly 5633520d82 Fix clipboard clearing on non-Wayland platforms; drop dos2unix as a WSL dependency; suggest openssh-server on Debian and Fedora
Former-commit-id: 046786460edc669fa9b962314953164c1cce078e
Former-commit-id: d9f0eeb57d73a0255c9dc713533d08d30413bf90
2023-10-23 10:35:59 -04:00
RandyTheSilly a487bd561a Fix clipboard contents sometimes failing to clear
Former-commit-id: 8ecbbb6199864fda973e47605a10d737318f3993
Former-commit-id: eb625eded3f9518967ee9fa06201311e8571e394
2023-10-20 00:32:29 -04:00
RandyTheSilly cbc0538e82 Call printf with %b to avoid interpreting directives
Former-commit-id: f1e812634d538b8e8c412b0c3aa1c43aab1c4f1b
Former-commit-id: f95072b8742e752be18dc3fe9cba17c8e82eb41b
2023-10-19 20:52:35 -04:00
RandyTheSilly 5c589de4f2 Switch to v1.5.1 extension pointer
Former-commit-id: 3997d7c4f7a05a8992b93a9b8d73c3d44e4c6026
Former-commit-id: 2a56f062345d177023c9a6f1f5ed5415450a9155
2023-10-19 18:47:06 -04:00
RandyTheSilly 44392467e5 Require identity to be passed to whitelist_verify() (for extension support with custom SSH keys)
Former-commit-id: 9db42722585f1fb707d5144e828cefddfcbf5137
Former-commit-id: 1e79c49459b49eb422788eba315a6d9c6e09f9a1
2023-10-18 15:14:20 -04:00
RandyTheSilly 18aacf648b Add pop-up explaining optional configuration menu
Former-commit-id: ed0d42781420080646ec6051e9f9c56ba8eaba99
Former-commit-id: 48b7d5de29ee9ad88b0aec222954769473ddda8a
2023-10-17 00:52:54 -04:00
RandyTheSilly ac621a8a73 Add "TROUBLESHOOTING" section to man page
Former-commit-id: 352412410256035906c586975b9cc5460f6b19be
Former-commit-id: 2d1092037448f1d2aaf9c137b751efcbe984995e
2023-10-17 00:40:06 -04:00
RandyTheSilly 98f76ac1ab Exclude "known_hosts.old" in ssh key listing
Former-commit-id: fc088270e9865b35ae4b10f020fec916e5093a8a
Former-commit-id: 8988481a7d3f37636dfa534312890760cd24ca81
2023-10-16 23:51:20 -04:00
RandyTheSilly bdff85f024 Fix copy_id_check resulting in errors when called from another module
Former-commit-id: 31bb5415499ca5f06413eba61eaafd284d2d5da1
Former-commit-id: 9cd0e6852e09874a1a06c8cde22e539f1cb58345
2023-10-16 23:46:19 -04:00
RandyTheSilly 2bcabe8597 Update README.md to specify Python 3.12 support
Former-commit-id: cfc8548cfb03f839d3cec89be68f839e8863de89
Former-commit-id: 1c0495fb2421d5f1b035a468fa15175b2a031e43
2023-10-02 20:05:10 -04:00
RandyTheSilly 7259a8732c (MacOS) Port new clipboard behavior to MacOS
Former-commit-id: bd887c5ee6a75130b8c994034708ae7893764251
Former-commit-id: e1621ed1a986f731c8217d741fd84a42196cb4e9
2023-09-26 20:29:51 -04:00
RandyTheSilly 20cdc27b88 (Haiku) Port new clipboard behavior to Haiku
Former-commit-id: 3a40559cf39475a0291187a4853c1e5f335b96f0
Former-commit-id: 95bfec78129d9d22afb16a33775df8d3abb24fa1
2023-09-26 19:51:05 -04:00
RandyTheSilly 2f6bdb8d2e (Termux) Port new clipboard clear behavior to Termux
Former-commit-id: bdda7fae8c2305e1f64c468d45c205d12645f669
Former-commit-id: e6fea38cc85a03b8970f0cdf61390825cc048c20
2023-09-26 19:32:56 -04:00
RandyTheSilly eb4fae3e9e Fixed new clipboard clear behavior on FreeBSD
Former-commit-id: e276182b11c518bc7b880eaebb7d9c8fd528761b
Former-commit-id: fc1f067c5c0df86109795feca05527be5322d829
2023-09-26 19:03:07 -04:00
RandyTheSilly 30afbafcbf Fixed xclip throwing errors on FreeBSD
Former-commit-id: 65b466c0598f2194d452c0897ffa9eeb7ec3b281
Former-commit-id: 51ad92728be141113047bb25fb8841d141d545e6
2023-09-26 18:37:42 -04:00
RandyTheSilly e010180219 (X11) Port new clipboard clear behavior to X11
Former-commit-id: 11edd45f8ccaa6de548d6656ef50cd0c1bdff0b4
Former-commit-id: 1fb779b6d27937d83802f50fb6857f24ec7765ed
2023-09-25 13:53:59 -04:00
RandyTheSilly 1f4e7d5cae (WSL) Marked "dos2unix" as a dependency for the WSL package
Former-commit-id: e1adc5ac923a07d87ccb2ee3c687b9e935200617
Former-commit-id: a906b07e1cc19921d1ec94cec99644657b963ee1
2023-09-25 13:11:01 -04:00
RandyTheSilly 67983da22e (WSL) Port new clipboard clear behavior to WSL
Former-commit-id: e1d96418abae79f6af75d72eeb6279e661196bd5
Former-commit-id: 0b345de72d9dc13de2f0db7c26264e0deb685965
2023-09-25 12:54:21 -04:00
RandyTheSilly aa0bd017c9 Fixed CLIPBOARD.py port-job overriding the new clipboard clear behavior
Former-commit-id: 13f705945bf0bbe95b5699f2bd64b9c790324048
Former-commit-id: 54e8c6f8d54e592bdda885de296f56f1d5ecdd24
2023-09-24 22:04:00 -04:00
RandyTheSilly 5a4e952d4d (Wayland) Only clear clipboard contents if they have not been changed since altered by sshyp
Former-commit-id: 2b6be8d327419bccc51430f732fb03ff9bba0439
Former-commit-id: 09406c55e394e97c90d984067bff9b4677b0496a
2023-09-24 21:50:14 -04:00
RandyTheSilly db05e9b05e Fixed offline-mode sync warning displaying at inappropriate times
Former-commit-id: 37c0fe00ec30a903ec7ba40525f01a2963b24d72
Former-commit-id: e6cea4d212e6f9b231dc0d0743b667b9f2eb376c
2023-09-24 19:35:48 -04:00
RandyTheSilly 397ad7d409 Update Haiku package to use Python 3.11
Former-commit-id: cc3ae472b67be63d960b4993c355f9d5acf7d899
Former-commit-id: 8d758713964c081c6dbb56899f2b84e51d25eac0
2023-09-24 19:27:20 -04:00
RandyTheSilly 5af02866b4 Fixed clipboard contents leaking through process list on Wayland
Former-commit-id: bb1485fec2d891af9f49b4d8e81df72b0e6160d9
Former-commit-id: 2eff4ace8b82735a890bf1e669d92d7abbb59dfa
2023-09-24 18:56:42 -04:00
RandyTheSilly e8267f838a Read some fields from config file as boolean values
Former-commit-id: ba2f082fc4d043d55ba5d25875ac9b9600bbbd3a
Former-commit-id: 4e31560e91047d035eb87e312fd17f3e8ad1935f
2023-09-24 16:38:30 -04:00
RandyTheSilly e4fbcd6825 Added an error for when the configured text editor cannot be found on the system
Former-commit-id: f41356854b88cf6a53604ef37025667d17041da7
Former-commit-id: 627cb9298583b7fbb6358edead75c65e671619d0
2023-09-24 15:04:36 -04:00
RandyTheSilly 9ba4b8d724 Added a warning when the user manually attempts to sync in offline mode
Former-commit-id: f54591cc6997078a248c5d8dc3dcd533824c04e4
Former-commit-id: fb405077f39e22840e98bca6a854496bd583c49c
2023-09-24 14:35:01 -04:00
RandyTheSilly 0b73d4016f Always use the user-specified ssh keyfile in place of the previously hard-coded ~/.ssh/sshyp
Former-commit-id: 2ec48a889401a5da39c162f966b2e1eaa4c90aad
Former-commit-id: e0071948993af51c3de731eaf49c16d5fdab3632
2023-09-24 14:03:44 -04:00
15 changed files with 716 additions and 348 deletions
+5 -4
View File
@@ -1,7 +1,7 @@
![sshyp](https://github.com/rwinkhart/sshyp-labs/blob/main/extra/artwork/sshyp-banner.png)
[![release](https://img.shields.io/github/v/release/rwinkhart/sshyp)](https://github.com/rwinkhart/sshyp/releases)
![python](https://img.shields.io/badge/python-3.7--3.11-yellow)
![python](https://img.shields.io/badge/python-3.8--3.12-yellow)
[![downloads](https://img.shields.io/github/downloads/rwinkhart/sshyp/total)](https://github.com/rwinkhart/sshyp/releases)
[![CodeQL](https://github.com/rwinkhart/sshyp/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](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.
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
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
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:
- improve OS compatibility
- migrate from gpg to a better-suited utility focused on symmetric cipher encryption
- seize the thrones, shear the humans
+2 -1
View File
@@ -1,4 +1,5 @@
#!/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 push
+31
View File
@@ -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
07/12/2023
+82
View File
@@ -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
View File
@@ -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
\fBsshyp\fR - Simple, self-hosted, synchronized password management for UNIX(-like) systems. Alternative to (and compatible with) pass/password-store.
.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':
sshyp /notes/test\ note add -n
Manually syncing entries with the server:
Manually synchronizing entries with the server:
sshyp sync
.SH ARGUMENTS (CLIENT)
help/-h bring up the help menu
@@ -99,6 +99,32 @@ Client setup:
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.
.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
0 - no error
+42
View File
@@ -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
View File
@@ -8,12 +8,12 @@ home = expanduser('~')
# PORT START SSHYNC-REMOTE
# REMOTE
# 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
for _file in listdir(f"{home}/.config/sshyp/deleted"):
_file_path, _sep, _device = _file.partition('\x1f')
_file_path = _file_path.replace('\x1e', '/')
if _device == _client_device_name:
if _device == _client_device_id:
print(_file_path)
try:
remove(f"{home}/.config/sshyp/deleted/{_file}")
@@ -31,7 +31,7 @@ def remote_list_gen(_client_device_name, _remote_dir):
# 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):
from shutil import rmtree
_directory = f"{home}/.local/share/sshyp/"
@@ -44,8 +44,8 @@ def delete(_file_path, _target_database, _silent):
if not _silent:
print(f"location does not exist {_target_database}")
if _target_database == 'remotely':
for _device_name in listdir(f"{home}/.config/sshyp/devices"):
open(f"{home}/.config/sshyp/deleted/" + _file_path.replace('/', '\x1e') + '\x1f' + _device_name, 'w')
for _device_id in listdir(f"{home}/.config/sshyp/devices"):
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
@@ -78,8 +78,8 @@ def remote_list_fetch(_user_data):
_deletion_database = _remote_data[0].strip().splitlines()
_folder_database = _remote_data[1].strip().splitlines()
_titles_mods = _remote_data[2].strip().splitlines()
return _deletion_database, _folder_database, _titles_mods[:len(_titles_mods)//2], \
_titles_mods[len(_titles_mods)//2:]
return _deletion_database, _folder_database, _titles_mods[:len(_titles_mods) // 2], \
_titles_mods[len(_titles_mods) // 2:]
# 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
def get_profile(_profile_dir):
from configparser import ConfigParser
_profile_data = ConfigParser()
_profile_data = ConfigParser(interpolation=None)
if isfile(_profile_dir):
_profile_data.read(_profile_dir)
else:
+168 -159
View File
@@ -18,29 +18,32 @@ home = expanduser('~')
# generates and prints full entry list
def entry_list_gen(_directory=f"{home}/.local/share/sshyp/"):
from shutil import get_terminal_size
from textwrap import fill
_ran = False
print("\nfor a list of usable commands, run 'sshyp help'\n\n\u001b[38;5;0;48;5;15msshyp entries:\u001b[0m\n")
_ran, _width = False, get_terminal_size().columns
print("\nfor a list of usable commands, run 'sshyp help'\n\n\u001b[38;5;0;48;5;15msshyp entries:\u001b[0m", end='')
for _root, _dirs, _files in sorted(walk(_directory, topdown=True)):
_entry_list, _color_alternator = [], 1
_color_alternator = 1
if _ran:
print(f"\u001b[38;5;15;48;5;238m{_root.replace(f'{home}/.local/share/sshyp', '', 1)}/\u001b[0m")
for filename in sorted(_files):
print(f"\n\n\u001b[38;5;7;48;5;8m{_root.replace(f'{home}/.local/share/sshyp', '', 1)}/\u001b[0m")
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:
_entry_list.append(filename[:-4])
_print_string = _filename[:-4]
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
_real = len(' '.join(_entry_list)) - (5.5 * len(_entry_list))
if _real <= get_terminal_size()[0]:
_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')
if _ran and _char_counter < 1:
print('\u001b[38;5;9m-empty directory-\u001b[0m', end='')
_ran = True
print('\n')
# displays the contents of an entry in a readable format
@@ -49,29 +52,29 @@ def entry_reader(_decrypted_entry):
if pass_show:
_entry_password = f'\u001b[38;5;10m{_decrypted_entry[0]}\u001b[0m'
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()
for _num in range(len(_decrypted_entry)):
try:
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] != '':
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] != '':
print(f"\u001b[38;5;15;48;5;238murl:\u001b[0m\n{_decrypted_entry[_num]}\n")
elif _num >= 3 and _decrypted_entry[_num] != '' and _notes_flag != 1:
_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(f"\u001b[38;5;7;48;5;8murl:\u001b[0m\n{_decrypted_entry[_num]}\n")
elif _notes_flag == 1 and _num >= 3:
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:
try:
_line_test = _decrypted_entry[_num + 1]
_ = _decrypted_entry[_num + 1]
except IndexError:
print()
except IndexError:
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
@@ -81,7 +84,7 @@ def string_gen(_complexity, _length):
if _complexity == 's':
_character_pool = string.ascii_letters + string.digits
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('~', '')
else:
_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:
_tmp.write(_joined_note_lines)
_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)
_new_note = _tmp.read().rstrip()
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
# encrypts an entry and cleans up the temporary files
# encrypts an entry
def encrypt(_entry_data, _entry_dir, _gpg_id):
_bytes_data = '\n'.join(_entry_data).rstrip().encode()
_encrypted_data = run(('gpg', '-qr', str(_gpg_id), '-e'), input=_bytes_data, stdout=PIPE).stdout
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):
_contents = None
# check quick-unlock status, fetch passphrase
if _quick_verify == 'true':
_quick_pass = whitelist_verify(port, username_ssh, ip, client_device_id)
if _quick_verify:
_quick_pass = whitelist_verify(port, username_ssh, ip, client_device_id, identity)
else:
if _quick_pass is None:
_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
def whitelist_verify(_port, _username_ssh, _ip, _client_device_id):
def whitelist_verify(_port, _username_ssh, _ip, _client_device_id, _identity):
try:
run(('gpg', '--pinentry-mode', 'cancel', '-qd', '--output', '/dev/null',
f"{home}/.config/sshyp/lock.gpg"), stderr=DEVNULL, check=True)
return False
except CalledProcessError:
_i, _full_password = 0, ''
_server_whitelist = run(('ssh', '-i', f"{home}/.ssh/sshyp", '-p', _port, f"{_username_ssh}@{_ip}",
f'python3 -c \'from os import listdir; print(*listdir("/home/{_username_ssh}'
f'/.config/sshyp/whitelist"))\''), stdout=PIPE, text=True).stdout.rstrip().split()
_server_whitelist = run(('ssh', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
'python3 -c \'from os import listdir; [print(_id, end="\x1f") for _id in '
f'listdir("/home/{_username_ssh}/.config/sshyp/whitelist")]\''),
stdout=PIPE, text=True).stdout.rstrip().split('\x1f')
for _device_id in _server_whitelist:
if _device_id == _client_device_id:
from getpass import getpass
_quick_unlock_password = getpass(prompt='\nquick-unlock pin: ')
_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"-qd ~/.config/sshyp/excluded.gpg"), stdout=PIPE, text=True).stdout.rstrip()
while _i < len(_quick_unlock_password_excluded):
@@ -226,7 +232,7 @@ def target_exists_check(_target_name, _expected_presence):
else:
print(f"\n\u001b[38;5;9merror: (/{_target_name}) does not exist\u001b[0m\n")
s_exit(2)
# returns target type (entry == True, folder == False, null == None), optional errors
def target_type_check(_target_name, _expected_type=True, _error=False):
@@ -245,33 +251,34 @@ def target_type_check(_target_name, _expected_type=True, _error=False):
s_exit(2)
# ensures an edited entry is optimized for best compatibility
def line_edit(_lines, _edit_data, _edit_line):
# ensures an entry has enough lines to complete an edit and optionally carries out the edit
def line_edit(_lines, _edit_line, _edit_data=None):
# ensure enough lines are present for edited field
while len(_lines) < _edit_line + 1:
_lines.append('')
# write the edited field
_lines[_edit_line] = _edit_data.rstrip()
if _edit_data is not None:
# write the edited field
_lines[_edit_line] = _edit_data.rstrip()
return _lines
# attempts to connect to the user's server via ssh to register the device for syncing
def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _sshyp_data):
# attempts to connect to the user's server via ssh to register the device for synchronization
def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshyp_data):
from stweak import write_config
if not _sshyp_data.has_section('CLIENT-ONLINE'):
_sshyp_data.add_section('CLIENT-ONLINE')
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'{_client_device_id}").touch(mode=0o400, exist_ok=True)\''), stderr=DEVNULL, check=True)
except CalledProcessError:
print('\n\u001b[38;5;9mwarning: ssh connection could not be made - ensure the public key (~/.ssh/sshyp.pub) is '
'registered on the remote server and that the entered ip, port, and username are correct\n\nsyncing '
'functionality will be disabled until this is addressed\u001b[0m\n')
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1')
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\n'
'synchronization functionality will be disabled until this is addressed\u001b[0m\n')
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'true')
write_config(_sshyp_data)
return True
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '0')
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'false')
write_config(_sshyp_data)
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
def print_info():
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'
_border = '\u001b[38;5;7;48;5;8m' + 14*'<>' + '-' + 14*'<>' + '\u001b[0m\n'
_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'
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*' '}..
{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*' '}<[◎]|_|=
}}-}}-*]{4*' '}`..'..'{9*' '}\u001b[0m♥♥♥\u001b[0m{9*' '}`..'..'{6*' '}|
{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 "
f"version 1.5.0\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"
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 ",
f"randall winkhart\u001b[38;5;15;48;5;15m{9*' '}\u001b[38;5;7;48;5;8m/\u001b[0m")
{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 * ' '}_)
{5 * ' '}_.{3 * ' '}:{7 * ' '}*{7 * ' '}\u001b[38;5;9m♥♥♥♥♥\u001b[0m{7 * ' '}*{7 * ' '}:{3 * ' '}<[◎]|_|=
}}-}}-*]{4 * ' '}`..'..'{9 * ' '}\u001b[0m♥♥♥\u001b[0m{9 * ' '}`..'..'{6 * ' '}|
{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 "
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"
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-2024 ",
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")
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 '
'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'
' more details.\n\nhttps://opensource.org/licenses/GPL-3.0\n')
'warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\nSee the GNU General Public License '
'for more details:\nhttps://opensource.org/licenses/GPL-3.0\n')
elif arguments[0] == 'add' and device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> add <option>\u001b[0m\n
\u001b[1moptions:\u001b[0m
add:
password/-p{12*' '}add a password entry
note/-n{16*' '}add a note entry
folder/-f{14*' '}add a new folder for entries\n""")
password/-p{12 * ' '}add a password entry
note/-n{16 * ' '}add a note entry
folder/-f{14 * ' '}add a new folder for entries\n""")
elif arguments[0] == 'edit' and device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> edit <option>\u001b[0m\n
\u001b[1moptions:\u001b[0m
edit:
rename/relocate/-r{5*' '}rename or relocate an entry
username/-u{12*' '}change the username of an entry
password/-p{12*' '}change the password of an entry
url/-l{17*' '}change the url attached to an entry
note/-n{16*' '}change the note attached to an entry\n""")
rename/relocate/-r{5 * ' '}rename or relocate an entry
username/-u{12 * ' '}change the username of an entry
password/-p{12 * ' '}change the password of an entry
url/-l{17 * ' '}change the url attached to an entry
note/-n{16 * ' '}change the note attached to an entry\n""")
elif arguments[0] == 'copy' and device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> copy <option>\u001b[0m\n
\u001b[1moptions:\u001b[0m
copy:
username/-u{12*' '}copy the username 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
note/-n{16*' '}copy the note of an entry to your clipboard\n""")
username/-u{12 * ' '}copy the username 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
note/-n{16 * ' '}copy the note of an entry to your clipboard\n""")
elif arguments[0] == 'gen' and device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> gen [option]\u001b[0m\n
\u001b[1moptions:\u001b[0m
gen:
update/-u{14*' '}generate a password for an existing entry\n""")
update/-u{14 * ' '}generate a password for an existing entry\n""")
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 program comes with absolutely no warranty; type 'sshyp license' for details""")
if device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp [/<entry name> [argument] [option]] | [argument]\n
\u001b[1marguments:\u001b[0m
help/-h{17*' '}bring up this menu
version/-v{14*' '}display sshyp version info
init{20*' '}set up sshyp
tweak{19*' '}change configuration options/manage extensions and updates
add{21*' '}add an entry
gen{21*' '}generate a new password
edit{20*' '}edit an existing entry
copy{20*' '}copy details of an entry to your clipboard
shear{19*' '}delete an existing entry
sync{20*' '}manually sync the entry directory via sshync
help/-h{17 * ' '}bring up this menu
version/-v{14 * ' '}display sshyp version info
init{20 * ' '}set up sshyp
tweak{19 * ' '}change configuration options/manage extensions and updates
add{21 * ' '}add an entry
gen{21 * ' '}generate a new password
edit{20 * ' '}edit an existing entry
copy{20 * ' '}copy details of an entry to your clipboard
shear{19 * ' '}delete an existing entry
sync{20 * ' '}manually sync the entry directory via sshync
\n\u001b[1moptions:\u001b[0m
add:
password/-p{12*' '}add a password entry
note/-n{16*' '}add a note entry
folder/-f{14*' '}add a new folder for entries
password/-p{12 * ' '}add a password entry
note/-n{16 * ' '}add a note entry
folder/-f{14 * ' '}add a new folder for entries
edit:
rename/relocate/-r{5*' '}rename or relocate an entry
username/-u{12*' '}change the username of an entry
password/-p{12*' '}change the password of an entry
url/-l{17*' '}change the url attached to an entry
note/-n{16*' '}change the note attached to an entry
rename/relocate/-r{5 * ' '}rename or relocate an entry
username/-u{12 * ' '}change the username of an entry
password/-p{12 * ' '}change the password of an entry
url/-l{17 * ' '}change the url attached to an entry
note/-n{16 * ' '}change the note attached to an entry
copy:
username/-u{12*' '}copy the username 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
note/-n{16*' '}copy the note 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
url/-l{17 * ' '}copy the url of an entry to your clipboard
note/-n{16 * ' '}copy the note of an entry to your clipboard
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>'
\u001b[1mtip 2:\u001b[0m type 'sshyp' to view a list of saved entries\n""")
# PORT START HELP-SERVER
else:
print(f"""\n\u001b[1musage:\u001b[0m sshyp <argument>\n
\u001b[1marguments:\u001b[0m
help/-h{17*' '}bring up this menu
version/-v{14*' '}display sshyp version info
init{20*' '}set up sshyp
tweak{19*' '}change configuration options/manage extensions and updates\n""")
help/-h{17 * ' '}bring up this menu
version/-v{14 * ' '}display sshyp version info
init{20 * ' '}set up sshyp
tweak{19 * ' '}change configuration options/manage extensions and updates\n""")
# PORT END HELP-SERVER
@@ -387,7 +394,7 @@ def read_shortcut():
# calls sshync to sync changes to the user's server
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
for _root, _dirs, _files in walk(f"{home}/.local/share/sshyp"):
for _path in _root.splitlines():
@@ -416,7 +423,7 @@ def add_entry():
else:
_note = ''
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)
@@ -424,7 +431,7 @@ def add_entry():
def add_folder():
Path(directory + entry_name).mkdir(mode=0o700, parents=True, exist_ok=True)
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'.mkdir(mode=0o700, parents=True, exist_ok=True)\''))
@@ -453,21 +460,18 @@ def rename():
# if renaming a folder
if not ssh_error:
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'.rename(Path("{directory_ssh}{_new_name}"))\''))
f'.rename(Path("{directory_ssh}{_new_name}"))\''))
else:
move(f"{directory}{entry_name}", f"{directory}{_new_name}")
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)\''))
# edits the contents of an entry
def edit():
# set to avoid PEP8 warnings
_detail, _edit_line = None, None
# ensure the edit target is an entry
target_type_check(entry_name, True, True)
@@ -480,10 +484,10 @@ def edit():
_detail, _edit_line = str(input('\nurl: ')), 2
if arguments[2] in ('note', '-n'):
_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:
_new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), _detail,
_edit_line)
_new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), _edit_line, _detail)
print('\n\u001b[1mentry preview:\u001b[0m')
entry_reader(_new_lines)
encrypt(_new_lines, directory + entry_name, gpg_id)
@@ -491,13 +495,11 @@ def edit():
# generates a password for a new or an existing entry
def gen():
# set to avoid PEP8 warnings
_username, _url, _notes = None, None, None
# gen update
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)
_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
else:
# make sure the gen target does not already exist
@@ -509,7 +511,7 @@ def gen():
_note = edit_note([])
else:
_note = ''
_new_lines = [_password, _username, _url, _note]
_new_lines = [_password, _username, _url] + _note.split('\n')
print('\n\u001b[1mentry preview:\u001b[0m')
entry_reader(_new_lines)
encrypt(_new_lines, directory + entry_name, gpg_id)
@@ -517,6 +519,7 @@ def gen():
# copies a specified field of an entry to the clipboard
def copy_data():
from hashlib import sha512
from subprocess import Popen
# ensure the copy target is an entry
target_type_check(entry_name, True, True)
@@ -533,33 +536,38 @@ def copy_data():
# ensure field is not blank
if _copy_subject == '':
raise IndexError
# store hashed _copy_subject for later comparison
_hash = sha512()
_hash.update(_copy_subject.encode('utf-8'))
# PORT START CLIPBOARD
# WSL clipboard detection
if 'WSL_DISTRO_NAME' in environ:
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
elif 'WAYLAND_DISPLAY' in environ:
run(('wl-copy', _copy_subject))
Popen('sleep 30; wl-copy -c', shell=True)
run('wl-copy', stdin=Popen(('printf', '%b', _copy_subject.replace('\\', '\\\\')), stdout=PIPE).stdout)
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'wayland'))
# Haiku clipboard detection
elif uname()[0] == 'Haiku':
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
elif uname()[0] == 'Darwin':
run('pbcopy', stdin=Popen(('printf', _copy_subject.replace('\\', '\\\\').replace('%', '%%')), stdout=PIPE)
.stdout)
Popen("sleep 30; printf '' | pbcopy", shell=True)
run('pbcopy', stdin=Popen(('printf', '%b', _copy_subject.replace('\\', '\\\\')), stdout=PIPE).stdout)
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'mac'))
# Termux (Android) clipboard detection
elif isdir("/data/data/com.termux"):
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
elif 'DISPLAY' in environ:
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', _copy_subject.replace('\\', '\\\\')
.replace('%', '%%')), stdout=PIPE).stdout)
Popen("sleep 30; printf '' | xclip -sel c", shell=True)
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', '%b', _copy_subject.replace('\\', '\\\\')), stdout=PIPE)
.stdout)
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'x11'))
else:
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')
@@ -570,7 +578,7 @@ def copy_data():
def remove_data():
decrypt(None, _quick_verify=quick_unlock_enabled)
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)\''))
else:
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/'
if isdir(_extension_dir):
for _extension in listdir(_extension_dir):
_extension_config = ConfigParser()
_extension_config = ConfigParser(interpolation=None)
_extension_config.read(_extension_dir + _extension)
_input_com = _extension_config.get('config', 'input').split()
if _input_com == arguments[arg_start:]:
@@ -601,10 +609,7 @@ if __name__ == "__main__":
try:
# set default states
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
arguments = argv[1:]
arg_count = len(arguments)
@@ -615,8 +620,8 @@ if __name__ == "__main__":
arg_start = 1
entry_name = arguments[0].strip('/')
# determine whether to show passwords in entry previews
if arg_count > 1 and arguments[arg_count-1] in ('--show', '-s'):
arguments.pop()
if arg_count > 1 and arguments[arg_count - 1] in ('--show', '-s'):
del arguments[-1]
arg_count -= 1
pass_show = True
else:
@@ -624,35 +629,36 @@ if __name__ == "__main__":
# import saved userdata
try:
sshyp_data = ConfigParser()
sshyp_data = ConfigParser(interpolation=None)
sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini")
device_type = sshyp_data.get('GENERAL', 'device_type')
if device_type == 'client':
directory = f"{home}/.local/share/sshyp/"
gpg_id = sshyp_data.get('CLIENT-GENERAL', 'gpg_id')
editor = sshyp_data.get('CLIENT-GENERAL', 'text_editor')
offline_mode_enabled = sshyp_data.get('CLIENT-GENERAL', 'offline_mode_enabled')
if offline_mode_enabled == 'true':
offline_mode_enabled = sshyp_data.getboolean('CLIENT-GENERAL', 'offline_mode_enabled')
if offline_mode_enabled:
ssh_error = True
quick_unlock_enabled = 'false'
quick_unlock_enabled = False
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')
ip = sshyp_data.get('SSHYNC', 'ip')
port = sshyp_data.get('SSHYNC', 'port')
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]
ssh_error = int(sshyp_data.get('CLIENT-ONLINE', 'ssh_error'))
if ssh_error == 1:
ssh_error = copy_id_check(port, username_ssh, ip, client_device_id, sshyp_data)
ssh_error = sshyp_data.getboolean('CLIENT-ONLINE', 'ssh_error')
if ssh_error:
ssh_error = copy_id_check(port, username_ssh, ip, client_device_id, identity, sshyp_data)
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(f"{73*'!'}\n")
print(f"{73 * '!'}\n")
s_exit(1)
else:
from stweak import wrapped_entry
wrapped_entry(False)
wrapped_entry(False, 'additional configuration options:')
s_exit()
# run function based on arguments
@@ -703,17 +709,20 @@ if __name__ == "__main__":
success_flag = True
from stweak import wrapped_entry
wrapped_entry(device_type)
if arg_count > 0 and success_flag == 0 and arguments[0] != 'sync':
if arguments[0] not in ('help', '-h', 'version', '-v', 'license'):
extension_runner()
else:
print_info()
elif not ssh_error:
if sync_flag:
if sync_flag:
sync()
elif (arg_count > 0 and arguments[0] == 'sync') or (arg_count > 1 and arguments[1] == 'shear'):
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
# server arguments
+251 -131
View File
@@ -5,12 +5,12 @@ from os import environ, listdir, remove
from os.path import exists, expanduser, isfile
from pathlib import Path
from random import randint
from shutil import get_terminal_size, which
from subprocess import PIPE, run
from shutil import which
from subprocess import CalledProcessError, DEVNULL, PIPE, run
# PORT START UNAME-IMPORT-STWEAK
from os import uname
# 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"):
_exists_flag = True
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):
curs_set(0)
_selected = 0
while True:
# clear curses window
stdscr.clear()
stdscr.addstr(0, 0, _pretext)
for _i, _option in enumerate(_options):
_y = _i + _pretext.count('\n') + 2
if _i == _selected:
stdscr.addstr(_y, 0, "[*] " + _option, A_REVERSE)
# get terminal size
_height, _width = stdscr.getmaxyx()
# track whether to generate interactive buttons (depending on terminal size)
_button_gen = False
# split text based on new lines
_pretext_lines = _pretext.split('\n')
# iterate through lines, splitting further (wrapping) as needed, to add to curses window
_current_line = 0
for _line in _pretext_lines:
if _current_line <= _height - 4:
_button_gen = True
while len(_line) > _width:
stdscr.addstr(_current_line, 0, _line[:_width])
_line = _line[_width:]
_current_line += 1
stdscr.addstr(_current_line, 0, _line)
_current_line += 1
else:
stdscr.addstr(_y, 0, "[ ] " + _option)
stdscr.refresh()
_key = stdscr.getch()
_button_gen = False
stdscr.addstr(_current_line, 0, '# warning: re-size terminal to see more information'[:_width - 1])
_current_line += 1
break
# create user-interactive options if all information has been displayed to the user
if _button_gen:
for _i, _option in enumerate(_options):
_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
if _key == KEY_UP:
_selected = (_selected-1) % len(_options)
elif _key == KEY_DOWN:
_selected = (_selected+1) % len(_options)
elif _key == ord('\n'):
break
_key = stdscr.getch()
if _button_gen:
if _key == KEY_UP:
_selected = (_selected - 1) % len(_options)
elif _key == KEY_DOWN:
_selected = (_selected + 1) % len(_options)
# ord('\n') == 10
elif _key == 10:
break
stdscr.refresh()
curs_set(1)
return _selected
@@ -55,9 +92,9 @@ def curses_radio(_options, _pretext):
def curses_text(_pretext):
stdscr.clear()
stdscr.addstr(0, 0, _pretext)
_term_columns = get_terminal_size()[0]
_editwin = newwin(1, _term_columns-2, 3, 1)
rectangle(stdscr, 2, 0, 4, _term_columns-1)
_width = stdscr.getmaxyx()[1]
_editwin = newwin(1, _width - 2, 3, 1)
rectangle(stdscr, 2, 0, 4, _width - 1)
stdscr.refresh()
_box = Textbox(_editwin)
# 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')]
_named_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')
_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"):
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',
'Name-Comment: gpg-sshyp\n', 'Name-Email: github.com/rwinkhart/sshyp\n',
'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")
_gpg_id = run(('gpg', '-k', '--with-colons'), stdout=PIPE, text=True).stdout.splitlines()[-1].split(':')[9]
else:
@@ -145,28 +191,34 @@ def editor_config(_env_mode):
# ssh+sshync configuration
def ssh_config():
def ssh_config(_reconfig=False):
# private key selection/generation
_keys = []
# ensure ~/.ssh directory exists
Path(f"{home}/.ssh").mkdir(mode=0o700, exist_ok=True)
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}"):
_keys.append(f"{home}/.ssh/{_file}")
_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?')
_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:
_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'))
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)'
'\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))
else:
_ssh_key = _keys[_key_selected_num]
# ssh+sshync configuration
_uiport = curses_text('enter the username, ip, and ssh port of your sshyp server:\n\n\n\n\n(ctrl+g/enter to '
'confirm)\n\nexample inputs:\n\n ipv4: user@10.10.10.10:22\n ipv6: user@[2000:2000:2000:2000'
@@ -184,23 +236,45 @@ def ssh_config():
sshyp_data.set('SSHYNC', 'remote_dir', f"/home/{_username_ssh}/.local/share/sshyp/")
sshyp_data.set('SSHYNC', 'identity_file', _ssh_key)
write_config()
return _iport[1], _username_ssh, _iport[0]
return _iport[1], _username_ssh, _iport[0], _ssh_key
# 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
_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 '
'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 = _device_id_prefix + '-' + _device_id_suffix
# 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}")
open(f"{home}/.config/sshyp/devices/{_device_id}", 'w')
# test server connection and attempt to register device id
copy_id_check(_ip, _username_ssh, _port, _device_id, sshyp_data)
# test server connection and attempt to register device id - only run if _reconfig is True, since the keyfile
# 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
@@ -232,11 +306,11 @@ def refresh_encryption():
_directory = f"{home}/.local/share/sshyp"
# 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"
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?")
if _proceed != 1:
return 3
_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/"
"share/sshyp.new\n\nare you sure you wish to re-encrypt all entries?")
if _sure != 1:
return 3
# set new gpg key
gpg_config()
@@ -248,14 +322,14 @@ def refresh_encryption():
# prompt for unlock and display do not close warning
decrypt(None)
curses_radio(['okay'], 'entry optimization may take some time - select "okay" to start - '
'do not terminate this process!')
curses_radio(['okay'], 'entry optimization may take some time (especially on slower devices)\n\nselect "okay" '
'to start\n\ndo not terminate this process!')
# remove existing conflicts
for _extension in ('.new', '.old'):
if exists(f"{_directory}{_extension}"):
rmtree(f"{_directory}{_extension}")
# decrypt, optimize, and re-encrypt each entry with the newly selected key
if isdir(_directory):
_gpg_id = sshyp_data.get('CLIENT-GENERAL', 'gpg_id')
@@ -265,30 +339,52 @@ def refresh_encryption():
encrypt(decrypt(f"{_root}/{_filename[:-4]}"),
f"{_root.replace(_directory, _directory + '.new', 1)}/{_filename[:-4]}", _gpg_id)
# create a backup of the original version and activate the new version
# create a backup of the original version and activate the new version
move(_directory, _directory + '.old')
move(_directory + '.new', _directory)
return 1
else:
return 2
# 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
def whitelist_setup():
_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:
_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 "
"or less\n\ncannot be a negative number"))
if not 0 <= _short_password_length <= _half_length:
"to confirm)\n\nnote: do NOT enter your desired pin - this is simply "
"an integer used to determine the length of the auto-generated pin"
"\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
except ValueError:
_short_password_length = _half_length
_i, _quick_unlock_password, _quick_unlock_password_excluded = 0, '', ''
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
else:
_quick_unlock_password_excluded += _char
@@ -298,6 +394,8 @@ def whitelist_setup():
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',
'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',
_quick_unlock_password, f"{home}/.config/sshyp/gpg-gen"))
remove(f"{home}/.config/sshyp/gpg-gen")
@@ -305,43 +403,44 @@ def whitelist_setup():
# encrypt excluded with the assembly key
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}")
# adds or removes quick-unlock whitelisted device ids
def whitelist_manage(_action):
_whitelisted_ids = listdir(f"{home}/.config/sshyp/whitelist")
def whitelist_manage(_action, _back=False):
_whitelisted_ids = listdir(f"{home}/.config/sshyp/whitelist") + ['BACK']
_device_ids = listdir(f"{home}/.config/sshyp/devices")
# a value of True indicates adding
if _action:
_unwhitelisted_ids = []
for _id in _device_ids:
if _id not in _whitelisted_ids:
_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]}")
_unwhitelisted_ids = []
for _id in _device_ids:
if _id not in _whitelisted_ids:
_unwhitelisted_ids.append(_id)
_unwhitelisted_ids.append('BACK')
# prune deleted device ids from whitelist
for _id in _whitelisted_ids:
if _id not in _device_ids:
remove(f"{home}/.config/sshyp/whitelist/{_id}")
while not _back:
# _action == True indicates adding
if _action:
_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
def whitelist_menu():
while True:
_choice = curses_radio(('setup/create pin', 'add to whitelist', 'remove from whitelist',
_choice = curses_radio(('setup/create pin', 'add to whitelist', 'remove from whitelist',
'BACK'), 'quick-unlock/whitelist management')
if _choice == 0:
whitelist_setup()
@@ -360,23 +459,27 @@ def extension_downloader():
from os import chmod
from tempfile import gettempdir
from urllib.request import urlopen, urlretrieve
_file_data = urlopen("https://raw.githubusercontent.com/rwinkhart/sshyp-labs/main/pointers/v1.5.0").read()
_pointer = ConfigParser()
# the version listed below will NOT always match the version of sshyp being used
# it is only updated if new extensions are incompatible with previous sshyp versions
_file_data = urlopen("https://raw.githubusercontent.com/rwinkhart/sshyp-labs/main/pointers/v1.5.2").read()
_pointer = ConfigParser(interpolation=None)
_pointer.read_string(_file_data.decode('utf-8'))
_extensions = _pointer.sections()
_extensions.append('CANCEL')
_extensions.append('BACK')
_choice = curses_radio(_extensions, 'select an extension for more info')
if _choice == len(_extensions)-1:
if _choice == len(_extensions) - 1:
return False
_selected = _extensions[_choice]
_choice = curses_radio(('no', 'yes'), f"description: {_pointer.get(_selected, 'desc')}\n\nusage: "
f"{_pointer.get(_selected, 'usage')}\n\ninstall {_selected}?")
_divider = (stdscr.getmaxyx()[1]) * '-'
_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 _choice == 1:
# download extension files to temporary directory
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
_ext_exe = urlretrieve(_pointer.get(_selected, 'exe'), _exe_dir)
_ext_ini = urlretrieve(_pointer.get(_selected, 'ini'), _ini_dir)
urlretrieve(_pointer.get(_selected, 'exe'), _exe_dir)
urlretrieve(_pointer.get(_selected, 'ini'), _ini_dir)
# set permissions under active user
chmod(_exe_dir, 0o755)
chmod(_ini_dir, 0o644)
@@ -389,9 +492,9 @@ def extension_remover():
_installed = []
for _extension in listdir('/usr/lib/sshyp/extensions'):
_installed.append(_extension[:-4])
_installed.append('CANCEL')
_installed.append('BACK')
_choice = curses_radio(_installed, 'select an extension to uninstall')
if _choice == len(_installed)-1:
if _choice == len(_installed) - 1:
return False
_sure = curses_radio(('no', 'yes'), f"are you sure you want to remove {_installed[_choice]}?")
if _sure == 0:
@@ -438,14 +541,14 @@ def global_menu(_scr, _device_type, _top_message):
while True:
_options, _choice, _exit_signal = ['change device/synchronization types'], 0, False
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] enable/disable quick-unlock',
'[OPTIONAL] re-encrypt/optimize entries',
'[OPTIONAL] extension management'])
else:
_options.extend(['manage quick-unlock/whitelist'])
_options.extend(['EXIT/DONE'])
_options.extend(['remove registered device ids', 'manage quick-unlock/whitelist'])
_options.append('EXIT/DONE')
_choice += curses_radio(_options, _top_message)
if _choice == 0:
@@ -458,41 +561,44 @@ def global_menu(_scr, _device_type, _top_message):
# ...and text editor settings are missing
if not sshyp_data.has_option('CLIENT-GENERAL', 'text_editor'):
editor_config(True)
# ...and online (synced) mode is enabled...
# ...and online (synchronized) mode is enabled...
if _dev_sync_types[1] == 'false':
# ...and quick-unlock settings are missing
if not sshyp_data.has_option('CLIENT-ONLINE', 'quick_unlock_enabled'):
quick_unlock_config(True)
# 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
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
if not listdir(f"{home}/.config/sshyp/devices"):
if None in (_ip, _username_ssh, _port):
_ip, _username_ssh, _port = ssh_config()
dev_id_config(_ip, _username_ssh, _port)
_ip, _username_ssh, _port, _identity = ssh_config()
dev_id_config(_port, _username_ssh, _ip, _identity)
# ...or ssh_error is missing
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')
write_config()
_device_type = _dev_sync_types[0]
elif _choice == 1:
if _device_type == 'client':
gpg_config()
else:
whitelist_menu()
registered_dev_id_remover()
elif _choice == 2:
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:
_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:
editor_config(False)
elif _choice == 5:
@@ -553,12 +659,11 @@ def initial_setup(_scr):
# online (synchronized mode) configuration
if _dev_sync_types[1] != 'true':
# ssh+sshync configuration
_ip, _username_ssh, _port = ssh_config()
_ip, _username_ssh, _port, _identity = ssh_config()
# device id configuration
dev_id_config(_ip, _username_ssh, _port)
dev_id_config(_port, _username_ssh, _ip, _identity)
# PORT START CLIPTOOL
# check for clipboard tool and display warning if missing
@@ -576,39 +681,54 @@ def initial_setup(_scr):
# PORT END CLIPTOOL
# 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
def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'):
from curses import wrapper, use_default_colors
global gm_device_type
gm_device_type = _gm_device_type
# 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)))
# any other value will be provided as the global menu device type
else:
_repeat = True
while _repeat:
try:
_ext_name, _escalator, _action = \
wrapper(lambda _wrap_stdscr: (use_default_colors(),
global_menu(_wrap_stdscr, _gm_device_type, _gm_top_message)))[1]
except ChildProcessError:
print("\n\u001b[38;5;9merror: privilege escalation required\n\nneither 'doas' nor 'sudo' were found in "
"the system's $PATH\u001b[0m\n")
return
# only run if privilege escalation is needed
if _action is not None:
if _action:
# install with privilege escalation (outside of curses)
from tempfile import gettempdir
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir))
run((_escalator, 'mv', _exe_dir, f"/usr/lib/sshyp/{_ext_name}"))
run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
# any other value will be interpreted as the global menu device type
# this code still runs when called for init once the init menu terminates
_repeat = True
while _repeat:
try:
_ext_name, _escalator, _action = \
wrapper(lambda _wrap_stdscr: (use_default_colors(),
global_menu(_wrap_stdscr, gm_device_type, _gm_top_message)))[1]
except ChildProcessError:
print("\n\u001b[38;5;9merror: privilege escalation required\n\nneither 'doas' nor 'sudo' were found in "
"the system's $PATH\u001b[0m\n")
return
# only run if privilege escalation is needed
if _action is not None:
if _action:
# install with privilege escalation (outside of curses)
from tempfile import gettempdir
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
# PORT START TWEAK-EXT-CHOWN
if uname()[0] == 'FreeBSD':
run((_escalator, 'chown', 'root:wheel', _exe_dir, _ini_dir))
else:
# uninstall with privilege escalation (outside of curses)
run((_escalator, 'rm', '-I', f"/usr/lib/sshyp/{_ext_name}",
f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir))
# PORT END TWEAK-EXT-CHOWN
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:
_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
View File
@@ -1,6 +1,6 @@
#!/bin/sh
version=$(head -n1 extra/changelog-archive/2023 | cut -c8-)
version=$(head -n1 extra/changelog-archive/2024 | cut -c8-)
if [ -z "$2" ]; then
revision=1
else
@@ -21,6 +21,7 @@ _create_generic_linux() {
cd port-jobs
./CLIPTOOL.py LINUX
./CLIPBOARD.py LINUX
./CHOWN.py
./UNAME.py LINUX
./COMMENTS.py ALL
./BLANKS.py
@@ -135,7 +136,7 @@ licenses {
\"GNU GPL v3\"
}
copyrights {
\"2021-2023 Randall Winkhart\"
\"2021-2024 Randall Winkhart\"
}
provides {
sshyp_client = "$version"
@@ -144,7 +145,7 @@ provides {
requires {
gnupg
openssh
python310
python3.11
}
urls {
\"https://github.com/rwinkhart/sshyp\"
@@ -158,6 +159,7 @@ urls {
./RMEXTMAN.py
./CLIPTOOL.py
./CLIPBOARD.py HAIKU
./CHOWN.py
./UNAME.py TMP
./COMMENTS.py ALL
./BLANKS.py
@@ -200,6 +202,7 @@ _create_deb() {
./CLIPBOARD.py WSL
special=WSL-ONLY-DEBIAN
fi
./CHOWN.py
./UNAME.py LINUX
./COMMENTS.py ALL
./BLANKS.py
@@ -220,11 +223,17 @@ Section: utils
Architecture: all
Maintainer: Randall Winkhart <idgr at tutanota dot com>
Description: A light-weight, self-hosted, synchronized password manager
Depends: python3, gnupg, openssh-client
Suggests: wl-clipboard, xclip, bash-completion
Priority: optional
Installed-Size: $size
Depends: python3, gnupg, openssh-client
" > 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/
mv output/debiantemp/sshyp_"$version"-"$revision"_all.deb output/"$special"-sshyp_"$version"-"$revision"_all.deb
rm -rf output/debiantemp
@@ -247,6 +256,7 @@ _create_termux() {
./RMEXTMAN.py
./CLIPTOOL.py
./CLIPBOARD.py TERMUX
./CHOWN.py
./UNAME.py TERMUX
./COMMENTS.py ALL
./BLANKS.py
@@ -296,7 +306,7 @@ License: GPL-3.0-only
URL: https://github.com/rwinkhart/sshyp
Source0: GENERIC-FEDORA-sshyp-"$version".tar.xz
Requires: python gnupg openssh-clients
Recommends: wl-clipboard xclip bash-completion
Recommends: wl-clipboard xclip bash-completion openssh-server
%%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.
%%install
@@ -307,6 +317,7 @@ cp -r %%{_sourcedir}/usr %%{buildroot}
/usr/lib/sshyp/sshyp.py
/usr/lib/sshyp/sshync.py
/usr/lib/sshyp/stweak.py
/usr/lib/sshyp/clipclear.py
/usr/lib/sshyp/extensions/
/usr/share/bash-completion/completions/sshyp
/usr/share/zsh/site-functions/_sshyp
@@ -319,6 +330,7 @@ cp -r %%{_sourcedir}/usr %%{buildroot}
cd port-jobs
./CLIPTOOL.py LINUX
./CLIPBOARD.py LINUX
./CHOWN.py
./UNAME.py LINUX
./COMMENTS.py ALL
./BLANKS.py
@@ -371,6 +383,7 @@ printf "/usr/bin/sshyp
/usr/lib/sshyp/sshyp.py
/usr/lib/sshyp/sshync.py
/usr/lib/sshyp/stweak.py
/usr/lib/sshyp/clipclear.py
/usr/local/share/bash-completion/completions/sshyp
/usr/local/share/zsh/site-functions/_sshyp
/usr/share/licenses/sshyp/license
@@ -381,7 +394,8 @@ printf "/usr/bin/sshyp
cp lib/* port-jobs/working/
cd port-jobs
./CLIPTOOL.py LINUX
./CLIPBOARD.py LINUX
./CLIPBOARD.py BSD
./CHOWN.py BSD
./UNAME.py TMP
./COMMENTS.py ALL
./BLANKS.py
+28
View File
@@ -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
View File
@@ -3,44 +3,60 @@ import re
from sys import argv, exit as s_exit
# read arguments
arguments, replacement = argv[1:], None
arguments, clip_replacement, clear_replacement = argv[1:], None, None
# define replacement text depending on arguments
if len(arguments) > 0:
if arguments[0] == 'WSL':
replacement = """run(('powershell.exe', '-c', "Set-Clipboard '" + _copy_subject.replace("'", "''") + "'"))
Popen("sleep 30; powershell.exe -c Set-Clipboard ''", shell=True, stdout=DEVNULL, stderr=DEVNULL)"""
clip_replacement = """run(('powershell.exe', '-c', "Set-Clipboard '" + _copy_subject.replace("'", "''") + "'"))
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':
replacement = """run('pbcopy', stdin=Popen(('printf', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')
.replace('%', '%%')), stdout=PIPE).stdout)
Popen("sleep 30; printf '' | pbcopy", shell=True)"""
clip_replacement = """run('pbcopy', stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout)
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'mac'))"""
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':
replacement = """run(('clipboard', '-c', _copy_subject))
Popen('sleep 30; clipboard -r', shell=True)"""
clip_replacement = """run(('clipboard', '-c', _copy_subject))
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':
replacement = """run(('termux-clipboard-set', _copy_subject))
Popen("sleep 30; termux-clipboard-set ''", shell=True)"""
elif arguments[0] == 'LINUX':
replacement = """if 'WAYLAND_DISPLAY' in environ:
run(('wl-copy', _copy_subject))
Popen('sleep 30; wl-copy -c', shell=True)
clip_replacement = """run(('termux-clipboard-set', _copy_subject))
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'termux'))"""
clear_replacement = """hash_paste.update(run('termux-clipboard-get', stdout=PIPE).stdout.strip())
if argv[1] == hash_paste.hexdigest():
run(("termux-clipboard-set", "''"))"""
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:
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', _copy_subject
.replace('\\\\\\', '\\\\\\\\\\\\\\').replace('%', '%%')), stdout=PIPE).stdout)
Popen("sleep 30; printf '' | xclip -sel c", shell=True)"""
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout)
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'x11'))"""
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:
s_exit()
# define PORT target
string1 = '# PORT START CLIPBOARD'
string2 = '# PORT END CLIPBOARD'
targets = (('CLIPBOARD', 'sshyp.py', '', '', clip_replacement),
('CLIPCLEAR', 'clipclear.py', '\n', '', clear_replacement))
# read input file
text = open('working/sshyp.py', 'r').read()
# find and replace the defined PORT target
regex = re.compile(f"{string1}.*?{string2}", re.DOTALL)
new_text = re.sub(regex, replacement, text)
# write updated text
open('working/sshyp.py', 'w').write(new_text)
for target in targets:
# read text from target file
text = open(f"working/{target[1]}", 'r').read()
# compile regex and modify text
regex = re.compile(f"{target[2]}# PORT START {target[0]}.*?# PORT END {target[0]}{target[3]}", re.DOTALL)
new_text = re.sub(regex, target[4], text)
# write updated text to target file
open(f"working/{target[1]}", 'w').write(new_text)
+2 -4
View File
@@ -1,14 +1,12 @@
#!/usr/bin/env python3
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'], "
"'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 "
"at https://github.com/rwinkhart/sshyp-labs/releases')"))
for target in targets:
# read text from target file
text = open(f"working/{target[1]}", 'r').read()
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/sh
if [ "$1" = 'PREP' ]; then
sed -i '1 s/.*/#!\/bin\/env\ python3.10/' ./*.py
sed -i '1 s/.*/#!\/bin\/env\ python3.11/' ./*.py
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
+1 -1
View File
@@ -1,5 +1,5 @@
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
it under the terms of version 3 (only) of the GNU General Public License