Compare commits

...
60 Commits
Author SHA1 Message Date
RandyTheSilly f774ff63aa [wiki] Add a postmortem documenting sshyp's flaws 2024-08-08 21:26:15 -04:00
RandyTheSilly 15e54cb25b [wiki] Migrate wiki to main repo for long-term archival 2024-08-08 20:50:19 -04:00
RandyTheSilly d96de2b7b8 Advertise MUTN as the successor to sshyp 2024-08-08 20:24:06 -04:00
RandyTheSilly 8ff431dc46 Generate ed25519 keys instead of RSA 2024-07-10 00:58:36 -04:00
RandyTheSilly 5b4d3b1267 Fix awkward warning wording, add information on sshyp's in-development successor 2024-03-21 16:57:39 -04:00
RandyTheSilly a88c4e74a0 Remove inaccurate downloads counter (was counting clones) and CodeQL 2024-03-21 16:33:03 -04:00
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
28 changed files with 708 additions and 375 deletions
-72
View File
@@ -1,72 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '34 22 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+2 -1
View File
@@ -1 +1,2 @@
* /output
/port-jobs/working
+31 -32
View File
@@ -1,44 +1,27 @@
![sshyp](https://github.com/rwinkhart/sshyp-labs/blob/main/extra/artwork/sshyp-banner.png) ![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) [![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.12-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)
pronounced as: 'sheep', 'shēp' pronounced as: 'sheep', 'shēp'
sshyp is a very simple self-hosted, synchronized password manager for UNIX(-like) systems (currently Haiku/FreeBSD/Linux). sshyp is a self-hosted, synchronized password manager for UNIX(-like) systems (currently Haiku/FreeBSD/Linux). It has been succeeded by [MUTN](https://github.com/rwinkhart/MUTN) due to its many [flaws](https://github.com/rwinkhart/sshyp/blob/main/wiki/postmortem.md).
sshyp is compatible with entries created by pass/password-store, as its original goal was to be like pass/password-store, but far more user-friendly to synchronize with a self-hosted server. sshyp is compatible with entries created by pass/password-store, as its original goal was to be like pass/password-store, but far more user-friendly to synchronize with a self-hosted server.
sshyp makes use of a custom sftp wrapper, called sshync (ssh+sync), to reliably sync user entries with a local or remote server. sshyp makes use of a custom sftp wrapper, called sshync (ssh+sync), to reliably sync user entries with a local or remote server.
The name "sshyp" is a combination of its syncing library, "sshync", and "passwords". The name "sshyp" is a combination of its synchronization library, "sshync", and "passwords".
# WARNING # WARNING
It is your responsibility to assess the security and stability of "sshyp" before using it and ensure it meets your needs. It is your responsibility to assess the security and stability of sshyp and to ensure it meets your needs before using it.
I am not responsible for any data loss or breaches of your information resulting from the use of "sshyp". I am not responsible for any data loss or breaches of your information resulting from the use of sshyp.
"sshyp" is a new project that is constantly being updated, and though safety and security are priorities, they cannot be guaranteed. sshyp has not been extensively tested by the public; safety and security are priorities, but they cannot be guaranteed.
Always check the [known bugs](https://github.com/rwinkhart/sshyp/wiki/Known-Bugs) list before updating or installing sshyp.
# Mission Statement
sshyp aims to make it as simple as possible to manage passwords and notes via CLI across multiple devices in a secure, self-hosted fashion.
What sshyp can do:
- securely manage a collection of encrypted passwords and notes via CLI
- generate new, secure passwords to the user's choice in length and complexity
- securely sync said passwords and notes seamlessly between devices (or just manage them offline)
- utilize [extensions](https://github.com/rwinkhart/sshyp-labs) to interact with your entries is additional ways (such as generating TOTP keys or managing your entries in a GUI)
- everything above with entries created by pass/password-store!
- everything above on Haiku, FreeBSD, Linux, and Termux!
# Installation # Installation
**Important:** *Shell completions (both Bash and ZSH) may require additional configuration on some distributions - please see [this page](https://github.com/rwinkhart/sshyp/wiki/Completions) of the wiki for support.* **Important:** *Shell completions (both Bash and ZSH) may require additional configuration on some distributions - please see [this page](https://github.com/rwinkhart/sshyp/blob/main/wiki/completions.md) of the wiki for support.*
Please see the [installation guide](https://github.com/rwinkhart/sshyp/wiki/Installation) in the sshyp wiki for directions specific to your distribution/OS. Please see the [installation guide](https://github.com/rwinkhart/sshyp/blob/main/wiki/install.md) in the sshyp wiki for directions specific to your distribution/OS.
Pre-built packages exist for Haiku, FreeBSD, Alpine Linux, Debian/Ubuntu Linux, Fedora Linux, Termux, and WSL. These can be downloaded from the releases page. Pre-built packages exist for Haiku, FreeBSD, Alpine Linux, Debian/Ubuntu Linux, Fedora Linux, Termux, and WSL. These can be downloaded from the releases page.
@@ -74,13 +57,29 @@ Please note that decrypting and reading entries is disabled on server devices fo
All available options can be found with `sshyp help`, or alternatively, in the man page. All available options can be found with `sshyp help`, or alternatively, in the man page.
# Roadmap # Roadmap (successor)
Short-term Goals:
- implement a method for tracking how long it has been since a password has been changed (password aging) [MUTN](https://github.com/rwinkhart/MUTN) has been developed as a direct successor to sshyp. It is not yet at release v1.0.0, therefore breaking changes will be made.
- find and fix more bugs
Long-term Goals: If you're interested in migrating to MUTN early, please see the [migration guide](https://github.com/rwinkhart/libmutton/blob/main/wiki/migration.md).
- migrate from gpg to a better-suited utility focused on symmetric cipher encryption Some key differences are:
- seize the thrones, shear the humans - Modularity/Maintainability
- MUTN is based off of [libmutton](https://github.com/rwinkhart/libmutton), enabling third-party clients
- Due to the modularity of the code, there will be no more "extension" support
- sshyp-mfa functionality is built into the successor
- Server and client code are now two completely separate projects
- This greatly simplifies the code and makes it easier to maintain
- This also means that third-party clients do not need to maintain separate server code
- Stability
- sshyp has a track record of making breaking changes in most of its updates
- This will not be the case with MUTN (starting with release v1.0.0)
- Efficiency
- MUTN is written in a compiled language (Go)
- This means that encryption and SSH-sync can be done natively in Go rather than relying on GnuPG/OpenSSH (this is possible with Python, but it would require users to install third-party libraries)
- MUTN makes small tweaks to the design established by sshyp to make user interactions less frustrating
- Platform support
- MUTN was built from the ground-up to support both UNIX-like platforms AND Windows
- Unfortunately, using Go means dropping Haiku support, as newer versions of Go do not (yet) support Haiku
From this point forward, sshyp will only receive minimal support (as needed) and transitional updates.
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/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 sed -i 's/[[:space:]]*$//' ./lib/* ./port-jobs/* ./package.sh
git commit -m "$1" git commit -am "$1"
git push git push
+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
+2 -2
View File
@@ -1,4 +1,4 @@
.TH sshyp 1 "24 October 2023" "v1.5.1" "sshyp man page" .TH sshyp 1 "08 January 2024" "v1.5.3" "sshyp man page"
.SH NAME .SH NAME
\fBsshyp\fR - Simple, self-hosted, synchronized password management for UNIX(-like) systems. Alternative to (and compatible with) pass/password-store. \fBsshyp\fR - Simple, self-hosted, synchronized password management for UNIX(-like) systems. Alternative to (and compatible with) pass/password-store.
.SH SYNOPSIS .SH SYNOPSIS
@@ -38,7 +38,7 @@ Making a new entry saved as '~/.local/share/sshyp/school/university.gpg' using t
Creating a note-only entry saved as '~/.local/share/sshyp/notes/test note.gpg': Creating a note-only entry saved as '~/.local/share/sshyp/notes/test note.gpg':
sshyp /notes/test\ note add -n sshyp /notes/test\ note add -n
Manually syncing entries with the server: Manually synchronizing entries with the server:
sshyp sync sshyp sync
.SH ARGUMENTS (CLIENT) .SH ARGUMENTS (CLIENT)
help/-h bring up the help menu help/-h bring up the help menu
+3 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from hashlib import sha512 from hashlib import sha512
from subprocess import PIPE, run from subprocess import DEVNULL, PIPE, run
from sys import argv from sys import argv
from time import sleep from time import sleep
@@ -16,7 +16,7 @@ if argv[2] == 'wsl':
run(('powershell.exe', '-c', 'Set-Clipboard')) run(('powershell.exe', '-c', 'Set-Clipboard'))
elif argv[2] == 'wayland': elif argv[2] == 'wayland':
hash_paste.update(run('wl-paste', stdout=PIPE).stdout.strip()) hash_paste.update(run('wl-paste', stdout=PIPE, stderr=DEVNULL).stdout.strip())
if argv[1] == hash_paste.hexdigest(): if argv[1] == hash_paste.hexdigest():
run(('wl-copy', '-c')) run(('wl-copy', '-c'))
@@ -33,7 +33,7 @@ elif argv[2] == 'mac':
elif argv[2] == 'termux': elif argv[2] == 'termux':
hash_paste.update(run('termux-clipboard-get', stdout=PIPE).stdout.strip()) hash_paste.update(run('termux-clipboard-get', stdout=PIPE).stdout.strip())
if argv[1] == hash_paste.hexdigest(): if argv[1] == hash_paste.hexdigest():
run(("termux-clipboard-set", "''")) run(("termux-clipboard-set", ''))
elif argv[2] == 'x11': elif argv[2] == 'x11':
hash_paste.update(run(('xclip', '-o', '-sel', 'c'), stdout=PIPE).stdout.strip()) hash_paste.update(run(('xclip', '-o', '-sel', 'c'), stdout=PIPE).stdout.strip())
+7 -7
View File
@@ -8,12 +8,12 @@ home = expanduser('~')
# PORT START SSHYNC-REMOTE # PORT START SSHYNC-REMOTE
# REMOTE # REMOTE
# prints all necessary remote data to stdout # prints all necessary remote data to stdout
def remote_list_gen(_client_device_name, _remote_dir): def remote_list_gen(_client_device_id, _remote_dir):
# deletions # deletions
for _file in listdir(f"{home}/.config/sshyp/deleted"): for _file in listdir(f"{home}/.config/sshyp/deleted"):
_file_path, _sep, _device = _file.partition('\x1f') _file_path, _sep, _device = _file.partition('\x1f')
_file_path = _file_path.replace('\x1e', '/') _file_path = _file_path.replace('\x1e', '/')
if _device == _client_device_name: if _device == _client_device_id:
print(_file_path) print(_file_path)
try: try:
remove(f"{home}/.config/sshyp/deleted/{_file}") remove(f"{home}/.config/sshyp/deleted/{_file}")
@@ -31,7 +31,7 @@ def remote_list_gen(_client_device_name, _remote_dir):
# HYBRID # HYBRID
# deletes a file or folder and/or marks it for deletion upon syncing # deletes a file or folder and/or marks it for deletion upon synchronizing
def delete(_file_path, _target_database, _silent): def delete(_file_path, _target_database, _silent):
from shutil import rmtree from shutil import rmtree
_directory = f"{home}/.local/share/sshyp/" _directory = f"{home}/.local/share/sshyp/"
@@ -44,8 +44,8 @@ def delete(_file_path, _target_database, _silent):
if not _silent: if not _silent:
print(f"location does not exist {_target_database}") print(f"location does not exist {_target_database}")
if _target_database == 'remotely': if _target_database == 'remotely':
for _device_name in listdir(f"{home}/.config/sshyp/devices"): for _device_id in listdir(f"{home}/.config/sshyp/devices"):
open(f"{home}/.config/sshyp/deleted/" + _file_path.replace('/', '\x1e') + '\x1f' + _device_name, 'w') open(f"{home}/.config/sshyp/deleted/" + _file_path.replace('/', '\x1e') + '\x1f' + _device_id, 'w')
# retrieves and returns titles and mod times from the local device # retrieves and returns titles and mod times from the local device
@@ -78,8 +78,8 @@ def remote_list_fetch(_user_data):
_deletion_database = _remote_data[0].strip().splitlines() _deletion_database = _remote_data[0].strip().splitlines()
_folder_database = _remote_data[1].strip().splitlines() _folder_database = _remote_data[1].strip().splitlines()
_titles_mods = _remote_data[2].strip().splitlines() _titles_mods = _remote_data[2].strip().splitlines()
return _deletion_database, _folder_database, _titles_mods[:len(_titles_mods)//2], \ return _deletion_database, _folder_database, _titles_mods[:len(_titles_mods) // 2], \
_titles_mods[len(_titles_mods)//2:] _titles_mods[len(_titles_mods) // 2:]
# checks for and acts upon files and folders marked for deletion # checks for and acts upon files and folders marked for deletion
+111 -116
View File
@@ -18,29 +18,32 @@ home = expanduser('~')
# generates and prints full entry list # generates and prints full entry list
def entry_list_gen(_directory=f"{home}/.local/share/sshyp/"): def entry_list_gen(_directory=f"{home}/.local/share/sshyp/"):
from shutil import get_terminal_size from shutil import get_terminal_size
from textwrap import fill _ran, _width = False, get_terminal_size().columns
_ran = False print("\nfor a list of usable commands, run 'sshyp help'\n\n\u001b[38;5;0;48;5;15msshyp entries:\u001b[0m", end='')
print("\nfor a list of usable commands, run 'sshyp help'\n\n\u001b[38;5;0;48;5;15msshyp entries:\u001b[0m\n")
for _root, _dirs, _files in sorted(walk(_directory, topdown=True)): for _root, _dirs, _files in sorted(walk(_directory, topdown=True)):
_entry_list, _color_alternator = [], 1 _color_alternator = 1
if _ran: if _ran:
print(f"\u001b[38;5;15;48;5;238m{_root.replace(f'{home}/.local/share/sshyp', '', 1)}/\u001b[0m") print(f"\n\n\u001b[38;5;7;48;5;8m{_root.replace(f'{home}/.local/share/sshyp', '', 1)}/\u001b[0m")
for filename in sorted(_files): elif _root == f"{home}/.local/share/sshyp/" and len(_files) > 0:
print("\n\n\u001b[38;5;7;48;5;8m/\u001b[0m")
_char_counter = 0
for _filename in sorted(_files):
if _color_alternator > 0: if _color_alternator > 0:
_entry_list.append(filename[:-4]) _print_string = _filename[:-4]
else: else:
_entry_list.append(f"\u001b[38;5;8m{filename[:-4]}\u001b[0m") _print_string = f"\u001b[38;5;8m{_filename[:-4]}\u001b[0m"
# -3 instead of -4 to account for trailing space character
_char_counter += len(_filename) - 3
if _char_counter >= _width:
# reset _char_counter to length of first entry in new line
_char_counter = len(_filename) - 3
print()
print(_print_string + ' ', end='')
_color_alternator = _color_alternator * -1 _color_alternator = _color_alternator * -1
_real = len(' '.join(_entry_list)) - (5.5 * len(_entry_list)) if _ran and _char_counter < 1:
if _real <= get_terminal_size()[0]: print('\u001b[38;5;9m-empty directory-\u001b[0m', end='')
_width = len(' '.join(_entry_list))
else:
_width = (len(' '.join(_entry_list)) / (_real / get_terminal_size()[0]) - 25)
if len(_entry_list) > 0:
print(fill(' '.join(_entry_list), width=_width) + '\n')
elif _ran:
print('\u001b[38;5;9m-empty directory-\u001b[0m\n')
_ran = True _ran = True
print('\n')
# displays the contents of an entry in a readable format # displays the contents of an entry in a readable format
@@ -49,29 +52,29 @@ def entry_reader(_decrypted_entry):
if pass_show: if pass_show:
_entry_password = f'\u001b[38;5;10m{_decrypted_entry[0]}\u001b[0m' _entry_password = f'\u001b[38;5;10m{_decrypted_entry[0]}\u001b[0m'
else: else:
_entry_password = f'\u001b[38;5;3mend command in "--show" or "-s" to view\u001b[0m' _entry_password = '\u001b[38;5;3mend command in "--show" or "-s" to view\u001b[0m'
print() print()
for _num in range(len(_decrypted_entry)): for _num in range(len(_decrypted_entry)):
try: try:
if _num == 0 and _decrypted_entry[1] != '': if _num == 0 and _decrypted_entry[1] != '':
print(f"\u001b[38;5;15;48;5;238musername:\u001b[0m\n{_decrypted_entry[1]}\n") print(f"\u001b[38;5;7;48;5;8musername:\u001b[0m\n{_decrypted_entry[1]}\n")
elif _num == 1 and _decrypted_entry[0] != '': elif _num == 1 and _decrypted_entry[0] != '':
print(f"\u001b[38;5;15;48;5;238mpassword:\u001b[0m\n{_entry_password}\n") print(f"\u001b[38;5;7;48;5;8mpassword:\u001b[0m\n{_entry_password}\n")
elif _num == 2 and _decrypted_entry[2] != '': elif _num == 2 and _decrypted_entry[2] != '':
print(f"\u001b[38;5;15;48;5;238murl:\u001b[0m\n{_decrypted_entry[_num]}\n") print(f"\u001b[38;5;7;48;5;8murl:\u001b[0m\n{_decrypted_entry[_num]}\n")
elif _num >= 3 and _decrypted_entry[_num] != '' and _notes_flag != 1: elif _notes_flag == 1 and _num >= 3:
_notes_flag = 1
print('\u001b[38;5;15;48;5;238mnotes:\u001b[0m\n' + _decrypted_entry[_num])
elif _num >= 3 and _notes_flag == 1:
print(_decrypted_entry[_num]) print(_decrypted_entry[_num])
elif _notes_flag != 1 and _num >= 3 and _decrypted_entry[_num] != '':
_notes_flag = 1
print('\u001b[38;5;7;48;5;8mnotes:\u001b[0m\n' + _decrypted_entry[_num])
if _notes_flag == 1: if _notes_flag == 1:
try: try:
_line_test = _decrypted_entry[_num + 1] _ = _decrypted_entry[_num + 1]
except IndexError: except IndexError:
print() print()
except IndexError: except IndexError:
if _num == 0 and _decrypted_entry[0] != '': if _num == 0 and _decrypted_entry[0] != '':
print(f"\u001b[38;5;15;48;5;238mpassword:\u001b[0m\n{_entry_password}\n") print(f"\u001b[38;5;7;48;5;8mpassword:\u001b[0m\n{_entry_password}\n")
# generates and returns a random string based on input # generates and returns a random string based on input
@@ -81,7 +84,7 @@ def string_gen(_complexity, _length):
if _complexity == 's': if _complexity == 's':
_character_pool = string.ascii_letters + string.digits _character_pool = string.ascii_letters + string.digits
elif _complexity == 'f': elif _complexity == 'f':
_character_pool = string.digits + string.ascii_letters + string.punctuation.replace('/', '').replace('\\', '')\ _character_pool = string.digits + string.ascii_letters + string.punctuation.replace('/', '').replace('\\', '') \
.replace("'", '').replace('"', '').replace('`', '').replace('~', '') .replace("'", '').replace('"', '').replace('`', '').replace('~', '')
else: else:
_character_pool = string.digits + string.ascii_letters + string.punctuation _character_pool = string.digits + string.ascii_letters + string.punctuation
@@ -135,17 +138,15 @@ def edit_note(_note_lines, _exit_on_match=False):
return _new_note return _new_note
# encrypts an entry and cleans up the temporary files # encrypts an entry
def encrypt(_entry_data, _entry_dir, _gpg_id): def encrypt(_entry_data, _entry_dir, _gpg_id):
_bytes_data = '\n'.join(_entry_data).rstrip().encode() _bytes_data = '\n'.join(_entry_data).rstrip().encode()
_encrypted_data = run(('gpg', '-qr', str(_gpg_id), '-e'), input=_bytes_data, stdout=PIPE).stdout _encrypted_data = run(('gpg', '-qr', str(_gpg_id), '-e'), input=_bytes_data, stdout=PIPE).stdout
open(_entry_dir + '.gpg', 'wb').write(_encrypted_data) open(_entry_dir + '.gpg', 'wb').write(_encrypted_data)
# decrypts an entry to a temporary directory # decrypts an entry and returns its contents
def decrypt(_entry_dir, _quick_verify=None, _quick_pass=None): def decrypt(_entry_dir, _quick_verify=None, _quick_pass=None):
_contents = None
# check quick-unlock status, fetch passphrase # check quick-unlock status, fetch passphrase
if _quick_verify: if _quick_verify:
_quick_pass = whitelist_verify(port, username_ssh, ip, client_device_id, identity) _quick_pass = whitelist_verify(port, username_ssh, ip, client_device_id, identity)
@@ -192,8 +193,9 @@ def whitelist_verify(_port, _username_ssh, _ip, _client_device_id, _identity):
except CalledProcessError: except CalledProcessError:
_i, _full_password = 0, '' _i, _full_password = 0, ''
_server_whitelist = run(('ssh', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}", _server_whitelist = run(('ssh', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
f'python3 -c \'from os import listdir; print(*listdir("/home/{_username_ssh}' 'python3 -c \'from os import listdir; [print(_id, end="\x1f") for _id in '
f'/.config/sshyp/whitelist"))\''), stdout=PIPE, text=True).stdout.rstrip().split() f'listdir("/home/{_username_ssh}/.config/sshyp/whitelist")]\''),
stdout=PIPE, text=True).stdout.rstrip().split('\x1f')
for _device_id in _server_whitelist: for _device_id in _server_whitelist:
if _device_id == _client_device_id: if _device_id == _client_device_id:
from getpass import getpass from getpass import getpass
@@ -249,17 +251,18 @@ def target_type_check(_target_name, _expected_type=True, _error=False):
s_exit(2) s_exit(2)
# ensures an edited entry is optimized for best compatibility # ensures an entry has enough lines to complete an edit and optionally carries out the edit
def line_edit(_lines, _edit_data, _edit_line): def line_edit(_lines, _edit_line, _edit_data=None):
# ensure enough lines are present for edited field # ensure enough lines are present for edited field
while len(_lines) < _edit_line + 1: while len(_lines) < _edit_line + 1:
_lines.append('') _lines.append('')
if _edit_data is not None:
# write the edited field # write the edited field
_lines[_edit_line] = _edit_data.rstrip() _lines[_edit_line] = _edit_data.rstrip()
return _lines return _lines
# attempts to connect to the user's server via ssh to register the device for syncing # attempts to connect to the user's server via ssh to register the device for synchronization
def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshyp_data): def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshyp_data):
from stweak import write_config from stweak import write_config
if not _sshyp_data.has_section('CLIENT-ONLINE'): if not _sshyp_data.has_section('CLIENT-ONLINE'):
@@ -270,8 +273,8 @@ def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshy
f'{_client_device_id}").touch(mode=0o400, exist_ok=True)\''), stderr=DEVNULL, check=True) f'{_client_device_id}").touch(mode=0o400, exist_ok=True)\''), stderr=DEVNULL, check=True)
except CalledProcessError: except CalledProcessError:
print(f'\n\u001b[38;5;9mwarning: ssh connection could not be made - ensure the public key ({_identity}) is ' print(f'\n\u001b[38;5;9mwarning: ssh connection could not be made - ensure the public key ({_identity}) is '
'registered on the remote server and that the entered ip, port, and username are correct\n\nsyncing ' 'registered on the remote server and that the entered ip, port, and username are correct\n\n'
'functionality will be disabled until this is addressed\u001b[0m\n') 'synchronization functionality will be disabled until this is addressed\u001b[0m\n')
_sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'true') _sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'true')
write_config(_sshyp_data) write_config(_sshyp_data)
return True return True
@@ -285,101 +288,101 @@ def copy_id_check(_port, _username_ssh, _ip, _client_device_id, _identity, _sshy
# prints help text based on argument # prints help text based on argument
def print_info(): def print_info():
if arguments[0] in ('version', '-v'): if arguments[0] in ('version', '-v'):
_blank = '\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m' + 55*' ' + '\u001b[38;5;7;48;5;8m/\u001b[0m' _blank = '\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m' + 55 * ' ' + '\u001b[38;5;7;48;5;8m/\u001b[0m'
_border = '\u001b[38;5;7;48;5;8m' + 14*'<>' + '-' + 14*'<>' + '\u001b[0m\n' _border = '\u001b[38;5;7;48;5;8m' + 14 * '<>' + '-' + 14 * '<>' + '\u001b[0m\n'
print(f"""\nsshyp is a simple, self-hosted, sftp-synchronized\npassword manager for unix(-like) systems\n print(f"""\nsshyp is a simple, self-hosted, sftp-synchronized\npassword manager for unix(-like) systems\n
{9*' '}..{15*' '}\u001b[38;5;12m♥♥ \u001b[38;5;9m♥♥\u001b[0m{15*' '}.. {9 * ' '}..{15 * ' '}\u001b[38;5;12m♥♥ \u001b[38;5;9m♥♥\u001b[0m{15 * ' '}..
{8*' '}/()\\''.''.{7*' '}\u001b[38;5;12m♥♥♥\u001b[0m♥♥♥♥\u001b[0m{7*' '}.''.''/()\\{3*' '}_) {8 * ' '}/()\\''.''.{7 * ' '}\u001b[38;5;12m♥♥♥\u001b[0m♥♥♥♥\u001b[0m{7 * ' '}.''.''/()\\{3 * ' '}_)
{5*' '}_.{3*' '}:{7*' '}*{7*' '}\u001b[38;5;9m♥♥♥♥♥\u001b[0m{7*' '}*{7*' '}:{3*' '}<[◎]|_|= {5 * ' '}_.{3 * ' '}:{7 * ' '}*{7 * ' '}\u001b[38;5;9m♥♥♥♥♥\u001b[0m{7 * ' '}*{7 * ' '}:{3 * ' '}<[◎]|_|=
}}-}}-*]{4*' '}`..'..'{9*' '}\u001b[0m♥♥♥\u001b[0m{9*' '}`..'..'{6*' '}| }}-}}-*]{4 * ' '}`..'..'{9 * ' '}\u001b[0m♥♥♥\u001b[0m{9 * ' '}`..'..'{6 * ' '}|
{4*' '}◎-◎{4*' '}//{3*' '}\\\\{10*' '}\u001b[38;5;9m♥\u001b[0m{10*' '}//{3*' '}\\\\{5*' '}/|\\""") {4 * ' '}◎-◎{4 * ' '}//{3 * ' '}\\\\{10 * ' '}\u001b[38;5;9m♥\u001b[0m{10 * ' '}//{3 * ' '}\\\\{5 * ' '}/|\\""")
print(f"{_border}{_blank}\n\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{18*' '}\u001b[38;5;15;48;5;8msshyp " print(f"{_border}{_blank}\n\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{18 * ' '}\u001b[38;5;15;48;5;8msshyp "
f"version 1.5.1\u001b[38;5;15;48;5;15m{18*' '}\u001b[38;5;7;48;5;8m/\u001b[0m") f"version 1.5.3\u001b[38;5;15;48;5;15m{18 * ' '}\u001b[38;5;7;48;5;8m/\u001b[0m")
print(f"\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{14*' '}\u001b[38;5;15;48;5;8mthe fortified flock" print(f"\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{14 * ' '}\u001b[38;5;15;48;5;8mthe fortified flock"
f" update\u001b[38;5;15;48;5;15m{15*' '}\u001b[38;5;7;48;5;8m/\u001b[0m\n{_blank}") f" update\u001b[38;5;15;48;5;15m{15 * ' '}\u001b[38;5;7;48;5;8m/\u001b[0m\n{_blank}")
print(f"\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{9*' '}\u001b[38;5;15;48;5;8mcopyright 2021-2023 ", print(f"\u001b[38;5;7;48;5;8m\\\u001b[38;5;15;48;5;15m{9 * ' '}\u001b[38;5;15;48;5;8mcopyright 2021-2024 ",
f"randall winkhart\u001b[38;5;15;48;5;15m{9*' '}\u001b[38;5;7;48;5;8m/\u001b[0m") f"randall winkhart\u001b[38;5;15;48;5;15m{9 * ' '}\u001b[38;5;7;48;5;8m/\u001b[0m")
print(f"{_blank}\n{_border}\nsee https://github.com/rwinkhart/sshyp for more information\n") print(f"{_blank}\n{_border}\nsee https://github.com/rwinkhart/sshyp for more information\n")
elif arguments[0] == 'license': elif arguments[0] == 'license':
print('\nThis program is free software: you can redistribute it and/or modify it under the terms\nof version 3 ' print('\nThis program is free software: you can redistribute it and/or modify it under the terms of\nversion 3 '
'(only) of the GNU General Public License as published by the Free Software Foundation.\n\nThis program ' '(only) of the GNU General Public License as published by the Free Software Foundation.\n\nThis program '
'is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\nwithout even the implied ' 'is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\nwithout even the implied '
'warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\nSee the GNU General Public License for' 'warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\nSee the GNU General Public License '
' more details.\n\nhttps://opensource.org/licenses/GPL-3.0\n') 'for more details:\nhttps://opensource.org/licenses/GPL-3.0\n')
elif arguments[0] == 'add' and device_type == 'client': elif arguments[0] == 'add' and device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> add <option>\u001b[0m\n print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> add <option>\u001b[0m\n
\u001b[1moptions:\u001b[0m \u001b[1moptions:\u001b[0m
add: add:
password/-p{12*' '}add a password entry password/-p{12 * ' '}add a password entry
note/-n{16*' '}add a note entry note/-n{16 * ' '}add a note entry
folder/-f{14*' '}add a new folder for entries\n""") folder/-f{14 * ' '}add a new folder for entries\n""")
elif arguments[0] == 'edit' and device_type == 'client': elif arguments[0] == 'edit' and device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> edit <option>\u001b[0m\n print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> edit <option>\u001b[0m\n
\u001b[1moptions:\u001b[0m \u001b[1moptions:\u001b[0m
edit: edit:
rename/relocate/-r{5*' '}rename or relocate an entry rename/relocate/-r{5 * ' '}rename or relocate an entry
username/-u{12*' '}change the username of an entry username/-u{12 * ' '}change the username of an entry
password/-p{12*' '}change the password of an entry password/-p{12 * ' '}change the password of an entry
url/-l{17*' '}change the url attached to an entry url/-l{17 * ' '}change the url attached to an entry
note/-n{16*' '}change the note attached to an entry\n""") note/-n{16 * ' '}change the note attached to an entry\n""")
elif arguments[0] == 'copy' and device_type == 'client': elif arguments[0] == 'copy' and device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> copy <option>\u001b[0m\n print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> copy <option>\u001b[0m\n
\u001b[1moptions:\u001b[0m \u001b[1moptions:\u001b[0m
copy: copy:
username/-u{12*' '}copy the username of an entry to your clipboard username/-u{12 * ' '}copy the username of an entry to your clipboard
password/-p{12*' '}copy the password of an entry to your clipboard password/-p{12 * ' '}copy the password of an entry to your clipboard
url/-l{17*' '}copy the url of an entry to your clipboard url/-l{17 * ' '}copy the url of an entry to your clipboard
note/-n{16*' '}copy the note of an entry to your clipboard\n""") note/-n{16 * ' '}copy the note of an entry to your clipboard\n""")
elif arguments[0] == 'gen' and device_type == 'client': elif arguments[0] == 'gen' and device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> gen [option]\u001b[0m\n print(f"""\n\u001b[1musage:\u001b[0m sshyp /<entry name> gen [option]\u001b[0m\n
\u001b[1moptions:\u001b[0m \u001b[1moptions:\u001b[0m
gen: gen:
update/-u{14*' '}generate a password for an existing entry\n""") update/-u{14 * ' '}generate a password for an existing entry\n""")
else: else:
print("\n\u001b[1msshyp ", "copyright (c) 2021-2023 ", """randall winkhart\u001b[0m print("\n\u001b[1msshyp ", "copyright (c) 2021-2024 ", """randall winkhart\u001b[0m
this is free software, and you are welcome to redistribute it under certain conditions; this is free software, and you are welcome to redistribute it under certain conditions;
this program comes with absolutely no warranty; type 'sshyp license' for details""") this program comes with absolutely no warranty; type 'sshyp license' for details""")
if device_type == 'client': if device_type == 'client':
print(f"""\n\u001b[1musage:\u001b[0m sshyp [/<entry name> [argument] [option]] | [argument]\n print(f"""\n\u001b[1musage:\u001b[0m sshyp [/<entry name> [argument] [option]] | [argument]\n
\u001b[1marguments:\u001b[0m \u001b[1marguments:\u001b[0m
help/-h{17*' '}bring up this menu help/-h{17 * ' '}bring up this menu
version/-v{14*' '}display sshyp version info version/-v{14 * ' '}display sshyp version info
init{20*' '}set up sshyp init{20 * ' '}set up sshyp
tweak{19*' '}change configuration options/manage extensions and updates tweak{19 * ' '}change configuration options/manage extensions and updates
add{21*' '}add an entry add{21 * ' '}add an entry
gen{21*' '}generate a new password gen{21 * ' '}generate a new password
edit{20*' '}edit an existing entry edit{20 * ' '}edit an existing entry
copy{20*' '}copy details of an entry to your clipboard copy{20 * ' '}copy details of an entry to your clipboard
shear{19*' '}delete an existing entry shear{19 * ' '}delete an existing entry
sync{20*' '}manually sync the entry directory via sshync sync{20 * ' '}manually sync the entry directory via sshync
\n\u001b[1moptions:\u001b[0m \n\u001b[1moptions:\u001b[0m
add: add:
password/-p{12*' '}add a password entry password/-p{12 * ' '}add a password entry
note/-n{16*' '}add a note entry note/-n{16 * ' '}add a note entry
folder/-f{14*' '}add a new folder for entries folder/-f{14 * ' '}add a new folder for entries
edit: edit:
rename/relocate/-r{5*' '}rename or relocate an entry rename/relocate/-r{5 * ' '}rename or relocate an entry
username/-u{12*' '}change the username of an entry username/-u{12 * ' '}change the username of an entry
password/-p{12*' '}change the password of an entry password/-p{12 * ' '}change the password of an entry
url/-l{17*' '}change the url attached to an entry url/-l{17 * ' '}change the url attached to an entry
note/-n{16*' '}change the note attached to an entry note/-n{16 * ' '}change the note attached to an entry
copy: copy:
username/-u{12*' '}copy the username of an entry to your clipboard username/-u{12 * ' '}copy the username of an entry to your clipboard
password/-p{12*' '}copy the password of an entry to your clipboard password/-p{12 * ' '}copy the password of an entry to your clipboard
url/-l{17*' '}copy the url of an entry to your clipboard url/-l{17 * ' '}copy the url of an entry to your clipboard
note/-n{16*' '}copy the note of an entry to your clipboard note/-n{16 * ' '}copy the note of an entry to your clipboard
gen: gen:
update/-u{14*' '}generate a password for an existing entry update/-u{14 * ' '}generate a password for an existing entry
\n\u001b[1mtip 1:\u001b[0m you can quickly read an entry with 'sshyp /<entry name>' \n\u001b[1mtip 1:\u001b[0m you can quickly read an entry with 'sshyp /<entry name>'
\u001b[1mtip 2:\u001b[0m type 'sshyp' to view a list of saved entries\n""") \u001b[1mtip 2:\u001b[0m type 'sshyp' to view a list of saved entries\n""")
# PORT START HELP-SERVER # PORT START HELP-SERVER
else: else:
print(f"""\n\u001b[1musage:\u001b[0m sshyp <argument>\n print(f"""\n\u001b[1musage:\u001b[0m sshyp <argument>\n
\u001b[1marguments:\u001b[0m \u001b[1marguments:\u001b[0m
help/-h{17*' '}bring up this menu help/-h{17 * ' '}bring up this menu
version/-v{14*' '}display sshyp version info version/-v{14 * ' '}display sshyp version info
init{20*' '}set up sshyp init{20 * ' '}set up sshyp
tweak{19*' '}change configuration options/manage extensions and updates\n""") tweak{19 * ' '}change configuration options/manage extensions and updates\n""")
# PORT END HELP-SERVER # PORT END HELP-SERVER
@@ -391,7 +394,7 @@ def read_shortcut():
# calls sshync to sync changes to the user's server # calls sshync to sync changes to the user's server
def sync(_start_text=''): def sync(_start_text=''):
print(f"{_start_text}syncing entries with the server device...\n") print(f"{_start_text}synchronizing entries with the server device...\n")
# set permissions before uploading # set permissions before uploading
for _root, _dirs, _files in walk(f"{home}/.local/share/sshyp"): for _root, _dirs, _files in walk(f"{home}/.local/share/sshyp"):
for _path in _root.splitlines(): for _path in _root.splitlines():
@@ -420,7 +423,7 @@ def add_entry():
else: else:
_note = '' _note = ''
print('\n\u001b[1mentry preview:\u001b[0m') print('\n\u001b[1mentry preview:\u001b[0m')
entry_reader([_password, _username, _url, _note]) entry_reader([_password, _username, _url] + _note.split('\n'))
encrypt([_password, _username, _url, _note], directory + entry_name, gpg_id) encrypt([_password, _username, _url, _note], directory + entry_name, gpg_id)
@@ -469,9 +472,6 @@ def rename():
# edits the contents of an entry # edits the contents of an entry
def edit(): def edit():
# set to avoid PEP8 warnings
_detail, _edit_line = None, None
# ensure the edit target is an entry # ensure the edit target is an entry
target_type_check(entry_name, True, True) target_type_check(entry_name, True, True)
@@ -484,10 +484,10 @@ def edit():
_detail, _edit_line = str(input('\nurl: ')), 2 _detail, _edit_line = str(input('\nurl: ')), 2
if arguments[2] in ('note', '-n'): if arguments[2] in ('note', '-n'):
_old_lines = decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled) _old_lines = decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled)
_new_lines = _old_lines[0:3] + edit_note(_old_lines[3:], True).split('\n') # pass 2 as _edit_line to simulate a full entry minus notes so that notes are appended correctly
_new_lines = line_edit(_old_lines[0:3], 2) + edit_note(_old_lines[3:], True).split('\n')
else: else:
_new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), _detail, _new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), _edit_line, _detail)
_edit_line)
print('\n\u001b[1mentry preview:\u001b[0m') print('\n\u001b[1mentry preview:\u001b[0m')
entry_reader(_new_lines) entry_reader(_new_lines)
encrypt(_new_lines, directory + entry_name, gpg_id) encrypt(_new_lines, directory + entry_name, gpg_id)
@@ -495,13 +495,11 @@ def edit():
# generates a password for a new or an existing entry # generates a password for a new or an existing entry
def gen(): def gen():
# set to avoid PEP8 warnings
_username, _url, _notes = None, None, None
# gen update # gen update
if arg_count == 3 and arguments[2] in ('update', '-u'): if arg_count == 3 and arguments[2] in ('update', '-u'):
# ensure the gen update target is an entry # ensure the gen update target is an entry
target_type_check(entry_name, True, True) target_type_check(entry_name, True, True)
_new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), pass_gen(), 0) _new_lines = line_edit(decrypt(directory + entry_name, _quick_verify=quick_unlock_enabled), 0, pass_gen())
# gen # gen
else: else:
# make sure the gen target does not already exist # make sure the gen target does not already exist
@@ -513,7 +511,7 @@ def gen():
_note = edit_note([]) _note = edit_note([])
else: else:
_note = '' _note = ''
_new_lines = [_password, _username, _url, _note] _new_lines = [_password, _username, _url] + _note.split('\n')
print('\n\u001b[1mentry preview:\u001b[0m') print('\n\u001b[1mentry preview:\u001b[0m')
entry_reader(_new_lines) entry_reader(_new_lines)
encrypt(_new_lines, directory + entry_name, gpg_id) encrypt(_new_lines, directory + entry_name, gpg_id)
@@ -612,9 +610,6 @@ if __name__ == "__main__":
# set default states # set default states
ssh_error, success_flag, sync_flag, silent_sync, pass_show = False, False, False, False, False ssh_error, success_flag, sync_flag, silent_sync, pass_show = False, False, False, False, False
# set to avoid PEP8 warnings
arg_start, device_type, offline_mode_enabled = None, None, None
# retrieve typed argument # retrieve typed argument
arguments = argv[1:] arguments = argv[1:]
arg_count = len(arguments) arg_count = len(arguments)
@@ -625,8 +620,8 @@ if __name__ == "__main__":
arg_start = 1 arg_start = 1
entry_name = arguments[0].strip('/') entry_name = arguments[0].strip('/')
# determine whether to show passwords in entry previews # determine whether to show passwords in entry previews
if arg_count > 1 and arguments[arg_count-1] in ('--show', '-s'): if arg_count > 1 and arguments[arg_count - 1] in ('--show', '-s'):
arguments.pop() del arguments[-1]
arg_count -= 1 arg_count -= 1
pass_show = True pass_show = True
else: else:
@@ -657,13 +652,13 @@ if __name__ == "__main__":
if ssh_error: if ssh_error:
ssh_error = copy_id_check(port, username_ssh, ip, client_device_id, identity, sshyp_data) ssh_error = copy_id_check(port, username_ssh, ip, client_device_id, identity, sshyp_data)
except (FileNotFoundError, NoSectionError, NoOptionError): except (FileNotFoundError, NoSectionError, NoOptionError):
print(f"\n{73*'!'}") print(f"\n{73 * '!'}")
print("not all necessary configurations have been made - please run 'sshyp init'") print("not all necessary configurations have been made - please run 'sshyp init'")
print(f"{73*'!'}\n") print(f"{73 * '!'}\n")
s_exit(1) s_exit(1)
else: else:
from stweak import wrapped_entry from stweak import wrapped_entry
wrapped_entry(False) wrapped_entry(False, 'additional configuration options:')
s_exit() s_exit()
# run function based on arguments # run function based on arguments
+204 -86
View File
@@ -5,12 +5,12 @@ from os import environ, listdir, remove
from os.path import exists, expanduser, isfile from os.path import exists, expanduser, isfile
from pathlib import Path from pathlib import Path
from random import randint from random import randint
from shutil import get_terminal_size, which from shutil import which
from subprocess import PIPE, run from subprocess import CalledProcessError, DEVNULL, PIPE, run
# PORT START UNAME-IMPORT-STWEAK # PORT START UNAME-IMPORT-STWEAK
from os import uname from os import uname
# PORT END UNAME-IMPORT-STWEAK # PORT END UNAME-IMPORT-STWEAK
home, sshyp_data, stdscr = expanduser('~'), ConfigParser(interpolation=None), None home, sshyp_data, stdscr, gm_device_type = expanduser('~'), ConfigParser(interpolation=None), None, None
if isfile(f"{home}/.config/sshyp/sshyp.ini"): if isfile(f"{home}/.config/sshyp/sshyp.ini"):
_exists_flag = True _exists_flag = True
sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini") sshyp_data.read(f"{home}/.config/sshyp/sshyp.ini")
@@ -28,25 +28,62 @@ def write_config(_sshyp_data=sshyp_data):
def curses_radio(_options, _pretext): def curses_radio(_options, _pretext):
curs_set(0) curs_set(0)
_selected = 0 _selected = 0
while True: while True:
# clear curses window
stdscr.clear() stdscr.clear()
stdscr.addstr(0, 0, _pretext) # get terminal size
_height, _width = stdscr.getmaxyx()
# track whether to generate interactive buttons (depending on terminal size)
_button_gen = False
# split text based on new lines
_pretext_lines = _pretext.split('\n')
# iterate through lines, splitting further (wrapping) as needed, to add to curses window
_current_line = 0
for _line in _pretext_lines:
if _current_line <= _height - 4:
_button_gen = True
while len(_line) > _width:
stdscr.addstr(_current_line, 0, _line[:_width])
_line = _line[_width:]
_current_line += 1
stdscr.addstr(_current_line, 0, _line)
_current_line += 1
else:
_button_gen = False
stdscr.addstr(_current_line, 0, '# warning: re-size terminal to see more information'[:_width - 1])
_current_line += 1
break
# create user-interactive options if all information has been displayed to the user
if _button_gen:
for _i, _option in enumerate(_options): for _i, _option in enumerate(_options):
_y = _i + _pretext.count('\n') + 2 _y = _i + _current_line + 1
if _y < _height:
if _i == _selected: if _i == _selected:
stdscr.addstr(_y, 0, "[*] " + _option, A_REVERSE) stdscr.addstr(_y, 0, "[*] " + _option, A_REVERSE)
else: else:
stdscr.addstr(_y, 0, "[ ] " + _option) stdscr.addstr(_y, 0, "[ ] " + _option)
stdscr.refresh() else:
_key = stdscr.getch() stdscr.addstr(_y - 1, 0, '# warning: re-size terminal to see more information')
# 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 break
# update _selected based on user input
_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() stdscr.refresh()
curs_set(1) curs_set(1)
return _selected return _selected
@@ -55,9 +92,9 @@ def curses_radio(_options, _pretext):
def curses_text(_pretext): def curses_text(_pretext):
stdscr.clear() stdscr.clear()
stdscr.addstr(0, 0, _pretext) stdscr.addstr(0, 0, _pretext)
_term_columns = get_terminal_size()[0] _width = stdscr.getmaxyx()[1]
_editwin = newwin(1, _term_columns-2, 3, 1) _editwin = newwin(1, _width - 2, 3, 1)
rectangle(stdscr, 2, 0, 4, _term_columns-1) rectangle(stdscr, 2, 0, 4, _width - 1)
stdscr.refresh() stdscr.refresh()
_box = Textbox(_editwin) _box = Textbox(_editwin)
# let the user edit until ctrl+g/enter is struck # let the user edit until ctrl+g/enter is struck
@@ -102,13 +139,22 @@ def gpg_config():
_named_uid_list.append(_uid.split(':')[9].replace('\\x3a', ':').replace('\\x5c', '\\')) _named_uid_list.append(_uid.split(':')[9].replace('\\x3a', ':').replace('\\x5c', '\\'))
_named_uid_list.append('auto-generate') _named_uid_list.append('auto-generate')
_gpg_id_sel = curses_radio(_named_uid_list, 'gpg key selection') _gpg_id_sel = curses_radio(_named_uid_list, 'gpg key selection')
if _gpg_id_sel == len(_named_uid_list)-1: if _gpg_id_sel == len(_named_uid_list) - 1:
if not isfile(f"{home}/.config/sshyp/gpg-gen"): if not isfile(f"{home}/.config/sshyp/gpg-gen"):
open(f"{home}/.config/sshyp/gpg-gen", 'w').writelines([ open(f"{home}/.config/sshyp/gpg-gen", 'w').writelines([
'Key-Type: 1\n', 'Key-Length: 4096\n', 'Key-Usage: sign encrypt\n', 'Name-Real: sshyp\n', 'Key-Type: eddsa\n', 'Key-Curve: ed25519\n', 'Key-Usage: sign\n', 'Subkey-Type: ecdh\n',
'Name-Comment: gpg-sshyp\n', 'Name-Email: github.com/rwinkhart/sshyp\n', 'Subkey-Curve: cv25519\n', 'Subkey-Usage: encrypt\n', 'Name-Real: sshyp\n',
'Expire-Date: 0']) '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") remove(f"{home}/.config/sshyp/gpg-gen")
_gpg_id = run(('gpg', '-k', '--with-colons'), stdout=PIPE, text=True).stdout.splitlines()[-1].split(':')[9] _gpg_id = run(('gpg', '-k', '--with-colons'), stdout=PIPE, text=True).stdout.splitlines()[-1].split(':')[9]
else: else:
@@ -145,7 +191,7 @@ def editor_config(_env_mode):
# ssh+sshync configuration # ssh+sshync configuration
def ssh_config(): def ssh_config(_reconfig=False):
# private key selection/generation # private key selection/generation
_keys = [] _keys = []
# ensure ~/.ssh directory exists # ensure ~/.ssh directory exists
@@ -155,14 +201,20 @@ def ssh_config():
and not _file.endswith('.pub') and isfile(f"{home}/.ssh/{_file}"): and not _file.endswith('.pub') and isfile(f"{home}/.ssh/{_file}"):
_keys.append(f"{home}/.ssh/{_file}") _keys.append(f"{home}/.ssh/{_file}")
_keys.extend(['auto-generate', 'other (type the location)']) _keys.extend(['auto-generate', 'other (type the location)'])
# append a back button if launched optionally
if _reconfig:
_keys.append('BACK')
_key_selected_num = curses_radio(_keys, 'which private ssh key would you like to use for sshyp?') _key_selected_num = curses_radio(_keys, 'which private ssh key would you like to use for sshyp?')
_gen_index = len(_keys)-2 if _reconfig and _key_selected_num == len(_keys) - 1:
return
_gen_index = len(_keys) - 2
if _key_selected_num >= _gen_index: if _key_selected_num >= _gen_index:
_ssh_key = expanduser(curses_text('enter the location for your private ssh key:\n\n\n\n\n(ctrl+g/enter to ' _ssh_key = expanduser(curses_text('enter the location for your private ssh key:\n\n\n\n\n(ctrl+g/enter to '
'confirm)\n\nexample input:\n\n~/.ssh/privkey')) 'confirm)\n\nexample input:\n\n~/.ssh/privkey'))
if _key_selected_num == _gen_index: if _key_selected_num == _gen_index:
_passphrase = curses_text('enter your desired ssh keyfile passphrase:\n\n\n\n\n(ctrl+g/enter to confirm)' _passphrase = curses_text('enter your desired ssh keyfile passphrase:\n\n\n\n\n(ctrl+g/enter to confirm)'
'\n\ntip: you can leave this blank to use the keyfile without a passphrase') '\n\nnote: it is recommended to leave this blank and to use the keyfile without '
'a passphrase (the keyfile itself serves as your identity)')
run(('ssh-keygen', '-q', '-t', 'ed25519', '-N', _passphrase, '-f', _ssh_key)) run(('ssh-keygen', '-q', '-t', 'ed25519', '-N', _passphrase, '-f', _ssh_key))
else: else:
_ssh_key = _keys[_key_selected_num] _ssh_key = _keys[_key_selected_num]
@@ -188,19 +240,41 @@ def ssh_config():
# device id configuration # device id configuration
def dev_id_config(_ip, _username_ssh, _port, _identity): def dev_id_config(_port, _username_ssh, _ip, _identity, _reconfig=False):
if _reconfig:
_sure = curses_radio(('no', 'yes'), "WARNING: ensure this sshyp client is synchronized (up-to-date) before "
"changing the device id\n\nfailure to do so may result in sync"
"hronization issues\n\nare you sure you wish to change the device id?")
if _sure != 1:
return
from sshyp import copy_id_check, string_gen from sshyp import copy_id_check, string_gen
_device_id_prefix = curses_text('name this device:\n\n\n\n\n(ctrl+g/enter to confirm)\n\nimportant: this ' _device_id_prefix = curses_text('set this device\'s id:\n\n\n\n\n(ctrl+g/enter to confirm)\n\nimportant: this '
'id must be unique amongst your client devices\n\nthis is used to keep track of ' 'id must be unique amongst your client devices\n\nthis is used to keep track of '
'database syncing and quick-unlock permissions\n') 'database synchronization and quick-unlock permissions\n')
_device_id_suffix = string_gen('f', randint(24, 48)) _device_id_suffix = string_gen('f', randint(24, 48))
_device_id = _device_id_prefix + '-' + _device_id_suffix _device_id = _device_id_prefix + '-' + _device_id_suffix
# remove existing device ids # remove existing device ids
for _id in listdir(f"{home}/.config/sshyp/devices"): _device_id_list = listdir(f"{home}/.config/sshyp/devices")
for _id in _device_id_list:
remove(f"{home}/.config/sshyp/devices/{_id}") remove(f"{home}/.config/sshyp/devices/{_id}")
open(f"{home}/.config/sshyp/devices/{_device_id}", 'w') open(f"{home}/.config/sshyp/devices/{_device_id}", 'w')
# test server connection and attempt to register device id # test server connection and attempt to register device id - only run if _reconfig is True, since the keyfile
copy_id_check(_ip, _username_ssh, _port, _device_id, _identity, sshyp_data) # needs to be registered with the server before a successful connection can be made
if _reconfig:
# copy_id_check() returns false if successful
if not copy_id_check(_port, _username_ssh, _ip, _device_id, _identity, sshyp_data):
# remove old device id from registered pool and whitelist
run(('ssh', '-o', 'ConnectTimeout=3', '-i', _identity, '-p', _port, f"{_username_ssh}@{_ip}",
'python3 -c \'from pathlib import Path; '
f'Path("/home/{_username_ssh}/.config/sshyp/devices/{_device_id_list[0]}").unlink(missing_ok=True); '
f'Path("/home/{_username_ssh}/.config/sshyp/whitelist/{_device_id_list[0]}")'
f'.unlink(missing_ok=True)\''), stderr=DEVNULL, stdout=DEVNULL)
else:
sshyp_data.set('CLIENT-ONLINE', 'ssh_error', 'true')
write_config()
curses_radio(['okay'], 'this device will be registered with the server upon the first successful sync\n\n'
'you can force this now by running "sshyp sync"\n\nif you have not done so already, '
'ensure your ssh pubkey has been appended to the server\'s authorized_keys file!')
# quick-unlock configuration # quick-unlock configuration
@@ -232,10 +306,10 @@ def refresh_encryption():
_directory = f"{home}/.local/share/sshyp" _directory = f"{home}/.local/share/sshyp"
# warn the user of potential data loss and prompt to continue # warn the user of potential data loss and prompt to continue
_proceed = curses_radio(('no', 'yes'), "WARNING: proceeding with this action will remove/overwrite any directories" _sure = curses_radio(('no', 'yes'), "WARNING: proceeding with this action will remove/overwrite any directories"
f" matching the following:\n\n{home}/.local/share/sshyp.old\n{home}/.local/" f" matching the following:\n\n{home}/.local/share/sshyp.old\n{home}/.local/"
"share/sshyp.new\n\nare you sure you wish to re-encrypt all entries?") "share/sshyp.new\n\nare you sure you wish to re-encrypt all entries?")
if _proceed != 1: if _sure != 1:
return 3 return 3
# set new gpg key # set new gpg key
@@ -248,8 +322,8 @@ def refresh_encryption():
# prompt for unlock and display do not close warning # prompt for unlock and display do not close warning
decrypt(None) decrypt(None)
curses_radio(['okay'], 'entry optimization may take some time - select "okay" to start - ' curses_radio(['okay'], 'entry optimization may take some time (especially on slower devices)\n\nselect "okay" '
'do not terminate this process!') 'to start\n\ndo not terminate this process!')
# remove existing conflicts # remove existing conflicts
for _extension in ('.new', '.old'): for _extension in ('.new', '.old'):
@@ -274,21 +348,43 @@ def refresh_encryption():
# PORT START WHITELIST-SERVER # PORT START WHITELIST-SERVER
# removes a registered device id from the server-side pool and prunes the quick-unlock whitelist
def registered_dev_id_remover(_back=False):
_device_ids = listdir(f"{home}/.config/sshyp/devices") + ['BACK']
_whitelisted_ids = listdir(f"{home}/.config/sshyp/whitelist")
while not _back:
_del_id = curses_radio(_device_ids, 'WARNING: only remove registered device ids if they are no longer in use'
'\n\nthe removal of an active device id will result in the malfunction of '
'the device still using the removed id\n\nregistered device id to remove:')
if _del_id == len(_device_ids) - 1:
_back = True
else:
# remove deleted device id from whitelist
if _device_ids[_del_id] in _whitelisted_ids:
remove(f"{home}/.config/sshyp/whitelist/{_whitelisted_ids[_del_id]}")
_whitelisted_ids = [_id for _id in _whitelisted_ids if _id != _device_ids[_del_id]]
# remove deleted device id from device pool
remove(f"{home}/.config/sshyp/devices/{_device_ids[_del_id]}")
del _device_ids[_del_id]
# takes input from the user to set up quick-unlock pin # takes input from the user to set up quick-unlock pin
def whitelist_setup(): def whitelist_setup():
_gpg_password_temp = str(curses_text('full gpg passphrase:\n\n\n\n\n(ctrl+g/enter to confirm)')) _gpg_password_temp = str(curses_text('full gpg passphrase:\n\n\n\n\n(ctrl+g/enter to confirm)'))
_half_length = int(len(_gpg_password_temp)/2) _half_length = len(_gpg_password_temp) // 2
try: try:
_short_password_length = int(curses_text(f"quick unlock pin length ({_half_length}):\n\n\n\n\n(ctrl+g/enter " _short_password_length = int(curses_text(f"quick unlock pin length ({_half_length}):\n\n\n\n\n(ctrl+g/enter "
"to confirm)\n\npin must be half the length of the gpg passphrase " "to confirm)\n\nnote: do NOT enter your desired pin - this is simply "
"or less\n\ncannot be a negative number")) "an integer used to determine the length of the auto-generated pin"
if not 0 <= _short_password_length <= _half_length: "\n\npin must be half the length of the gpg passphrase or less and "
"must be greater than 0"))
if not 0 < _short_password_length <= _half_length:
_short_password_length = _half_length _short_password_length = _half_length
except ValueError: except ValueError:
_short_password_length = _half_length _short_password_length = _half_length
_i, _quick_unlock_password, _quick_unlock_password_excluded = 0, '', '' _i, _quick_unlock_password, _quick_unlock_password_excluded = 0, '', ''
for _char in _gpg_password_temp: for _char in _gpg_password_temp:
if _i % 2 == 1 and _i < _short_password_length*2: if _i % 2 == 1 and _i < _short_password_length * 2:
_quick_unlock_password += _char _quick_unlock_password += _char
else: else:
_quick_unlock_password_excluded += _char _quick_unlock_password_excluded += _char
@@ -298,6 +394,8 @@ def whitelist_setup():
open(f"{home}/.config/sshyp/gpg-gen", 'w').writelines([ open(f"{home}/.config/sshyp/gpg-gen", 'w').writelines([
'Key-Type: 1\n', 'Key-Length: 4096\n', 'Key-Usage: sign encrypt\n', 'Name-Real: sshyp\n', 'Key-Type: 1\n', 'Key-Length: 4096\n', 'Key-Usage: sign encrypt\n', 'Name-Real: sshyp\n',
'Name-Comment: gpg-sshyp-whitelist\n', 'Name-Email: github.com/rwinkhart/sshyp\n', 'Expire-Date: 0']) 'Name-Comment: gpg-sshyp-whitelist\n', 'Name-Email: github.com/rwinkhart/sshyp\n', 'Expire-Date: 0'])
curses_radio(['okay'], 'gpg key generation may take some time (especially on slower devices)\n\nselect "okay" '
'to start\n\ndo not terminate this process!')
run(('gpg', '-q', '--pinentry-mode', 'loopback', '--batch', '--generate-key', '--passphrase', run(('gpg', '-q', '--pinentry-mode', 'loopback', '--batch', '--generate-key', '--passphrase',
_quick_unlock_password, f"{home}/.config/sshyp/gpg-gen")) _quick_unlock_password, f"{home}/.config/sshyp/gpg-gen"))
remove(f"{home}/.config/sshyp/gpg-gen") remove(f"{home}/.config/sshyp/gpg-gen")
@@ -305,37 +403,38 @@ def whitelist_setup():
# encrypt excluded with the assembly key # encrypt excluded with the assembly key
from sshyp import encrypt from sshyp import encrypt
encrypt(_quick_unlock_password_excluded, f"{home}/.config/sshyp/excluded", _gpg_id) encrypt([_quick_unlock_password_excluded], f"{home}/.config/sshyp/excluded", _gpg_id)
curses_radio(['okay, I have it memorized'], f"your quick-unlock pin: {_quick_unlock_password}") curses_radio(['okay, I have it memorized'], f"your quick-unlock pin: {_quick_unlock_password}")
# adds or removes quick-unlock whitelisted device ids # adds or removes quick-unlock whitelisted device ids
def whitelist_manage(_action): def whitelist_manage(_action, _back=False):
_whitelisted_ids = listdir(f"{home}/.config/sshyp/whitelist") _whitelisted_ids = listdir(f"{home}/.config/sshyp/whitelist") + ['BACK']
_device_ids = listdir(f"{home}/.config/sshyp/devices") _device_ids = listdir(f"{home}/.config/sshyp/devices")
# a value of True indicates adding
if _action:
_unwhitelisted_ids = [] _unwhitelisted_ids = []
for _id in _device_ids: for _id in _device_ids:
if _id not in _whitelisted_ids: if _id not in _whitelisted_ids:
_unwhitelisted_ids.append(_id) _unwhitelisted_ids.append(_id)
_unwhitelisted_ids.append('cancel') _unwhitelisted_ids.append('BACK')
_add_id = curses_radio(_unwhitelisted_ids, 'id to add to whitelist:')
if _add_id == len(_unwhitelisted_ids)-1:
return
open(f"{home}/.config/sshyp/whitelist/{_unwhitelisted_ids[_add_id]}", 'w').write('')
else:
_whitelisted_choices = _whitelisted_ids + ['cancel']
_del_id = curses_radio(_whitelisted_choices, 'id to remove from whitelist:')
if _del_id == len(_whitelisted_choices)-1:
return
remove(f"{home}/.config/sshyp/whitelist/{_whitelisted_ids[_del_id]}")
# prune deleted device ids from whitelist while not _back:
for _id in _whitelisted_ids: # _action == True indicates adding
if _id not in _device_ids: if _action:
remove(f"{home}/.config/sshyp/whitelist/{_id}") _add_id = curses_radio(_unwhitelisted_ids, 'id to add to whitelist:')
if _add_id == len(_unwhitelisted_ids) - 1:
_back = True
else:
open(f"{home}/.config/sshyp/whitelist/{_unwhitelisted_ids[_add_id]}", 'w').write('')
del _unwhitelisted_ids[_add_id]
# _action == False indicates removing
else:
_del_id = curses_radio(_whitelisted_ids, 'id to remove from whitelist:')
if _del_id == len(_whitelisted_ids) - 1:
_back = True
else:
remove(f"{home}/.config/sshyp/whitelist/{_whitelisted_ids[_del_id]}")
del _whitelisted_ids[_del_id]
# runs quick-unlock configuration menu # runs quick-unlock configuration menu
@@ -360,23 +459,27 @@ def extension_downloader():
from os import chmod from os import chmod
from tempfile import gettempdir from tempfile import gettempdir
from urllib.request import urlopen, urlretrieve from urllib.request import urlopen, urlretrieve
_file_data = urlopen("https://raw.githubusercontent.com/rwinkhart/sshyp-labs/main/pointers/v1.5.1").read() # the version listed below will NOT always match the version of sshyp being used
# it is only updated if new extensions are incompatible with previous sshyp versions
_file_data = urlopen("https://raw.githubusercontent.com/rwinkhart/sshyp-labs/main/pointers/v1.5.2").read()
_pointer = ConfigParser(interpolation=None) _pointer = ConfigParser(interpolation=None)
_pointer.read_string(_file_data.decode('utf-8')) _pointer.read_string(_file_data.decode('utf-8'))
_extensions = _pointer.sections() _extensions = _pointer.sections()
_extensions.append('CANCEL') _extensions.append('BACK')
_choice = curses_radio(_extensions, 'select an extension for more info') _choice = curses_radio(_extensions, 'select an extension for more info')
if _choice == len(_extensions)-1: if _choice == len(_extensions) - 1:
return False return False
_selected = _extensions[_choice] _selected = _extensions[_choice]
_choice = curses_radio(('no', 'yes'), f"description: {_pointer.get(_selected, 'desc')}\n\nusage: " _divider = (stdscr.getmaxyx()[1]) * '-'
f"{_pointer.get(_selected, 'usage')}\n\ninstall {_selected}?") _choice = curses_radio(('no', 'yes'), '# description\n' + _divider + '\n\n' + _pointer.get(_selected, 'desc') +
'\n\n# usage\n' + _divider + '\n\n' + _pointer.get(_selected, 'usage').replace('<br>', '\n')
+ '\n\n' + _divider + '\n\ninstall ' + _selected + '?')
# if installing the extension... # if installing the extension...
if _choice == 1: if _choice == 1:
# download extension files to temporary directory # download extension files to temporary directory
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini" _exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
_ext_exe = urlretrieve(_pointer.get(_selected, 'exe'), _exe_dir) urlretrieve(_pointer.get(_selected, 'exe'), _exe_dir)
_ext_ini = urlretrieve(_pointer.get(_selected, 'ini'), _ini_dir) urlretrieve(_pointer.get(_selected, 'ini'), _ini_dir)
# set permissions under active user # set permissions under active user
chmod(_exe_dir, 0o755) chmod(_exe_dir, 0o755)
chmod(_ini_dir, 0o644) chmod(_ini_dir, 0o644)
@@ -389,9 +492,9 @@ def extension_remover():
_installed = [] _installed = []
for _extension in listdir('/usr/lib/sshyp/extensions'): for _extension in listdir('/usr/lib/sshyp/extensions'):
_installed.append(_extension[:-4]) _installed.append(_extension[:-4])
_installed.append('CANCEL') _installed.append('BACK')
_choice = curses_radio(_installed, 'select an extension to uninstall') _choice = curses_radio(_installed, 'select an extension to uninstall')
if _choice == len(_installed)-1: if _choice == len(_installed) - 1:
return False return False
_sure = curses_radio(('no', 'yes'), f"are you sure you want to remove {_installed[_choice]}?") _sure = curses_radio(('no', 'yes'), f"are you sure you want to remove {_installed[_choice]}?")
if _sure == 0: if _sure == 0:
@@ -438,14 +541,14 @@ def global_menu(_scr, _device_type, _top_message):
while True: while True:
_options, _choice, _exit_signal = ['change device/synchronization types'], 0, False _options, _choice, _exit_signal = ['change device/synchronization types'], 0, False
if _device_type == 'client': if _device_type == 'client':
_options.extend(['change gpg key', 're-configure ssh(ync)', 'change device name', _options.extend(['change gpg key', 're-configure ssh(ync)', 'change device id',
'[OPTIONAL, RECOMMENDED] set custom text editor', '[OPTIONAL, RECOMMENDED] set custom text editor',
'[OPTIONAL] enable/disable quick-unlock', '[OPTIONAL] enable/disable quick-unlock',
'[OPTIONAL] re-encrypt/optimize entries', '[OPTIONAL] re-encrypt/optimize entries',
'[OPTIONAL] extension management']) '[OPTIONAL] extension management'])
else: else:
_options.extend(['manage quick-unlock/whitelist']) _options.extend(['remove registered device ids', 'manage quick-unlock/whitelist'])
_options.extend(['EXIT/DONE']) _options.append('EXIT/DONE')
_choice += curses_radio(_options, _top_message) _choice += curses_radio(_options, _top_message)
if _choice == 0: if _choice == 0:
@@ -458,7 +561,7 @@ def global_menu(_scr, _device_type, _top_message):
# ...and text editor settings are missing # ...and text editor settings are missing
if not sshyp_data.has_option('CLIENT-GENERAL', 'text_editor'): if not sshyp_data.has_option('CLIENT-GENERAL', 'text_editor'):
editor_config(True) editor_config(True)
# ...and online (synced) mode is enabled... # ...and online (synchronized) mode is enabled...
if _dev_sync_types[1] == 'false': if _dev_sync_types[1] == 'false':
# ...and quick-unlock settings are missing # ...and quick-unlock settings are missing
if not sshyp_data.has_option('CLIENT-ONLINE', 'quick_unlock_enabled'): if not sshyp_data.has_option('CLIENT-ONLINE', 'quick_unlock_enabled'):
@@ -472,7 +575,7 @@ def global_menu(_scr, _device_type, _top_message):
if not listdir(f"{home}/.config/sshyp/devices"): if not listdir(f"{home}/.config/sshyp/devices"):
if None in (_ip, _username_ssh, _port): if None in (_ip, _username_ssh, _port):
_ip, _username_ssh, _port, _identity = ssh_config() _ip, _username_ssh, _port, _identity = ssh_config()
dev_id_config(_ip, _username_ssh, _port, _identity) dev_id_config(_port, _username_ssh, _ip, _identity)
# ...or ssh_error is missing # ...or ssh_error is missing
elif not sshyp_data.has_option('CLIENT-ONLINE', 'ssh_error'): elif not sshyp_data.has_option('CLIENT-ONLINE', 'ssh_error'):
sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1') sshyp_data.set('CLIENT-ONLINE', 'ssh_error', '1')
@@ -482,17 +585,20 @@ def global_menu(_scr, _device_type, _top_message):
if _device_type == 'client': if _device_type == 'client':
gpg_config() gpg_config()
else: else:
whitelist_menu() registered_dev_id_remover()
elif _choice == 2: elif _choice == 2:
if _device_type == 'client': if _device_type == 'client':
ssh_config() ssh_config(True)
else: else:
_exit_signal = True whitelist_menu()
elif _choice == 3: elif _choice == 3:
if _device_type == 'client':
if not sshyp_data.has_section('SSHYNC'): if not sshyp_data.has_section('SSHYNC'):
ssh_config() ssh_config()
dev_id_config(sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'user'), dev_id_config(sshyp_data.get('SSHYNC', 'port'), sshyp_data.get('SSHYNC', 'user'),
sshyp_data.get('SSHYNC', 'port'), sshyp_data.get('SSHYNC', 'identity_file')) sshyp_data.get('SSHYNC', 'ip'), sshyp_data.get('SSHYNC', 'identity_file'), True)
else:
_exit_signal = True
elif _choice == 4: elif _choice == 4:
editor_config(False) editor_config(False)
elif _choice == 5: elif _choice == 5:
@@ -553,12 +659,11 @@ def initial_setup(_scr):
# online (synchronized mode) configuration # online (synchronized mode) configuration
if _dev_sync_types[1] != 'true': if _dev_sync_types[1] != 'true':
# ssh+sshync configuration # ssh+sshync configuration
_ip, _username_ssh, _port, _identity = ssh_config() _ip, _username_ssh, _port, _identity = ssh_config()
# device id configuration # device id configuration
dev_id_config(_ip, _username_ssh, _port, _identity) dev_id_config(_port, _username_ssh, _ip, _identity)
# PORT START CLIPTOOL # PORT START CLIPTOOL
# check for clipboard tool and display warning if missing # check for clipboard tool and display warning if missing
@@ -578,23 +683,31 @@ def initial_setup(_scr):
# run optional configuration menu # run optional configuration menu
curses_radio(['okay'], 'required configuration complete\n\na menu for additional (optional) configuration will be ' curses_radio(['okay'], 'required configuration complete\n\na menu for additional (optional) configuration will be '
'displayed\n\nthis menu can be safely exited at any time') 'displayed\n\nthis menu can be safely exited at any time')
wrapped_entry(_dev_sync_types[0], 'additional configuration options:')
# set gm_device_type so that after the init menu is terminated the global menu knows the device type
global gm_device_type
gm_device_type = _dev_sync_types[0]
return
# runs the specified entry function (menu start point) within a curses wrapper # runs the specified entry function (menu start point) within a curses wrapper
def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'): def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'):
from curses import wrapper, use_default_colors from curses import wrapper, use_default_colors
global gm_device_type
gm_device_type = _gm_device_type
# a boolean value represents init # a boolean value represents init
if isinstance(_gm_device_type, bool): if isinstance(gm_device_type, bool):
wrapper(lambda _wrap_stdscr: (use_default_colors(), initial_setup(_wrap_stdscr))) wrapper(lambda _wrap_stdscr: (use_default_colors(), initial_setup(_wrap_stdscr)))
# any other value will be provided as the global menu device type # any other value will be interpreted as the global menu device type
else: # this code still runs when called for init once the init menu terminates
_repeat = True _repeat = True
while _repeat: while _repeat:
try: try:
_ext_name, _escalator, _action = \ _ext_name, _escalator, _action = \
wrapper(lambda _wrap_stdscr: (use_default_colors(), wrapper(lambda _wrap_stdscr: (use_default_colors(),
global_menu(_wrap_stdscr, _gm_device_type, _gm_top_message)))[1] global_menu(_wrap_stdscr, gm_device_type, _gm_top_message)))[1]
except ChildProcessError: except ChildProcessError:
print("\n\u001b[38;5;9merror: privilege escalation required\n\nneither 'doas' nor 'sudo' were found in " print("\n\u001b[38;5;9merror: privilege escalation required\n\nneither 'doas' nor 'sudo' were found in "
"the system's $PATH\u001b[0m\n") "the system's $PATH\u001b[0m\n")
@@ -605,12 +718,17 @@ def wrapped_entry(_gm_device_type, _gm_top_message='configuration options:'):
# install with privilege escalation (outside of curses) # install with privilege escalation (outside of curses)
from tempfile import gettempdir from tempfile import gettempdir
_exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini" _exe_dir, _ini_dir = f"{gettempdir()}/sshyp_exe", f"{gettempdir()}/sshyp_ini"
# PORT START TWEAK-EXT-CHOWN
if uname()[0] == 'FreeBSD':
run((_escalator, 'chown', 'root:wheel', _exe_dir, _ini_dir))
else:
run((_escalator, 'chown', 'root:root', _exe_dir, _ini_dir)) 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', _exe_dir, f"/usr/lib/sshyp/{_ext_name}"))
run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini")) run((_escalator, 'mv', _ini_dir, f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
else: else:
# uninstall with privilege escalation (outside of curses) # uninstall with privilege escalation (outside of curses)
run((_escalator, 'rm', '-I', f"/usr/lib/sshyp/{_ext_name}", run((_escalator, 'rm', f"/usr/lib/sshyp/{_ext_name}",
f"/usr/lib/sshyp/extensions/{_ext_name}.ini")) f"/usr/lib/sshyp/extensions/{_ext_name}.ini"))
else: else:
_repeat = False _repeat = False
+10 -2
View File
@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
version=$(head -n1 extra/changelog-archive/2023 | cut -c8-) version=$(head -n1 extra/changelog-archive/2024 | cut -c8-)
if [ -z "$2" ]; then if [ -z "$2" ]; then
revision=1 revision=1
else else
@@ -21,6 +21,7 @@ _create_generic_linux() {
cd port-jobs cd port-jobs
./CLIPTOOL.py LINUX ./CLIPTOOL.py LINUX
./CLIPBOARD.py LINUX ./CLIPBOARD.py LINUX
./CHOWN.py
./UNAME.py LINUX ./UNAME.py LINUX
./COMMENTS.py ALL ./COMMENTS.py ALL
./BLANKS.py ./BLANKS.py
@@ -135,7 +136,7 @@ licenses {
\"GNU GPL v3\" \"GNU GPL v3\"
} }
copyrights { copyrights {
\"2021-2023 Randall Winkhart\" \"2021-2024 Randall Winkhart\"
} }
provides { provides {
sshyp_client = "$version" sshyp_client = "$version"
@@ -158,6 +159,7 @@ urls {
./RMEXTMAN.py ./RMEXTMAN.py
./CLIPTOOL.py ./CLIPTOOL.py
./CLIPBOARD.py HAIKU ./CLIPBOARD.py HAIKU
./CHOWN.py
./UNAME.py TMP ./UNAME.py TMP
./COMMENTS.py ALL ./COMMENTS.py ALL
./BLANKS.py ./BLANKS.py
@@ -200,6 +202,7 @@ _create_deb() {
./CLIPBOARD.py WSL ./CLIPBOARD.py WSL
special=WSL-ONLY-DEBIAN special=WSL-ONLY-DEBIAN
fi fi
./CHOWN.py
./UNAME.py LINUX ./UNAME.py LINUX
./COMMENTS.py ALL ./COMMENTS.py ALL
./BLANKS.py ./BLANKS.py
@@ -253,6 +256,7 @@ _create_termux() {
./RMEXTMAN.py ./RMEXTMAN.py
./CLIPTOOL.py ./CLIPTOOL.py
./CLIPBOARD.py TERMUX ./CLIPBOARD.py TERMUX
./CHOWN.py
./UNAME.py TERMUX ./UNAME.py TERMUX
./COMMENTS.py ALL ./COMMENTS.py ALL
./BLANKS.py ./BLANKS.py
@@ -313,6 +317,7 @@ cp -r %%{_sourcedir}/usr %%{buildroot}
/usr/lib/sshyp/sshyp.py /usr/lib/sshyp/sshyp.py
/usr/lib/sshyp/sshync.py /usr/lib/sshyp/sshync.py
/usr/lib/sshyp/stweak.py /usr/lib/sshyp/stweak.py
/usr/lib/sshyp/clipclear.py
/usr/lib/sshyp/extensions/ /usr/lib/sshyp/extensions/
/usr/share/bash-completion/completions/sshyp /usr/share/bash-completion/completions/sshyp
/usr/share/zsh/site-functions/_sshyp /usr/share/zsh/site-functions/_sshyp
@@ -325,6 +330,7 @@ cp -r %%{_sourcedir}/usr %%{buildroot}
cd port-jobs cd port-jobs
./CLIPTOOL.py LINUX ./CLIPTOOL.py LINUX
./CLIPBOARD.py LINUX ./CLIPBOARD.py LINUX
./CHOWN.py
./UNAME.py LINUX ./UNAME.py LINUX
./COMMENTS.py ALL ./COMMENTS.py ALL
./BLANKS.py ./BLANKS.py
@@ -377,6 +383,7 @@ printf "/usr/bin/sshyp
/usr/lib/sshyp/sshyp.py /usr/lib/sshyp/sshyp.py
/usr/lib/sshyp/sshync.py /usr/lib/sshyp/sshync.py
/usr/lib/sshyp/stweak.py /usr/lib/sshyp/stweak.py
/usr/lib/sshyp/clipclear.py
/usr/local/share/bash-completion/completions/sshyp /usr/local/share/bash-completion/completions/sshyp
/usr/local/share/zsh/site-functions/_sshyp /usr/local/share/zsh/site-functions/_sshyp
/usr/share/licenses/sshyp/license /usr/share/licenses/sshyp/license
@@ -388,6 +395,7 @@ printf "/usr/bin/sshyp
cd port-jobs cd port-jobs
./CLIPTOOL.py LINUX ./CLIPTOOL.py LINUX
./CLIPBOARD.py BSD ./CLIPBOARD.py BSD
./CHOWN.py BSD
./UNAME.py TMP ./UNAME.py TMP
./COMMENTS.py ALL ./COMMENTS.py ALL
./BLANKS.py ./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)
+1 -1
View File
@@ -39,7 +39,7 @@ if argv[1] == hash_paste.hexdigest():
run(('xclip', '-sel', 'c'), stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout) run(('xclip', '-sel', 'c'), stdin=Popen(('printf', '%b', _copy_subject.replace('\\\\\\', '\\\\\\\\\\\\\\')), stdout=PIPE).stdout)
Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'x11'))""" Popen((realpath(__file__).rsplit('/', 1)[0] + "/clipclear.py", _hash.hexdigest(), 'x11'))"""
clear_replacement = """if argv[2] == 'wayland': clear_replacement = """if argv[2] == 'wayland':
hash_paste.update(run('wl-paste', stdout=PIPE).stdout.strip()) hash_paste.update(run('wl-paste', stdout=PIPE, stderr=DEVNULL).stdout.strip())
if argv[1] == hash_paste.hexdigest(): if argv[1] == hash_paste.hexdigest():
run(('wl-copy', '-c')) run(('wl-copy', '-c'))
else: else:
+1 -1
View File
@@ -1,5 +1,5 @@
sshyp is a FOSS password manager that uses an sftp-based syncing back-end. sshyp is a FOSS password manager that uses an sftp-based syncing back-end.
Copyright (C) 2021-2023 Randall Winkhart idgr@tutanota.com Copyright (C) 2021-2024 Randall Winkhart idgr@tutanota.com
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of version 3 (only) of the GNU General Public License it under the terms of version 3 (only) of the GNU General Public License
+4
View File
@@ -0,0 +1,4 @@
## Known Bugs
No bugs are currently documented within sshyp, but this is due to a lack of current testing (bugs most certainly exist).
Development and testing efforts have been re-focused on [MUTN](https://github.com/rwinkhart/MUTN) and [libmutton](https://github.com/rwinkhart/libmutton).
+13
View File
@@ -0,0 +1,13 @@
## Clipboard Troubleshooting
### Clipboard managers can break sshyp
***
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. I noticed it with KDE Klipper, which is what prompted me to create this wiki page. **Clipboard managers should not be enabled by default in any environment** or distribution due to their **potential security implications**.
### Termux cannot clear the clipboard from the background
***
If using the Termux (Android) version of sshyp, the clipboard may not successfully be cleared after the 30 second timeout period if Termux is not actively in the foreground when the sleep timer expires. This is an unfortunate side-effect of running on Android and cannot be easily fixed. Due to Termux being at the bottom of the platform support priority list, I will not be investing time into working around this.
+26
View File
@@ -0,0 +1,26 @@
## Shell Completions Troubleshooting
ZSH completions not working? Make sure your ~/.zshrc contains the following:
```
autoload -Uz compinit && compinit
```
...and then restart your shell.
***
Bash completions not working? Install your distribution's 'bash-completion' package or source the completion script manually.
For most environments, this would mean adding the following to your ~/.bashrc:
```
source /usr/share/bash-completion/completions/sshyp
```
Note that this directory is different on FreeBSD and Haiku.
FreeBSD:
```
source /usr/local/share/bash-completion/completions/sshyp
```
Haiku:
```
source /system/data/bash-completion/completions/sshyp
```
...and then restart your shell.
*Please note that Bash completions are slightly more limited than ZSH completions, and as such, new entries will not be auto-completed until the completions script is re-sourced.*
+15
View File
@@ -0,0 +1,15 @@
**Note:** [Shell completions may require additional configuration to work](https://github.com/rwinkhart/sshyp/blob/main/wiki/completions.md).
[Haiku](https://github.com/rwinkhart/sshyp/blob/main/wiki/installationGuides/haiku.md)
[FreeBSD](https://github.com/rwinkhart/sshyp/blob/main/wiki/installationGuides/freebsd.md)
[Alpine Linux](https://github.com/rwinkhart/sshyp/blob/main/wiki/installationGuides/alpine.md)
[Arch Linux](https://github.com/rwinkhart/sshyp/blob/main/wiki/installationGuides/arch.md)
[Debian/Ubuntu Linux (or WSL)](https://github.com/rwinkhart/sshyp/blob/main/wiki/installationGuides/debian.md)
[Fedora Linux](https://github.com/rwinkhart/sshyp/blob/main/wiki/installationGuides/fedora.md)
[Termux (Android)](https://github.com/rwinkhart/sshyp/blob/main/wiki/installationGuides/termux.md)
+11
View File
@@ -0,0 +1,11 @@
## Installation (Alpine Linux)
The Alpine Linux package is actively tested on the latest stable release of Alpine Linux.
***
1. Download the *.apk file from the [latest tagged release of sshyp](https://github.com/rwinkhart/sshyp/releases)
2. Run `doas apk add --allow-untrusted <path/to/*.apk`
3. sshyp is now installed - run `sshyp init` to get started!
***
Creating a package from an APKBUILD? Just use `abuild -r` in the same directory as the APKBUILD.
+10
View File
@@ -0,0 +1,10 @@
## Installation (Arch Linux)
The Arch package is actively tested on up-to-date installations of Arch and Artix Linux.
***
The easiest method of installation is via the AUR. Search for "sshyp" using your favorite AUR helper or run:
```
git clone https://aur.archlinux.org/sshyp.git
cd sshyp
makepkg -si
```
sshyp is now installed - run `sshyp init` to get started!
+12
View File
@@ -0,0 +1,12 @@
## Installation (Debian/Ubuntu)
The Debian/Ubuntu package is actively tested on Debian 12 (standard) and Ubuntu 22.04 (WSL).
_OpenSSH warning:_ Sync support requires OpenSSH >= 8.7, meaning **Debian 12 (Bookworm)** is the minimum supported version.
***
1. Download the *.deb file from the [latest tagged release of sshyp](https://github.com/rwinkhart/sshyp/releases)
^ do not download the version ending in *_termux.deb by mistake!
2. Run `sudo dpkg -i <path/to/*.deb>; sudo apt install -f`
3. sshyp is now installed - run `sshyp init` to get started!
+10
View File
@@ -0,0 +1,10 @@
## Installation (Fedora)
The Fedora package is actively tested on the latest version of Fedora Linux.
This package may be install-able on other Red Hat-based distributions, but it is only tested with the latest version of Fedora.
***
1. Download the *.rpm file from the [latest tagged release of sshyp](https://github.com/rwinkhart/sshyp/releases)
2. Run `sudo dnf localinstall <path/to/*.rpm>`
3. sshyp is now installed - run `sshyp init` to get started!
+10
View File
@@ -0,0 +1,10 @@
## Installation (FreeBSD)
The FreeBSD package is actively tested on the latest version of FreeBSD 14 and should be fully functional on at least FreeBSD 13+.
***
1. Download the *.pkg file from the [latest tagged release of sshyp](https://github.com/rwinkhart/sshyp/releases)
2. Run `sudo pkg add <path/to/*.pkg>`
^ the install may fail if you are missing any dependencies - please install the dependencies reported by the package manager and try again
3. sshyp is now installed - run `sshyp init` to get started!
+14
View File
@@ -0,0 +1,14 @@
## Installation (Haiku)
**Missing features:** For security and technical reasons, the Haiku version of sshyp cannot be used as a server (it only functions as a client). Additionally, the Haiku package does not support the extension management system, so extensions still must be installed as separate packages.
Please note that sshyp for Haiku is targeting release R1 Beta 4 (64-bit). Previous releases have significant show-stopping bugs, while stability on the nightly releases cannot be guaranteed.
***
1. Download the *.hpkg file from the [latest tagged release of sshyp](https://github.com/rwinkhart/sshyp/releases)
2. Open the downloaded file and click "Install sshyp" in the top right of the window
**or**
Run `pkgman install <path/to/*.hpkg>`
3. sshyp is now installed - run `sshyp init` to get started!
+16
View File
@@ -0,0 +1,16 @@
## Installation (Termux)
**Missing features:** For technical reasons, the Termux version of sshyp cannot be used as a server (it only functions as a client). Additionally, the Termux package does not support the extension management system, so extensions still must be installed as separate packages.
The Termux package is minimally tested. Attempts to maintain compatibility are made and it should be fully functional on the latest versions of Termux+Termux:API (F-Droid versions), but newer features are more likely to be broken than on other platforms.
***
1. Install the Termux application from [F-Droid](https://f-droid.org/en/packages/com.termux/)
2. Install the Termux API from [F-Droid](https://f-droid.org/en/packages/com.termux.api/) (for clipboard support)
3. From within Termux, run `curl -L <github/link/to/latest/*_termux.deb> -o sshyp.deb`
^ copy the required download link from the [latest tagged release of sshyp](https://github.com/rwinkhart/sshyp/releases)
4. From within Termux, run `dpkg -i sshyp.deb; pkg install -f`
5. sshyp is now installed - run `sshyp init` to get started!
+27
View File
@@ -0,0 +1,27 @@
## Postmortem
### Reflecting on the Flaws of sshyp
Due to being my introduction to the world of programming, sshyp contains many flaws that have been addressed with the creation of [MUTN](https://github.com/rwinkhart/MUTN).
Many of these flaws are the result of a lack of direction for the project.
In the beginning, sshyp was actually called "rpass" and was meant to be a simple wrapper for pass/password-store with rsync integration for synchronization.
I quickly ran into issues using rsync and pivoted to using sftp, thus necessitating the rebrand to "sshyp".
sshyp grew to become its own standalone password manager with no relation to pass/password-store (except for entry import compatibility).
In terms of technical flaws resulting from a lack of experience, here is a non-exhaustive list:
- Port jobs are unnecessarily complex and difficult to maintain; they made working on the project into a chore
- Extensions are unnecessarily complex and their functionality is better left to third-party clients
- Delegating functionality to extensions meant poor integration with the entry format, help menus, and shell completions
- Due to sshyp being a program with no underlying library, third-party clients are not very feasible
- Not having a library with the goal of change stability led to breaking changes in nearly every release of sshyp
- Python was a poor choice for my personal desire of portability, as dependencies must be independently installed on each system
- This led to me avoiding all non-standard libraries, which meant relying on system binaries for things like GPG and SSH
- This meant taking into account the versions of these binaries shipped by each distribution
- Because of this, sshyp was tied to OpenSSH (no support for other SSH implementations)
- This also meant launching separate SSH processes for each item being synchronized, making sshyp's synchronization _very_ slow
- Dynamically typed languages are a poor choice for beginners, as they allow for poor programming practices that create bugs
- With sshyp, I attempted to "save memory" by re-using variables for multiple purposes
- sshyp also checks variable types, rather than values, as a shortcut for determining a function's exit status
- sshyp combined the client and server into one package, resulting in lots of unreadable spaghetti code (especially with argument parsing)
- sshyp was designed in a way where it would be very difficult to port to non-UNIX-like platforms (it would basically necessitate a complete rewrite)
Developing [the successor to sshyp](https://github.com/rwinkhart/MUTN) forced me to confront these flaws and make a better product. It also allowed me the chance to re-consider each design decision; I found a better way of doing nearly everything.
+6
View File
@@ -0,0 +1,6 @@
## Usage
**Note:** [Shell completions may require additional configuration to work](https://github.com/rwinkhart/sshyp/blob/main/wiki/completions.md).
Read the man page with `man sshyp`, or [preview it on GitHub](https://github.com/rwinkhart/sshyp/blob/main/extra/manpage).
Please note that the version of the man page on GitHub may not be accurate to the version of sshyp you have installed, as the git version is kept up to date with the source, rather than the latest tagged release.