mirror of
https://github.com/rwinkhart/MUTN.git
synced 2026-08-28 04:46:41 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff9ea4640f | ||
|
|
8d88f9c7fd | ||
|
|
b6e53451f1 | ||
|
|
b148dbf8bf | ||
|
|
18fb930a94 | ||
|
|
70ac2ad8ee | ||
|
|
d6d9a56532 | ||
|
|
966f340ddd | ||
|
|
f4abb4cc7e | ||
|
|
538d043656 | ||
|
|
926eb1f176 | ||
|
|
8e59d81fff | ||
|
|
1afbce9b1b | ||
|
|
8f9aa7eef3 | ||
|
|
5f02e41c05 | ||
|
|
0211941a48 | ||
|
|
0dfaf20fec | ||
|
|
32364717dc | ||
|
|
9af456590c | ||
|
|
441730e0e0 | ||
|
|
25f2013d62 | ||
|
|
cac35e1d25 | ||
|
|
88c60cc036 | ||
|
|
aaf69eb590 | ||
|
|
12167b1eed | ||
|
|
48ecf7cf83 | ||
|
|
fb70b38dc3 | ||
|
|
19dd1fd685 | ||
|
|
7016a8a130 | ||
|
|
8112496006 | ||
|
|
b26c2ce17c | ||
|
|
23c6d2e354 | ||
|
|
63fb511649 | ||
|
|
4c4f025d5b | ||
|
|
1a9de90f89 | ||
|
|
300c433909 | ||
|
|
bbd42728d0 | ||
|
|
55cd9b99da | ||
|
|
6478957372 | ||
|
|
b58461b9c4 | ||
|
|
a75416a679 | ||
|
|
fc1e3b11fc | ||
|
|
107a6f4f12 | ||
|
|
39a3460779 | ||
|
|
60d6961ff4 | ||
|
|
13aa507788 | ||
|
|
c1c9565511 | ||
|
|
41b4c4b6ca | ||
|
|
bc35766de5 | ||
|
|
3cb0bdce59 | ||
|
|
bfb6372c29 |
+1
-3
@@ -1,8 +1,6 @@
|
||||
/*.exe*
|
||||
/mutn
|
||||
/mutn.exe
|
||||
/MUTN
|
||||
/MUTN.exe
|
||||
/main
|
||||
/main.exe
|
||||
/version.go
|
||||
1output
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
MUTN
|
||||
Copyright (c) 2024-2025 Randall Winkhart
|
||||
Copyright (c) 2024-2026 Randall Winkhart
|
||||
|
||||
MUTN is licensed under the AGPL 3.0 due to its usage of
|
||||
a custom fork of https://github.com/jessp01/gohighlight.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
---
|
||||
Pronounced as: "mutton", "muh·tn"
|
||||
|
||||
MUTN is a simple, self-hosted, SSH-synchronized password and note manager based on [libmutton](https://github.com/rwinkhart/libmutton). It is the successor to [sshyp](https://github.com/rwinkhart/sshyp).
|
||||
MUTN is a simple, self-hosted, vertically integrated, SSH-synchronized password manager. It is based on and is the reference implementation of [libmutton](https://github.com/rwinkhart/libmutton).
|
||||
|
||||
> [!WARNING]
|
||||
>It is your responsibility to assess the security and stability of MUTN and to ensure it meets your needs before using it.
|
||||
@@ -23,14 +23,8 @@ Additionally, MUTN is available as a source package ("[mutn](https://aur.archlin
|
||||
After installing, please review the [usage guide](https://github.com/rwinkhart/MUTN/blob/main/wiki/usage.md).
|
||||
|
||||
# Roadmap
|
||||
### Release v0.4.0
|
||||
- [ ] Switch to fully compliant Markdown (do not preserve new lines)
|
||||
- [ ] Switch to alternative Markdown renderer (minimark or BEAN)
|
||||
### Release v0.5.0
|
||||
- [ ] libmutton v0.5.0
|
||||
- [ ] Implement "netpin" (quick-unlock)
|
||||
- [ ] Password aging support
|
||||
- [ ] Add yellow/red dot indicators to entry list readout for when passwords should be changed
|
||||
- Switch to alternative Markdown renderer (minimark or BEAN)
|
||||
### Release v1.0.0 - Distribution packages
|
||||
- [ ] Create packaging scripts
|
||||
- [x] Stable source PKGBUILD
|
||||
|
||||
@@ -28,7 +28,7 @@ _mutnCompletions() {
|
||||
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password note folder" -- "$cur" )
|
||||
;;
|
||||
copy )
|
||||
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password username totp url note" -- "$cur" )
|
||||
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password username totp url note menu" -- "$cur" )
|
||||
;;
|
||||
edit )
|
||||
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password username totp url note rename" -- "$cur" )
|
||||
|
||||
@@ -86,7 +86,7 @@ function cliMUTN-optionCompleter {
|
||||
|
||||
$possibleValues = @{
|
||||
add = @('password', 'note', 'folder')
|
||||
copy = @('password', 'username', 'totp', 'url', 'note')
|
||||
copy = @('password', 'username', 'totp', 'url', 'note', 'menu')
|
||||
edit = @('password', 'username', 'totp', 'url', 'note', 'rename')
|
||||
gen = @('update')
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ case ${words[-2]} in
|
||||
compadd {password,note,folder}
|
||||
;;
|
||||
copy )
|
||||
compadd {password,username,totp,url,note}
|
||||
compadd {password,username,totp,url,note,menu}
|
||||
;;
|
||||
edit )
|
||||
compadd {password,username,totp,url,note,rename}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.TH MUTN 1 "17 August 2025" "v0.3.0" "MUTN man page"
|
||||
.TH MUTN 1 "17 February 2026" "v0.4.0" "MUTN man page"
|
||||
|
||||
.SH NAME
|
||||
\fBmutn\fR - Simple, self-hosted, SSH-synchronized password and note management based on libmutton. It is the successor to sshyp.
|
||||
\fBmutn\fR - Simple, self-hosted, SSH-synchronized password and note management based on libmutton.
|
||||
|
||||
.SH SYNOPSIS
|
||||
Usage: mutn [/<entry name> [argument] [option]] | [argument]
|
||||
@@ -63,6 +63,7 @@ copy:
|
||||
totp|-t Generate and copy the TOTP token for an entry to your clipboard
|
||||
url|-l Copy the URL in an entry to your clipboard
|
||||
note|-n Copy the first note line in an entry to your clipboard
|
||||
menu|-m Open interactive menu to copy any combination of fields
|
||||
|
||||
edit:
|
||||
password|-pw Change the password in an entry
|
||||
@@ -83,11 +84,11 @@ add:
|
||||
.SH TIPS
|
||||
1. You can quickly read an entry with "mutn /<entry name>"
|
||||
|
||||
2. Type "mutn" (no arguments/options) to view a list of saved entries
|
||||
2. Using "add", "edit", or "copy" without specifying an option (field) will default to "password"
|
||||
|
||||
3. Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help
|
||||
3. Various changes can be made to the configuration and stored entries from the "mutn tweak" menu
|
||||
|
||||
4. Using "add", "edit", or "copy" without specifying an option (field) will default to "password"
|
||||
4. Type "mutn" (no arguments) to view a list of saved entries w/colored markers indicating the ages of stored passwords
|
||||
|
||||
.SH SETUP
|
||||
libmutton operates on a client-server model, and thus sync support requires you to have access to your own server (whether it be a physical home server or a cloud rental) with remote access via SSH.
|
||||
@@ -101,6 +102,7 @@ Once this has been done, you may proceed with the client setup:
|
||||
2. Run "mutn init" and follow the prompts
|
||||
3. If you already have entries on the server, sync them using "mutn sync"
|
||||
4. Optionally, shell completions (Bash, ZSH, and PowerShell 7+) can be enabled with your shell's respective method
|
||||
5. If importing passwords from another password manager, you may desire to use the "Age all entries" option from the "mutn tweak" menu to add initial age tracking data to the imported passwords
|
||||
|
||||
.SH TROUBLESHOOTING
|
||||
.B Clipboard not clearing:
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
**WARNING: AS LIBMUTTON HAS NOT YET REACHED v1.0.0, [BREAKING CHANGES](https://github.com/rwinkhart/libmutton/blob/main/wiki/breaking.md) IN FUTURE UPDATES ARE PLANNED**
|
||||
|
||||
**MUTN v0.3.1**
|
||||
Built with libmutton v0.4.4
|
||||
November 23, 2025
|
||||
|
||||
This is a simple dependency bump and bugfix release. Security issues are addressed. See the [libmutton v0.4.4 patchnotes](https://github.com/rwinkhart/libmutton/releases/tag/v0.4.4) for more information.
|
||||
|
||||
A new major version is well underway. This is likely the last release in the v0.3.x series before more breaking changes are made.
|
||||
|
||||
---
|
||||
|
||||
**MUTN v0.3.0**
|
||||
Built with libmutton v0.4.0
|
||||
August 17, 2025
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
**WARNING: AS LIBMUTTON HAS NOT YET REACHED v1.0.0, [BREAKING CHANGES](https://github.com/rwinkhart/libmutton/blob/main/wiki/breaking.md) IN FUTURE UPDATES ARE PLANNED**
|
||||
|
||||
**MUTN v0.4.0**
|
||||
Built with libmutton v0.5.0
|
||||
February 17, 2026
|
||||
|
||||
# The "Hardy Herd" Update
|
||||
This release adds password age-tracking support, a new interactive copy menu, and features many under-the-hood (breaking) changes.
|
||||
|
||||
## libmutton-derived Changes
|
||||
- **PLEASE** read [libmutton's release notes](https://github.com/rwinkhart/libmutton/releases/tag/v0.5.0), as they detail severe breaking changes. You **NEED** to update your `libmuttonserver` and run `mutn init` after this update!
|
||||
|
||||
## Features
|
||||
- (41b4c4b6ca2f5a74899e87c6d520df7b140671b3) (c1c95655114a689daf80d2410e19ba35b670b1b1) (107a6f4f1250de8f94d87c192d58cc47bef80c1f) (fc1e3b11fc810a79cc0673b6edf96d4753c0300c) (a75416a6792559179e39497cb033407f18c00620) (b58461b9c458795278f050ff10e91a2d05c21745) (b26c2ce17c0491890de03dc131717d557876a3e4) (8112496006c7d48dd4a8f21a9eea51eadc30f732) (12167b1eeddb49e8f5e62f788dc1da6265706f68) (aaf69eb590af4dc37013102dcff79ef115b9f809) (cac35e1d25b7a9a4bd7263406f642834418f6b76) Add new interactive copy menu
|
||||
- This menu aims to reduce the number of times the user is decrypting each entry
|
||||
- (1a9de90f890f133e6ab8f298241fe3a6237ba4c8) (7016a8a1309b06c63b9dd63fb126103d4cab6ccc) Password age tracking (from libmutton)
|
||||
- The `tweak` menu now allows adding age data to old (existing) entries
|
||||
- (926eb1f1766f28a69e194c74758c300f233856fb) Option to verify entries (integrity) in `tweak` menu
|
||||
|
||||
## Changes
|
||||
- (f4abb4cc7e5f303f68358fcdda659056422ebeb3) MUTN no longer preserves new lines in Markdown notes (more compliant)
|
||||
|
||||
## Dependencies (direct/replaced)
|
||||
- Dropped (libmutton)
|
||||
- golang.design/x/clipboard
|
||||
- gopkg.in/ini.v1
|
||||
- Bumped
|
||||
- Go: v1.25.4 => 1.26.0
|
||||
- github.com/rwinkhart/go-boilerplate: v0.1.0 => v0.3.1
|
||||
- github.com/rwinkhart/libmutton: v0.4.4 => v0.5.0
|
||||
- golang.org/x/term: v0.36.0 => v0.40.0
|
||||
@@ -1,12 +1,12 @@
|
||||
module github.com/rwinkhart/MUTN
|
||||
|
||||
go 1.24.6
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/glamour v0.7.0
|
||||
github.com/rwinkhart/go-boilerplate v0.1.0
|
||||
github.com/rwinkhart/libmutton v0.4.0
|
||||
golang.org/x/term v0.34.0
|
||||
github.com/rwinkhart/go-boilerplate v0.3.1
|
||||
github.com/rwinkhart/libmutton v0.5.0
|
||||
golang.org/x/term v0.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -14,37 +14,32 @@ require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkg/sftp v1.13.9 // indirect
|
||||
github.com/pkg/sftp v1.13.10 // indirect
|
||||
github.com/pquerna/otp v1.5.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rwinkhart/go-highlite v0.1.1 // indirect
|
||||
github.com/rwinkhart/peercred-mini v0.1.1 // indirect
|
||||
github.com/rwinkhart/rcw v0.2.2 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
github.com/rwinkhart/peercred-mini v0.1.4 // indirect
|
||||
github.com/rwinkhart/rcw v0.3.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.16 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||
golang.design/x/clipboard v0.7.1 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/image v0.30.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20250813145510-f12310a0cfd9 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
replace golang.org/x/sys => github.com/rwinkhart/sys v0.35.0
|
||||
replace golang.org/x/sys => github.com/rwinkhart/sys v0.41.0
|
||||
|
||||
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410
|
||||
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio v0.1.1
|
||||
|
||||
replace github.com/charmbracelet/glamour => github.com/rwinkhart/glamour-temp-MUTN v0.7.2
|
||||
|
||||
@@ -5,22 +5,23 @@ github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd3
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
@@ -29,8 +30,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
|
||||
github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
@@ -41,103 +42,37 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rwinkhart/glamour-temp-MUTN v0.7.2 h1:/CBexwkGdL8HuZjO7M0zzjGHaHCr9/r4XTOtUJa+pPE=
|
||||
github.com/rwinkhart/glamour-temp-MUTN v0.7.2/go.mod h1:DN/eJaNQ5h7dgxzfmWFo7By+b82CKEUdNaY6z4R4v3c=
|
||||
github.com/rwinkhart/go-boilerplate v0.1.0 h1:EzlVj6R7Bxtl79Nl7R5zRcg6s+Cf2FAqGIzR4giWTQg=
|
||||
github.com/rwinkhart/go-boilerplate v0.1.0/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY=
|
||||
github.com/rwinkhart/go-boilerplate v0.3.1 h1:vkVRuptO2s1yPzzwpvtiiB0c/hPB6yr0p1mzZ2Myb9E=
|
||||
github.com/rwinkhart/go-boilerplate v0.3.1/go.mod h1:ES13A2r9fnCVfyezwMBgY/RgA4pOIudOUXz3Jk/ikes=
|
||||
github.com/rwinkhart/go-highlite v0.1.1 h1:9TxbRhYVfD/3YaEgNk1BtEiZ0t8/5mxDUZqNMXHodT0=
|
||||
github.com/rwinkhart/go-highlite v0.1.1/go.mod h1:mWLMtCWcyV0BG4NeyPyAUGMjPvI0ds6vXmUq89QwBFA=
|
||||
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410 h1:NhHwFM3Pgm6zRUfFKvi0p5ndjfFbVWsRwmmhyFlG4PE=
|
||||
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/rwinkhart/libmutton v0.4.0 h1:dUB192cYaYWgfMzgLrM+eq3zCtKXNsXq7l8Kcfoe4RE=
|
||||
github.com/rwinkhart/libmutton v0.4.0/go.mod h1:QLDVXlwnQsySGew0YG6z3AbQBO8CFvm3cIw3WvA4Crs=
|
||||
github.com/rwinkhart/peercred-mini v0.1.1 h1:hDoqSEwynJN4w8OWlZNSQAjAum/ocP3AYF7DjtPV5qU=
|
||||
github.com/rwinkhart/peercred-mini v0.1.1/go.mod h1:dstv+IydIklCnffwAZgD2AWqkGe0mETn3lLtqJjYAeI=
|
||||
github.com/rwinkhart/rcw v0.2.2 h1:bTUi3BLjrcoibi5YDlzeiVKo608EmGetymXbJVM3oYE=
|
||||
github.com/rwinkhart/rcw v0.2.2/go.mod h1:lhTErVEG3klKVJkcoHuTw5pDHiS3Irlj7Z03IshHh7M=
|
||||
github.com/rwinkhart/sys v0.35.0 h1:6ZikETjyuv9ndnSEnXQmp/M3lxyCHWoHAm3lGzy0pnE=
|
||||
github.com/rwinkhart/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
github.com/rwinkhart/go-winio v0.1.1 h1:kAJKiqneR7cUR01Wn5/doAAV4kOGTEGPug4oinXc5N4=
|
||||
github.com/rwinkhart/go-winio v0.1.1/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
|
||||
github.com/rwinkhart/libmutton v0.5.0 h1:Du/o5Hyb26uE6lubqBISN/ZF9JZ5oObHVveQjAqrP0E=
|
||||
github.com/rwinkhart/libmutton v0.5.0/go.mod h1:peh8A1f3gnaF97m8J06U28JloKv04Kw8WtFCH+Pcq6s=
|
||||
github.com/rwinkhart/peercred-mini v0.1.4 h1:93+phjLknvJadEd2cu/ZPPWdfRSPOwFJzDBEn4ZtWVc=
|
||||
github.com/rwinkhart/peercred-mini v0.1.4/go.mod h1:E8eApo/izzmq4nGGR+kpJ0ZLAXjofnVRMGCeVMPQ/Ik=
|
||||
github.com/rwinkhart/rcw v0.3.0 h1:OdgA++IGcDrZqN6FzyYnzbz04fL8Q1MHvlgPjhtgJM4=
|
||||
github.com/rwinkhart/rcw v0.3.0/go.mod h1:UUY7kSku6kV2X1RWPUcK0Re+rQ6RvngWI8DVZJ11cqk=
|
||||
github.com/rwinkhart/sys v0.41.0 h1:pHB6HphVC132UXYZ6yeOk62abqgl+pyl5ZZnsk4iUKg=
|
||||
github.com/rwinkhart/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
|
||||
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp/shiny v0.0.0-20250813145105-42675adae3e6 h1:UmlTtCyqnD3jxsGe973Y9uZydewwYUboXPux+RCcKQU=
|
||||
golang.org/x/exp/shiny v0.0.0-20250813145105-42675adae3e6/go.mod h1:QnFR+evpZFrYgSiu+d/Rn6g/6bNqLQTp+rzKaVpFoeI=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/mobile v0.0.0-20250813145510-f12310a0cfd9 h1:tf0OY/FXi1sPkoNVKP4w+GStqIfqbFUqDoDDm4B+iCg=
|
||||
golang.org/x/mobile v0.0.0-20250813145510-f12310a0cfd9/go.mod h1:wNiuiJfmmgv45sw8EHpNeVWqpxLeEwQ8bkUIhuOYUh8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -11,12 +11,13 @@ import (
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/go-boilerplate/front"
|
||||
"github.com/rwinkhart/go-boilerplate/other"
|
||||
"github.com/rwinkhart/libmutton/cfg"
|
||||
"github.com/rwinkhart/libmutton/age"
|
||||
"github.com/rwinkhart/libmutton/clip"
|
||||
"github.com/rwinkhart/libmutton/config"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/crypt"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/syncclient"
|
||||
"github.com/rwinkhart/libmutton/synccycles"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -33,33 +34,31 @@ func main() {
|
||||
// if the first argument is an entry...
|
||||
if strings.HasPrefix(args[1], "/") {
|
||||
|
||||
// store location of target entry
|
||||
targetLocation := global.TargetLocationFormat(args[1])
|
||||
// store realPath of target entry
|
||||
realPath := global.GetRealPath(args[1])
|
||||
|
||||
// entry reader shortcut (if no other arguments are supplied)
|
||||
if argsCount == 2 {
|
||||
cli.EntryReaderDecrypt(targetLocation, true)
|
||||
cli.EntryReaderDecrypt(realPath, true)
|
||||
|
||||
// perform other operations on the entry (if other arguments are supplied)
|
||||
} else if argsCount == 3 || (argsCount == 4 && (args[3] == "show" || args[3] == "-s")) {
|
||||
if argsCount == 3 { // default to "password" if no field is specified (for copy, edit, and add)
|
||||
switch args[2] {
|
||||
case "show", "-s":
|
||||
cli.EntryReaderDecrypt(targetLocation, false)
|
||||
cli.EntryReaderDecrypt(realPath, false)
|
||||
case "copy":
|
||||
err := core.CopyArgument(targetLocation, 0)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to copy passphrase to clipboard: "+err.Error(), global.ErrorClipboard)
|
||||
if err := clip.CopyShortcut(realPath, 0, nil); err != nil {
|
||||
other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard)
|
||||
}
|
||||
case "edit":
|
||||
cli.EditEntryField(targetLocation, true, 0)
|
||||
cli.EditEntryField(realPath, 0)
|
||||
case "gen":
|
||||
cli.AddEntry(targetLocation, true, 1)
|
||||
cli.AddEntry(realPath, 1)
|
||||
case "add":
|
||||
cli.AddEntry(targetLocation, true, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
case "shear":
|
||||
err := syncclient.ShearRemoteFromClient(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory
|
||||
if err != nil {
|
||||
if err := syncclient.ShearRemote(args[1], false); err != nil {
|
||||
other.PrintError("Failed to shear target: "+err.Error(), back.ErrorWrite)
|
||||
}
|
||||
default:
|
||||
@@ -68,11 +67,11 @@ func main() {
|
||||
} else { // handle "show" or "-s" argument for gen, edit, and add
|
||||
switch args[2] {
|
||||
case "edit":
|
||||
cli.EditEntryField(targetLocation, false, 0)
|
||||
cli.EditEntryField(realPath, 0)
|
||||
case "gen":
|
||||
cli.AddEntry(targetLocation, false, 1)
|
||||
cli.AddEntry(realPath, 1)
|
||||
case "add":
|
||||
cli.AddEntry(targetLocation, false, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
default:
|
||||
cli.HelpMain()
|
||||
}
|
||||
@@ -94,11 +93,12 @@ func main() {
|
||||
field = 3
|
||||
case "note", "-n":
|
||||
field = 4
|
||||
case "menu", "-m":
|
||||
cli.CopyMenu(args[1], nil, nil)
|
||||
default:
|
||||
cli.HelpCopy()
|
||||
}
|
||||
err := core.CopyArgument(targetLocation, field)
|
||||
if err != nil {
|
||||
if err := clip.CopyShortcut(realPath, field, nil); err != nil {
|
||||
other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard)
|
||||
}
|
||||
case "edit":
|
||||
@@ -115,38 +115,38 @@ func main() {
|
||||
case "note", "-n":
|
||||
field = 4
|
||||
case "rename", "-r":
|
||||
isAccessible, _ := back.TargetIsFile(targetLocation, true) // error is ignored because dir/file status is irrelevant
|
||||
isAccessible, _ := back.TargetIsFile(realPath, true) // error is ignored because dir/file status is irrelevant
|
||||
if !isAccessible {
|
||||
other.PrintError("Failed to access location ("+targetLocation+")", back.ErrorTargetNotFound)
|
||||
other.PrintError("Failed to access location ("+realPath+")", back.ErrorTargetNotFound)
|
||||
}
|
||||
cli.RenameCli(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory
|
||||
default:
|
||||
cli.HelpEdit()
|
||||
}
|
||||
if argsCount == 4 {
|
||||
cli.EditEntryField(targetLocation, true, field)
|
||||
cli.EditEntryField(realPath, field)
|
||||
} else {
|
||||
switch args[4] {
|
||||
case "show", "-s":
|
||||
cli.EditEntryField(targetLocation, false, field)
|
||||
cli.EditEntryField(realPath, field)
|
||||
default:
|
||||
cli.EditEntryField(targetLocation, true, field)
|
||||
cli.EditEntryField(realPath, field)
|
||||
}
|
||||
}
|
||||
case "gen":
|
||||
if argsCount == 4 {
|
||||
switch args[3] {
|
||||
case "update", "-u":
|
||||
cli.GenUpdate(targetLocation, true)
|
||||
cli.GenUpdate(realPath)
|
||||
default:
|
||||
cli.HelpGen()
|
||||
}
|
||||
} else if args[3] == "update" || args[3] == "-u" {
|
||||
switch args[4] {
|
||||
case "show", "-s":
|
||||
cli.GenUpdate(targetLocation, false)
|
||||
cli.GenUpdate(realPath)
|
||||
default:
|
||||
cli.GenUpdate(targetLocation, true)
|
||||
cli.GenUpdate(realPath)
|
||||
}
|
||||
}
|
||||
cli.HelpGen()
|
||||
@@ -154,20 +154,19 @@ func main() {
|
||||
switch args[3] {
|
||||
case "password", "-pw":
|
||||
if argsCount == 4 {
|
||||
cli.AddEntry(targetLocation, true, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
} else {
|
||||
switch args[4] {
|
||||
case "show", "-s":
|
||||
cli.AddEntry(targetLocation, false, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
default:
|
||||
cli.AddEntry(targetLocation, true, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
}
|
||||
}
|
||||
case "note", "-n":
|
||||
cli.AddEntry(targetLocation, true, 2)
|
||||
cli.AddEntry(realPath, 2)
|
||||
case "folder", "-f":
|
||||
err := syncclient.AddFolderRemoteFromClient(args[1]) // pass the incomplete path as the server will have a different home directory
|
||||
if err != nil {
|
||||
if err := syncclient.AddFolderRemote(args[1]); err != nil {
|
||||
other.PrintError("Failed to add folder: "+err.Error(), back.ErrorWrite)
|
||||
}
|
||||
default:
|
||||
@@ -182,50 +181,65 @@ func main() {
|
||||
} else {
|
||||
switch args[1] {
|
||||
case "clipclear":
|
||||
err := core.ClipClearArgument()
|
||||
if err != nil {
|
||||
if err := clip.ClearArgument(); err != nil {
|
||||
other.PrintError("Failure occurred in clipboard clearing process: "+err.Error(), global.ErrorClipboard)
|
||||
}
|
||||
case "startrcwd":
|
||||
crypt.RCWDArgument()
|
||||
case "sync":
|
||||
_, err := syncclient.RunJob(true)
|
||||
_, err := syncclient.RunJob()
|
||||
if err != nil {
|
||||
other.PrintError("Failed to sync entries: "+err.Error(), global.ErrorSyncProcess)
|
||||
}
|
||||
case "init":
|
||||
err := core.LibmuttonInit(front.Input,
|
||||
[][3]string{{"MUTN", "textEditor", getTextEditorInput()}},
|
||||
confirmRCWPassphrase("new"), false)
|
||||
err := core.LibmuttonInit(
|
||||
front.Input,
|
||||
confirmRCWPassword("new"),
|
||||
front.InputBinary("Preserve existing config directory?"),
|
||||
false,
|
||||
"",
|
||||
map[string]any{"mutnTextEditor": getTextEditorInput()},
|
||||
)
|
||||
if err != nil {
|
||||
other.PrintError("Initialization failed: "+err.Error(), 0)
|
||||
}
|
||||
case "tweak":
|
||||
choice := front.InputMenuGen("Action:", []string{"Change device ID", "Change master passphrase/Optimize entries", "Set text editor"})
|
||||
choice := front.InputMenuGen("Action:", []string{"Set text editor", "Change device ID", "Age all entries", "Verify all entries", "Change master password/Optimize entries"})
|
||||
switch choice {
|
||||
case 1:
|
||||
newCfg := &config.CfgT{}
|
||||
newClientSpecificCfg := map[string]any{"mutnTextEditor": getTextEditorInput()}
|
||||
newCfg.ClientSpecific = &newClientSpecificCfg
|
||||
if err := config.Write(newCfg, true); err != nil {
|
||||
other.PrintError("Failed to set text editor: "+err.Error(), back.ErrorWrite)
|
||||
}
|
||||
case 2:
|
||||
oldDeviceID, err := global.GetCurrentDeviceID()
|
||||
if err != nil {
|
||||
other.PrintError("Failed to get current device ID: "+err.Error(), back.ErrorRead)
|
||||
}
|
||||
_, _, err = synccycles.DeviceIDGen(oldDeviceID, "")
|
||||
_, _, _, err = syncclient.GenDeviceID(oldDeviceID, "")
|
||||
if err != nil {
|
||||
other.PrintError("Failed to change device ID: "+err.Error(), global.ErrorSyncProcess)
|
||||
}
|
||||
fmt.Println("\nDevice ID changed successfully.")
|
||||
case 2:
|
||||
oldPassphrase := confirmRCWPassphrase("old")
|
||||
newPassphrase := confirmRCWPassphrase("new")
|
||||
fmt.Print("\nRe-encrypting entries. Please wait; do not force close this process.\n")
|
||||
err := core.EntryRefresh(oldPassphrase, newPassphrase, false)
|
||||
if err != nil {
|
||||
case 3:
|
||||
forceReage := front.InputBinary("Re-age aged entries?")
|
||||
fmt.Println("Aging entries; please wait - do not terminate this process")
|
||||
if err := age.AllPasswordEntries(forceReage, nil); err != nil {
|
||||
other.PrintError("Failed to age entries: "+err.Error(), 1)
|
||||
}
|
||||
case 4:
|
||||
verifyEntries(front.InputSecret("RCW Password:"))
|
||||
case 5:
|
||||
oldPassword := confirmRCWPassword("old")
|
||||
newPassword := confirmRCWPassword("new")
|
||||
fmt.Print("Re-encrypting entries; please wait - do not terminate this process.\n\n")
|
||||
if err := core.EntryRefresh(oldPassword, newPassword, false); err != nil {
|
||||
other.PrintError("Re-encryption failed: "+err.Error(), global.ErrorEncryption)
|
||||
}
|
||||
fmt.Println("\nRe-encryption complete.")
|
||||
case 3:
|
||||
err := cfg.WriteConfig([][3]string{{"MUTN", "textEditor", getTextEditorInput()}}, nil, true)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to set text editor: "+err.Error(), back.ErrorWrite)
|
||||
if front.InputBinary("\nRe-encryption complete. Verify new entries (recommended)?") {
|
||||
verifyEntries(newPassword)
|
||||
}
|
||||
}
|
||||
case "copy":
|
||||
@@ -245,14 +259,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// confirmRCWPassphrase prompts the user for a master passphrase and confirms it
|
||||
// is typed correctly. Returns the passphrase as a byte slice.
|
||||
func confirmRCWPassphrase(lowercasePrefix string) []byte {
|
||||
// confirmRCWPassword prompts the user for a master password and confirms it
|
||||
// is typed correctly. Returns the password as a byte slice.
|
||||
func confirmRCWPassword(lowercasePrefix string) []byte {
|
||||
for {
|
||||
// get master passphrase
|
||||
rcwPass := front.InputHidden(strings.ToUpper(string(lowercasePrefix[0])) + lowercasePrefix[1:] + " master passphrase:")
|
||||
if !bytes.Equal(rcwPass, front.InputHidden("Confirm "+lowercasePrefix+" master passphrase:")) || len(rcwPass) == 0 {
|
||||
fmt.Println(back.AnsiError + "Passphrases do not match or passphrase is invalid" + back.AnsiReset)
|
||||
// get master password
|
||||
rcwPass := front.InputSecret(strings.ToUpper(string(lowercasePrefix[0])) + lowercasePrefix[1:] + " master password:")
|
||||
if !bytes.Equal(rcwPass, front.InputSecret("Confirm "+lowercasePrefix+" master password:")) || len(rcwPass) == 0 {
|
||||
fmt.Println(back.AnsiError + "Passwords do not match or password is invalid" + back.AnsiReset)
|
||||
continue
|
||||
}
|
||||
return rcwPass
|
||||
@@ -263,3 +277,13 @@ func confirmRCWPassphrase(lowercasePrefix string) []byte {
|
||||
func getTextEditorInput() string {
|
||||
return cmp.Or(front.Input("Text editor (leave blank for $EDITOR, falls back to \""+cli.FallbackEditor+"\"):"), os.Getenv("EDITOR"), cli.FallbackEditor)
|
||||
}
|
||||
|
||||
// verifyEntries attempts to verify all entries using the provided RCW password.
|
||||
func verifyEntries(rcwPassword []byte) {
|
||||
fmt.Println("Verification in progress; please wait - do not terminate this process...")
|
||||
err := core.VerifyEntries(rcwPassword)
|
||||
if err != nil {
|
||||
other.PrintError("Verification failed: "+err.Error(), global.ErrorDecryption)
|
||||
}
|
||||
fmt.Println("Verification successful")
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
# get version number from cli package
|
||||
cd ..
|
||||
printf 'package main\nimport (\n"fmt"\n"github.com/rwinkhart/MUTN/src/cli"\n)\nfunc main() {\nfmt.Println(cli.MUTNVersion)\n}' > ./version.go
|
||||
version=$(go run -ldflags="-s -w" -tags noMarkdown ./version.go)
|
||||
version=$(go run -ldflags="-s -w" ./version.go)
|
||||
rm ./version.go
|
||||
cd ./packaging
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ pkgrel="$((revision-1))"
|
||||
pkgdesc='A simple, self-hosted, SSH-synchronized password/note manager for the CLI (based on libmutton)'
|
||||
arch='all'
|
||||
url='https://github.com/rwinkhart/MUTN'
|
||||
license='MIT'
|
||||
license='AGPL-3.0-only'
|
||||
depends=''
|
||||
makedepends='busybox go'
|
||||
options='!check net'
|
||||
@@ -41,7 +41,7 @@ build() {
|
||||
esac
|
||||
|
||||
# compile binary
|
||||
CGO_ENABLED=1 go build -ldflags=\"-s -w\" -trimpath -tags=stxAll ./mutn.go
|
||||
CGO_ENABLED=0 go build -ldflags=\"-s -w\" -trimpath -tags=stxAll ./mutn.go
|
||||
}
|
||||
|
||||
package() {
|
||||
|
||||
@@ -10,7 +10,7 @@ pkgrel="$revision"
|
||||
pkgdesc='A simple, self-hosted, SSH-synchronized password/note manager for the CLI (based on libmutton)'
|
||||
arch=('x86_64' 'i686' 'i486' 'pentium4' 'aarch64' 'armv7h' 'riscv64')
|
||||
url='https://github.com/rwinkhart/MUTN'
|
||||
license=('MIT')
|
||||
license=('AGPL-3.0-only')
|
||||
depends=()
|
||||
makedepends=(go grep gzip)
|
||||
optdepends=(
|
||||
@@ -45,7 +45,7 @@ build() {
|
||||
esac
|
||||
|
||||
# compile binary
|
||||
CGO_ENABLED=1 go build -ldflags=\"-s -w\" -trimpath -tags=stxAll ./mutn.go
|
||||
CGO_ENABLED=0 go build -ldflags=\"-s -w\" -trimpath -tags=stxAll ./mutn.go
|
||||
}
|
||||
|
||||
package() {
|
||||
|
||||
@@ -10,9 +10,9 @@ create_release_binaries() {
|
||||
printf '\nGenerating release binaries...\n'
|
||||
cd ..
|
||||
GOOS=linux CGO_ENABLED=0 GOAMD64=v2 go build -o ./packaging/1output/mutn-linux-x86_64_v2 -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
GOOS=linux GOARCH=arm64 GOARM64=v8.0 CGO_ENABLED=0 go build -o ./packaging/1output/mutn-linux-arm64v8.0 -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
GOOS=linux GOARCH=arm64 GOARM64=v8.7,crypto CGO_ENABLED=0 go build -o ./packaging/1output/mutn-linux-arm64v8.7+crypto -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
GOOS=linux CGO_ENABLED=0 GOARCH=arm64 GOARM64=v8.0 CGO_ENABLED=0 go build -o ./packaging/1output/mutn-linux-arm64v8.0 -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
GOOS=linux CGO_ENABLED=0 GOARCH=arm64 GOARM64=v8.7,crypto CGO_ENABLED=0 go build -o ./packaging/1output/mutn-linux-arm64v8.7+crypto -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
GOOS=windows CGO_ENABLED=0 GOAMD64=v2 go build -o ./packaging/1output/mutn-windows-x86_64_v2.exe -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
GOOS=windows GOARCH=arm64 GOARM64=v8.7,crypto CGO_ENABLED=0 go build -o ./packaging/1output/mutn-windows-arm64v8.7+crypto.exe -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
GOOS=windows CGO_ENABLED=0 GOARCH=arm64 GOARM64=v8.7,crypto CGO_ENABLED=0 go build -o ./packaging/1output/mutn-windows-arm64v8.7+crypto.exe -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
cd ./packaging
|
||||
}
|
||||
|
||||
+2
-2
@@ -13,11 +13,11 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
MUTNVersion = "0.3.0" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.2.A" -> "0.2.0", "0.2.B" -> "0.2.1"
|
||||
MUTNVersion = "0.4.0" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.B.0" -> "0.2.0", "0.2.A" -> "0.2.1"
|
||||
|
||||
ansiBlackOnWhite = "\033[38;5;0;48;5;15m"
|
||||
)
|
||||
|
||||
func init() {
|
||||
global.GetPassphrase = front.InputHidden
|
||||
global.GetPassword = front.InputSecret
|
||||
}
|
||||
|
||||
+16
-12
@@ -7,41 +7,45 @@ import (
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
)
|
||||
|
||||
// AddEntry creates a new entry at targetLocation by taking user input via CLI prompts.
|
||||
// AddEntry creates a new entry at realPath by taking user input via CLI prompts.
|
||||
// Requires: entryType (0 = standard password entry, 1 = auto-generated password entry, 2 = note-only entry).
|
||||
func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) {
|
||||
// ensure targetLocation is valid
|
||||
_, err := core.EntryAddPrecheck(targetLocation)
|
||||
func AddEntry(realPath string, entryType uint8) {
|
||||
// ensure realPath is valid
|
||||
_, err := core.EntryAddPrecheck(realPath)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to add entry: "+err.Error(), back.ErrorWrite)
|
||||
}
|
||||
|
||||
var decryptedEntry []string
|
||||
var decSlice []string
|
||||
|
||||
var password []byte
|
||||
if entryType < 2 {
|
||||
username := front.Input("Username:")
|
||||
|
||||
// determine whether to generate the password
|
||||
var password string
|
||||
if entryType == 0 {
|
||||
password = string(front.InputHidden("Password:"))
|
||||
password = front.InputSecret("Password:")
|
||||
} else {
|
||||
password = inputPasswordGen()
|
||||
}
|
||||
|
||||
totp := string(front.InputHidden("TOTP secret:"))
|
||||
totp := string(front.InputSecret("TOTP secret:"))
|
||||
url := front.Input("URL:")
|
||||
if front.InputBinary("Add a note to this entry?") {
|
||||
note, _ := editNote([]string{})
|
||||
decryptedEntry = append([]string{password, username, totp, url}, note...)
|
||||
decSlice = append([]string{string(password), username, totp, url}, note...)
|
||||
} else {
|
||||
decryptedEntry = []string{password, username, totp, url}
|
||||
decSlice = []string{string(password), username, totp, url}
|
||||
}
|
||||
} else {
|
||||
note, _ := editNote([]string{})
|
||||
decryptedEntry = append([]string{"", "", "", ""}, note...)
|
||||
decSlice = append([]string{"", "", "", ""}, note...)
|
||||
}
|
||||
|
||||
// write and preview the new entry
|
||||
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets)
|
||||
if password != nil {
|
||||
writeEntryCLI(realPath, decSlice, true, nil)
|
||||
} else {
|
||||
writeEntryCLI(realPath, decSlice, false, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/go-boilerplate/front"
|
||||
"github.com/rwinkhart/go-boilerplate/other"
|
||||
"github.com/rwinkhart/libmutton/clip"
|
||||
"github.com/rwinkhart/libmutton/crypt"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/syncclient"
|
||||
)
|
||||
|
||||
// CopyMenu decrypts an entry and allows the user to interactively copy
|
||||
// fields without having to re-decrypt each time. decSlice can be left nil
|
||||
// to decrypt the entry specified by vanityPath. If the entry was provided
|
||||
// decrypted, CopyMenu assumes it was edited and triggers a sync.
|
||||
func CopyMenu(vanityPath string, decSlice []string, oldPassword []byte) {
|
||||
realPath := global.GetRealPath(vanityPath)
|
||||
var err error
|
||||
if decSlice == nil {
|
||||
// decrypt entry
|
||||
decSlice, err = crypt.DecryptFileToSlice(realPath, nil)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption)
|
||||
}
|
||||
} else {
|
||||
fmt.Print("\n")
|
||||
_, err := syncclient.RunJob()
|
||||
if err != nil {
|
||||
other.PrintError("Failed to sync on copy menu entry: "+err.Error(), global.ErrorSyncProcess)
|
||||
}
|
||||
}
|
||||
|
||||
// determine populated fields in entry
|
||||
var fieldStrings = []string{"Username", "Password", "TOTP Code", "URL", "Note (first line)"}
|
||||
var indices = []int{1, 0, 2, 3, 4}
|
||||
var fieldOptions []string
|
||||
for i := range indices {
|
||||
if len(decSlice) > indices[i] && decSlice[indices[i]] != "" {
|
||||
fieldOptions = append(fieldOptions, fieldStrings[i])
|
||||
if indices[i] == 0 && oldPassword != nil {
|
||||
fieldOptions = append(fieldOptions, "Old Password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if notes are included, preview them
|
||||
if len(decSlice) > 4 {
|
||||
EntryReader(vanityPath, append([]string{"", "", "", ""}, decSlice[4:]...), true)
|
||||
if len(fieldOptions) < 1 { // if notes are the only thing included, exit
|
||||
fmt.Printf("\r%sNo copyable fields present, exiting copy menu...%s\n", back.AnsiWarning, back.AnsiReset)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// set up signal handling for ctrl+c to clear clipboard
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
if err = clip.ClearProcess(nil); err != nil {
|
||||
other.PrintError("Failed to clear clipboard on exit: "+err.Error(), global.ErrorClipboard)
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// set up timer to close copy menu if an item is
|
||||
// not selected (useful to avoid extra user input
|
||||
// when the copy menu is displayed automatically
|
||||
// and is not desired)
|
||||
const timeoutSeconds uint8 = 5
|
||||
selectedChan := make(chan bool, 1)
|
||||
go func() {
|
||||
for i := uint8(1); i <= timeoutSeconds; i++ {
|
||||
select {
|
||||
case <-selectedChan:
|
||||
return
|
||||
case <-time.After(1 * time.Second):
|
||||
if i == timeoutSeconds {
|
||||
fmt.Printf("\r%sNo field selected, exiting copy menu...%s\n", back.AnsiWarning, back.AnsiReset)
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Printf("\rField to copy (exiting in %d seconds): ", timeoutSeconds-i)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// copy selected field to clipboard
|
||||
var choice int
|
||||
var selectedField string
|
||||
for {
|
||||
fmt.Println()
|
||||
if selectedField != "" {
|
||||
choice = front.InputMenuGen("Field to copy:", fieldOptions)
|
||||
} else {
|
||||
choice = front.InputMenuGen("Field to copy (exiting in 5 seconds):", fieldOptions)
|
||||
selectedChan <- true
|
||||
}
|
||||
selectedField = fieldOptions[choice-1]
|
||||
if selectedField == "Old Password" {
|
||||
if err = clip.CopyBytes(false, oldPassword); err != nil {
|
||||
other.PrintError("Failed to copy old password to clipboard: "+err.Error(), global.ErrorClipboard)
|
||||
}
|
||||
continue
|
||||
}
|
||||
choice = indices[slices.Index(fieldStrings, selectedField)]
|
||||
if choice == 2 {
|
||||
fmt.Println(back.AnsiWarning + "[Starting]" + back.AnsiReset + " TOTP clipboard refresher")
|
||||
errorChan := make(chan error, 1)
|
||||
done := make(chan bool)
|
||||
go clip.TOTPCopier(decSlice[2], errorChan, done)
|
||||
// block until first successful copy
|
||||
if err = <-errorChan; err != nil {
|
||||
other.PrintError("Failed to copy TOTP token: "+err.Error(), global.ErrorClipboard)
|
||||
}
|
||||
// block until the user sends "q"
|
||||
for {
|
||||
input := front.Input("Service will run until 'q' is entered:")
|
||||
if input == "q" {
|
||||
break
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
fmt.Println(back.AnsiBlue + "[Stopped]" + back.AnsiReset + " TOTP clipboard refresher")
|
||||
} else {
|
||||
if err = clip.CopyBytes(false, []byte(decSlice[choice])); err != nil {
|
||||
other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-35
@@ -9,71 +9,77 @@ import (
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/go-boilerplate/front"
|
||||
"github.com/rwinkhart/go-boilerplate/other"
|
||||
"github.com/rwinkhart/libmutton/cfg"
|
||||
"github.com/rwinkhart/libmutton/config"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/syncclient"
|
||||
)
|
||||
|
||||
// RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server.
|
||||
func RenameCli(oldLocationIncomplete string) {
|
||||
func RenameCli(oldVanityPath string) {
|
||||
// prompt user for new location and rename
|
||||
newLocationIncomplete := front.Input("New location:")
|
||||
err := syncclient.RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete)
|
||||
if err != nil {
|
||||
newVanityPath := front.Input("New location:")
|
||||
if err := syncclient.RenameRemote(oldVanityPath, newVanityPath); err != nil {
|
||||
other.PrintError("Failed to rename entry: "+err.Error(), back.ErrorWrite)
|
||||
}
|
||||
|
||||
// exit is done from sync.RenameRemoteFromClient
|
||||
// exit is done from sync.RenameRemote
|
||||
}
|
||||
|
||||
// EditEntryField edits a field of an entry at targetLocation (user input).
|
||||
func EditEntryField(targetLocation string, hideSecrets bool, field int) {
|
||||
// EditEntryField edits a field of an entry at realPath (user input).
|
||||
func EditEntryField(realPath string, field int) {
|
||||
// fetch old entry data (with all required lines present)
|
||||
decryptedEntry, err := core.GetOldEntryData(targetLocation, field)
|
||||
decSlice, err := core.GetOldEntryData(realPath, field, nil)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead)
|
||||
}
|
||||
|
||||
// edit the field
|
||||
var oldPassword []byte
|
||||
switch field {
|
||||
case 0:
|
||||
decryptedEntry[field] = string(front.InputHidden("Password:"))
|
||||
oldPassword = []byte(decSlice[field])
|
||||
decSlice[field] = string(front.InputSecret("Password:"))
|
||||
case 1:
|
||||
decryptedEntry[field] = front.Input("Username:")
|
||||
decSlice[field] = front.Input("Username:")
|
||||
case 2:
|
||||
decryptedEntry[field] = string(front.InputHidden("TOTP secret:"))
|
||||
decSlice[field] = string(front.InputSecret("TOTP secret:"))
|
||||
case 3:
|
||||
decryptedEntry[field] = front.Input("URL:")
|
||||
decSlice[field] = front.Input("URL:")
|
||||
case 4: // edit notes fields
|
||||
// store note and non-note data separately
|
||||
fieldsMain := decryptedEntry[:4]
|
||||
fieldsNote := decryptedEntry[4:]
|
||||
fieldsMain := decSlice[:4]
|
||||
fieldsNote := decSlice[4:]
|
||||
|
||||
// edit the note
|
||||
editedNote, noteEdited := editNote(fieldsNote)
|
||||
if !noteEdited { // exit early if the note was not edited
|
||||
other.PrintError("Entry is unchanged", 0)
|
||||
}
|
||||
decryptedEntry = append(fieldsMain, editedNote...)
|
||||
decSlice = append(fieldsMain, editedNote...)
|
||||
}
|
||||
|
||||
// write and preview the modified entry
|
||||
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets)
|
||||
if field == 0 {
|
||||
writeEntryCLI(realPath, decSlice, true, oldPassword)
|
||||
} else {
|
||||
writeEntryCLI(realPath, decSlice, false, oldPassword)
|
||||
}
|
||||
}
|
||||
|
||||
// GenUpdate generates a new password for an entry at targetLocation (user input).
|
||||
func GenUpdate(targetLocation string, hideSecrets bool) {
|
||||
// GenUpdate generates a new password for an entry at realPath (user input).
|
||||
func GenUpdate(realPath string) {
|
||||
// fetch old entry data
|
||||
decryptedEntry, err := core.GetOldEntryData(targetLocation, 0)
|
||||
decSlice, err := core.GetOldEntryData(realPath, 0, nil)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead)
|
||||
}
|
||||
|
||||
// generate a new password
|
||||
decryptedEntry[0] = inputPasswordGen()
|
||||
oldPassword := []byte(decSlice[0])
|
||||
decSlice[0] = string(inputPasswordGen())
|
||||
|
||||
// write and preview the modified entry
|
||||
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets)
|
||||
writeEntryCLI(realPath, decSlice, true, oldPassword)
|
||||
}
|
||||
|
||||
// editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty).
|
||||
@@ -88,11 +94,16 @@ func editNote(baseNote []string) ([]string, bool) {
|
||||
}(tempFile.Name())
|
||||
|
||||
// fetch the user's text editor
|
||||
editorCfg, err := cfg.ParseConfig([][2]string{{"MUTN", "textEditor"}})
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
other.PrintError("Failed to retrieve text editor from libmutton.ini\n\nPlease specify one with \"mutn tweak\"", back.ErrorRead)
|
||||
other.PrintError("Failed to load libmuttoncfg.json: "+err.Error(), back.ErrorRead)
|
||||
}
|
||||
var editor string
|
||||
if value, exists := (*cfg.ClientSpecific)["mutnTextEditor"]; !exists {
|
||||
other.PrintError("Failed to retrieve text editor from libmuttoncfg.json\n\nPlease specify one with \"mutn tweak\"", back.ErrorRead)
|
||||
} else {
|
||||
editor = value.(string)
|
||||
}
|
||||
editor := editorCfg[0]
|
||||
|
||||
// write baseNote to tempFile (if it is not empty)
|
||||
baseNoteLen := len(baseNote)
|
||||
@@ -119,8 +130,7 @@ func editNote(baseNote []string) ([]string, bool) {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if err = cmd.Run(); err != nil {
|
||||
panic(back.AnsiError + "Failed to write note with " + editor + back.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement
|
||||
}
|
||||
|
||||
@@ -140,12 +150,6 @@ func editNote(baseNote []string) ([]string, bool) {
|
||||
// close tempFile
|
||||
_ = tempFile.Close() // error ignored; if the file could be opened, it can probably be closed
|
||||
|
||||
// remove trailing empty strings from the edited note
|
||||
note = back.RemoveTrailingEmptyStrings(note)
|
||||
|
||||
// clamp trailing whitespace in each note line
|
||||
core.ClampTrailingWhitespace(note)
|
||||
|
||||
// return the edited note if:
|
||||
// it is different from baseNote AND
|
||||
// [it is not empty (prevent needless writes when adding blank notes) OR
|
||||
@@ -153,7 +157,6 @@ func editNote(baseNote []string) ([]string, bool) {
|
||||
noteLen := len(note)
|
||||
if !reflect.DeepEqual(note, baseNote) && (noteLen > 0 || baseNoteLen != noteLen) {
|
||||
return note, true
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
+128
-107
@@ -3,158 +3,179 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/go-boilerplate/other"
|
||||
"github.com/rwinkhart/libmutton/age"
|
||||
"github.com/rwinkhart/libmutton/synccommon"
|
||||
)
|
||||
|
||||
// ANSI color constants used only in this file
|
||||
const (
|
||||
ansiAlternateEntryColor = "\033[38;5;8m"
|
||||
ansiDirectoryHeader = "\033[38;5;7;48;5;8m"
|
||||
ansiEmptyDirectoryWarning = "\033[38;5;3m"
|
||||
ansiAlternateEntryColor = "\033[38;5;8m"
|
||||
ansiDirectoryHeader = "\033[38;5;7;48;5;8m"
|
||||
)
|
||||
|
||||
// determineIndentation calculates and returns the final visual indentation multiplier (needed to adjust indentation for skipped parent directories); also subtracts "old" text from directory header.
|
||||
func writeIndent(sb *strings.Builder, indent int) { sb.WriteString(strings.Repeat(" ", indent*2)) }
|
||||
|
||||
// determineIndentation calculates the final visual indentation multiplier (adjusts for skipped parents)
|
||||
// and trims the parent directory name from the directory header (if applicable).
|
||||
func determineIndentation(skippedDirList []bool, dirList []string, currentDirIndex int) (int, string) {
|
||||
var subtractor int // tracks how much to subtract from expected indentation multiplier
|
||||
var lastPrefixIndex int // tracks the index (in both skippedDirList and dirList) of the last displayed parent directory
|
||||
var trimmedDirectory = dirList[currentDirIndex]
|
||||
trimmed := dirList[currentDirIndex]
|
||||
indent := strings.Count(trimmed, "/") - 1 // avoid indenting root-level dirs
|
||||
|
||||
// determine initial indentation multiplier based on "/" occurrences
|
||||
indent := strings.Count(trimmedDirectory, "/") - 1 // subtract 1 to avoid indenting root-level directories
|
||||
|
||||
for i, skipped := range skippedDirList[:currentDirIndex] { // checks each skipped directory to determine if it is a parent to the current directory
|
||||
if strings.HasPrefix(trimmedDirectory, dirList[i]+"/") { // if the current directory is the child of this iteration's directory...
|
||||
if skipped { // ...and this iteration's directory was skipped...
|
||||
subtractor++ // increment the subtractor to indicate that the visual indentation should be reduced
|
||||
subtractor, lastPrefixIndex := 0, 0
|
||||
for i, skipped := range skippedDirList[:currentDirIndex] {
|
||||
if strings.HasPrefix(trimmed, dirList[i]+"/") {
|
||||
if skipped {
|
||||
subtractor++
|
||||
} else {
|
||||
lastPrefixIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
indent = indent - subtractor // calculates final visual indentation multiplier
|
||||
|
||||
if indent < 0 { // disallow negative indentation multipliers
|
||||
indent = 0
|
||||
} else if indent > 0 { // trim the most recently displayed parent directory from the directory header to avoid displaying redundant information
|
||||
trimmedDirectory = strings.Replace(trimmedDirectory, dirList[lastPrefixIndex], "", 1)
|
||||
if indent -= subtractor; indent < 0 {
|
||||
return 0, trimmed
|
||||
}
|
||||
|
||||
return indent, trimmedDirectory
|
||||
if indent > 0 {
|
||||
trimmed = strings.TrimPrefix(trimmed, dirList[lastPrefixIndex])
|
||||
}
|
||||
return indent, trimmed
|
||||
}
|
||||
|
||||
// printFileEntry handles processing for printing file entries (determines color, wraps lines, and prints).
|
||||
func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlternator int8) (int, int8) {
|
||||
// determine color to print fileEntryName (alternate each time function is run)
|
||||
var colorCode string
|
||||
if colorAlternator > 0 {
|
||||
colorCode = ""
|
||||
} else {
|
||||
// printFileEntry handles processing for printing file entries (determines color, adds aging indicator, wraps lines, and prints).
|
||||
func printFileEntry(sb *strings.Builder, entry string, lastSlash int, charCounter *int, indent int, colorAlternator int8, agingTimestamp *int64) int8 {
|
||||
colorCode := ""
|
||||
if colorAlternator <= 0 {
|
||||
colorCode = ansiAlternateEntryColor
|
||||
}
|
||||
colorAlternator = -colorAlternator
|
||||
|
||||
// trim the containing directory from the entry to determine fileEntryName
|
||||
fileEntryName := entry[lastSlash:]
|
||||
|
||||
if charCounter == 0 { // indent first line of entries for each directory header
|
||||
fmt.Print(strings.Repeat(" ", indent*2))
|
||||
agingDot := ""
|
||||
switch age.TranslateAgeTimestamp(agingTimestamp) {
|
||||
case 1:
|
||||
agingDot = back.AnsiGreen + "⁍" + back.AnsiReset
|
||||
case 2:
|
||||
agingDot = back.AnsiWarning + "⁍" + back.AnsiReset
|
||||
case 3:
|
||||
agingDot = back.AnsiError + "⁍" + back.AnsiReset
|
||||
}
|
||||
|
||||
// determine whether to wrap to a new line (+1 is to account for trailing spaces)
|
||||
charCounter += len(fileEntryName) + 1
|
||||
if indentation := indent * 2; charCounter+(indentation) >= width {
|
||||
charCounter = len(fileEntryName) + 1
|
||||
fmt.Print("\n" + strings.Repeat(" ", indentation)) // indent each line
|
||||
name := entry[lastSlash:]
|
||||
if *charCounter == 0 {
|
||||
writeIndent(sb, indent)
|
||||
}
|
||||
|
||||
// print fileEntryName to screen
|
||||
fmt.Printf("%s%s%s ", colorCode, fileEntryName, back.AnsiReset)
|
||||
// wrap if needed; +1 accounts for trailing space, +1 more if aging dot is present
|
||||
inc := len(name) + 1
|
||||
if agingDot != "" {
|
||||
inc++
|
||||
}
|
||||
if *charCounter += inc; *charCounter+indent*2 >= width {
|
||||
*charCounter = len(name) + 1
|
||||
if agingDot != "" {
|
||||
*charCounter++
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
writeIndent(sb, indent)
|
||||
}
|
||||
|
||||
return charCounter, colorAlternator
|
||||
sb.WriteString(agingDot)
|
||||
sb.WriteString(colorCode)
|
||||
sb.WriteString(name)
|
||||
sb.WriteString(back.AnsiReset)
|
||||
sb.WriteString(" ")
|
||||
return colorAlternator
|
||||
}
|
||||
|
||||
// EntryListGen generates and displays the full libmutton entry list.
|
||||
func EntryListGen() {
|
||||
fileList, dirList, err := synccommon.WalkEntryDir()
|
||||
_, dirList, err := synccommon.WalkEntryDir()
|
||||
if err != nil {
|
||||
other.PrintError("Failed to generate entry list: "+err.Error(), back.ErrorRead)
|
||||
}
|
||||
entryMap, err := synccommon.GetAllEntryData()
|
||||
if err != nil {
|
||||
other.PrintError("Failed to retrieve entry aging data: "+err.Error(), back.ErrorRead)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// print header bar w/total entry count
|
||||
fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+back.AnsiReset)
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(ansiBlackOnWhite)
|
||||
_, _ = fmt.Fprint(&sb, len(entryMap))
|
||||
sb.WriteString(" libmutton entries:")
|
||||
sb.WriteString(back.AnsiReset)
|
||||
|
||||
// dirList iteration
|
||||
dirListLength := len(dirList) // save length for multiple references below
|
||||
var skippedDirList = make([]bool, dirListLength) // stores whether each directory was skipped during printout (later used to determine appropriate visual indentation)
|
||||
charCounter := 0 // track whether to line-wrap based on character count in line
|
||||
var colorAlternator int8 = 1 // track alternating colors for each printed entry name
|
||||
var containsSubdirectory bool // indicates whether the current directory contains a subdirectory
|
||||
var indent int // visual indentation multiplier
|
||||
var vanityDirectory string // directory header printed to end-user - visual only, not used in any processing
|
||||
for i, directory := range dirList {
|
||||
|
||||
// reset formatting variables for new directory
|
||||
charCounter = 0
|
||||
colorAlternator = 1
|
||||
|
||||
// default to assuming this directory will be skipped (unless it is the root)
|
||||
if i == 0 {
|
||||
skippedDirList[i] = false
|
||||
} else {
|
||||
skippedDirList[i] = true
|
||||
entriesByDir := make(map[string][]string, len(dirList))
|
||||
for vanityPath := range entryMap {
|
||||
if lastSlash := strings.LastIndex(vanityPath, "/"); lastSlash >= 0 {
|
||||
entriesByDir[vanityPath[:lastSlash]] = append(entriesByDir[vanityPath[:lastSlash]], vanityPath)
|
||||
}
|
||||
}
|
||||
for _, entries := range entriesByDir {
|
||||
sort.Strings(entries)
|
||||
}
|
||||
|
||||
// check if next directory is within the current one
|
||||
if dirListLength > i+1 {
|
||||
if nextDir := dirList[i+1]; directory == nextDir[:strings.LastIndex(nextDir, "/")] {
|
||||
containsSubdirectory = true
|
||||
} else {
|
||||
containsSubdirectory = false
|
||||
}
|
||||
} else {
|
||||
containsSubdirectory = false
|
||||
}
|
||||
|
||||
// fileList iteration
|
||||
containsFiles := false // indicates whether the current directory contains files (entries)
|
||||
for _, file := range fileList {
|
||||
|
||||
// print the current file if it belongs in the current directory - otherwise, break the loop and move on to the next directory
|
||||
if lastSlash := strings.LastIndex(file, "/") + 1; file[:lastSlash-1] == directory {
|
||||
|
||||
// print directory header if this is the first run of the loop
|
||||
if !containsFiles {
|
||||
containsFiles = true
|
||||
skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped
|
||||
indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier
|
||||
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory)
|
||||
}
|
||||
|
||||
charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator)
|
||||
}
|
||||
}
|
||||
|
||||
if !containsFiles { // if the current directory contains no files...
|
||||
if !containsSubdirectory { // nor does it contain any subdirectories...
|
||||
if dirListLength > 1 { // and directories besides the root-level exist... display directory header and empty directory warning
|
||||
skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped
|
||||
indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier
|
||||
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory)
|
||||
fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + back.AnsiReset)
|
||||
} else { // warn if the only thing that exists is the root-level directory
|
||||
fmt.Print("\n\nNothing's here! For help creating your first entry, run \"mutn help\".")
|
||||
}
|
||||
}
|
||||
// precompute whether each directory contains subdirectories;
|
||||
// handle root-level child dirs by treating "" as their parent.
|
||||
hasChildDir := make(map[string]bool, len(dirList))
|
||||
for _, d := range dirList {
|
||||
switch parentEnd := strings.LastIndex(d, "/"); {
|
||||
case parentEnd > 0:
|
||||
hasChildDir[d[:parentEnd]] = true
|
||||
case parentEnd == -1:
|
||||
hasChildDir[""] = true
|
||||
}
|
||||
}
|
||||
|
||||
// print trailing new lines for proper spacing after entry list is complete
|
||||
fmt.Print("\n\n")
|
||||
dirListLength := len(dirList)
|
||||
skippedDirList := make([]bool, dirListLength)
|
||||
charCounter := 0
|
||||
var colorAlternator int8 = 1
|
||||
|
||||
printDirHeader := func(i int) int {
|
||||
skippedDirList[i] = false
|
||||
indent, vanityDirectory := determineIndentation(skippedDirList, dirList, i)
|
||||
sb.WriteString("\n\n")
|
||||
writeIndent(&sb, indent)
|
||||
sb.WriteString(ansiDirectoryHeader)
|
||||
sb.WriteString(vanityDirectory)
|
||||
sb.WriteString("/" + back.AnsiReset + "\n")
|
||||
return indent
|
||||
}
|
||||
|
||||
for i, directory := range dirList {
|
||||
charCounter, colorAlternator = 0, 1
|
||||
skippedDirList[i] = i != 0
|
||||
|
||||
if entries := entriesByDir[directory]; len(entries) > 0 {
|
||||
indent := printDirHeader(i)
|
||||
for _, vanityPath := range entries {
|
||||
lastSlash := strings.LastIndex(vanityPath, "/") + 1
|
||||
colorAlternator = printFileEntry(&sb, vanityPath, lastSlash, &charCounter, indent, colorAlternator, entryMap[vanityPath].AgeTimestamp)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if hasChildDir[directory] {
|
||||
continue
|
||||
}
|
||||
|
||||
if dirListLength > 1 {
|
||||
indent := printDirHeader(i)
|
||||
writeIndent(&sb, indent)
|
||||
sb.WriteString(back.AnsiWarning + "-empty directory-" + back.AnsiReset)
|
||||
continue
|
||||
}
|
||||
|
||||
sb.WriteString("\n\nNothing's here! For help creating your first entry, run \"mutn help\".")
|
||||
}
|
||||
|
||||
sb.WriteString("\n\n")
|
||||
fmt.Print(sb.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
+20
-37
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
@@ -10,59 +9,53 @@ import (
|
||||
"github.com/rwinkhart/go-boilerplate/other"
|
||||
"github.com/rwinkhart/libmutton/crypt"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/syncclient"
|
||||
)
|
||||
|
||||
const ansiShownPassword = "\033[38;5;10m"
|
||||
|
||||
// EntryReader prints the decrypted contents of a libmutton entry in a human-readable format.
|
||||
func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
|
||||
fmt.Println()
|
||||
func EntryReader(vanityPath string, decSlice []string, hideSecrets bool) {
|
||||
fmt.Print("\n" + back.AnsiBold + vanityPath + back.AnsiReset + "\n\n")
|
||||
|
||||
fieldLoop:
|
||||
for field := range decryptedEntry {
|
||||
for field := range decSlice {
|
||||
switch field {
|
||||
case 0:
|
||||
// if the first field (password) is not empty, print it
|
||||
if decryptedEntry[0] != "" {
|
||||
if decSlice[0] != "" {
|
||||
if !hideSecrets {
|
||||
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + back.AnsiReset + "\n\n")
|
||||
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiShownPassword + decSlice[0] + back.AnsiReset + "\n\n")
|
||||
} else {
|
||||
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
|
||||
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + back.AnsiWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
// if the second field (username) is not empty, print it
|
||||
if decryptedEntry[1] != "" {
|
||||
fmt.Print(ansiDirectoryHeader + "Username:" + back.AnsiReset + "\n" + decryptedEntry[1] + "\n\n")
|
||||
if decSlice[1] != "" {
|
||||
fmt.Print(ansiDirectoryHeader + "Username:" + back.AnsiReset + "\n" + decSlice[1] + "\n\n")
|
||||
}
|
||||
case 2:
|
||||
// if the third field (TOTP secret) is not empty, print it
|
||||
if decryptedEntry[2] != "" {
|
||||
if decSlice[2] != "" {
|
||||
if !hideSecrets {
|
||||
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + back.AnsiReset + "\n\n")
|
||||
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiShownPassword + decSlice[2] + back.AnsiReset + "\n\n")
|
||||
} else {
|
||||
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
|
||||
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + back.AnsiWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
// if the fourth field (url) is not empty, print it
|
||||
if decryptedEntry[3] != "" {
|
||||
fmt.Print(ansiDirectoryHeader + "URL:" + back.AnsiReset + "\n" + decryptedEntry[3] + "\n\n")
|
||||
if decSlice[3] != "" {
|
||||
fmt.Print(ansiDirectoryHeader + "URL:" + back.AnsiReset + "\n" + decSlice[3] + "\n\n")
|
||||
}
|
||||
case 4:
|
||||
// print the notes header
|
||||
fmt.Println(ansiDirectoryHeader + "Notes:" + back.AnsiReset)
|
||||
|
||||
// combine remaining fields into a single string (to support Markdown rendering)
|
||||
var noteLines []string
|
||||
for ; field < len(decryptedEntry); field++ {
|
||||
noteLines = append(noteLines, decryptedEntry[field])
|
||||
}
|
||||
|
||||
// render notes as Markdown
|
||||
r, _ := glamour.NewTermRenderer(glamour.WithStylesFromJSONBytes(glamourStyle()), glamour.WithPreservedNewLines(), glamour.WithWordWrap(width))
|
||||
markdownNotesString, _ := r.Render(strings.Join(noteLines, "\n"))
|
||||
r, _ := glamour.NewTermRenderer(glamour.WithStylesFromJSONBytes(glamourStyle()), glamour.WithWordWrap(width))
|
||||
markdownNotesString, _ := r.Render(strings.Join(decSlice[4:], "\n"))
|
||||
_ = r.Close()
|
||||
|
||||
// print the rendered Markdown notes
|
||||
fmt.Print(markdownNotesString)
|
||||
@@ -71,27 +64,17 @@ fieldLoop:
|
||||
break fieldLoop
|
||||
}
|
||||
}
|
||||
|
||||
if syncEnabled {
|
||||
_, err := syncclient.RunJob(false)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to sync entries: "+err.Error(), global.ErrorSyncProcess)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader.
|
||||
func EntryReaderDecrypt(targetLocation string, hideSecrets bool) {
|
||||
_, err := back.TargetIsFile(targetLocation, true)
|
||||
func EntryReaderDecrypt(realPath string, hideSecrets bool) {
|
||||
_, err := back.TargetIsFile(realPath, true)
|
||||
if err != nil { // if the location does not exist or is a directory...
|
||||
other.PrintError("Failed to verify target location: "+err.Error(), back.ErrorTargetNotFound)
|
||||
}
|
||||
decBytes, err := crypt.DecryptFileToSlice(targetLocation)
|
||||
decSlice, err := crypt.DecryptFileToSlice(realPath, nil)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption)
|
||||
}
|
||||
EntryReader(decBytes, hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified
|
||||
// do not exit, as this is the job of EntryReader
|
||||
EntryReader(global.GetVanityPath(realPath), decSlice, hideSecrets)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
)
|
||||
|
||||
func HelpMain() {
|
||||
fmt.Print(back.AnsiBold + "\nMUTN | Copyright (c) 2024-2025 Randall Winkhart\n" + back.AnsiReset + `
|
||||
fmt.Print(back.AnsiBold + "\nMUTN | Copyright (c) 2024-2026 Randall Winkhart\n" + back.AnsiReset + `
|
||||
This software exists under the MIT license; you may redistribute it under certain conditions.
|
||||
This program comes with absolutely no warranty; type "mutn version" for details.
|
||||
|
||||
@@ -40,6 +40,7 @@ This program comes with absolutely no warranty; type "mutn version" for details.
|
||||
totp|-t Generate and copy the TOTP token for an entry to your clipboard
|
||||
url|-l Copy the URL in an entry to your clipboard
|
||||
note|-n Copy the first note line in an entry to your clipboard
|
||||
menu|-m Open interactive menu to copy any combination of fields
|
||||
edit:
|
||||
password|-pw|<blank> Change the password in an entry
|
||||
username|-u Change the username in an entry
|
||||
@@ -55,9 +56,9 @@ This program comes with absolutely no warranty; type "mutn version" for details.
|
||||
folder|-f Add a new folder for entries
|
||||
|
||||
` + back.AnsiBold + "Tip 1:" + back.AnsiReset + ` You can quickly read an entry with "mutn /<entry name>"
|
||||
` + back.AnsiBold + "Tip 2:" + back.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries
|
||||
` + back.AnsiBold + "Tip 3:" + back.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help
|
||||
` + back.AnsiBold + "Tip 4:" + back.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n")
|
||||
` + back.AnsiBold + "Tip 2:" + back.AnsiReset + ` Using "add", "edit", or "copy" without specifying an option (field) will default to "password"
|
||||
` + back.AnsiBold + "Tip 3:" + back.AnsiReset + ` Various changes can be made to the configuration and stored entries from the "mutn tweak" menu
|
||||
` + back.AnsiBold + "Tip 4:" + back.AnsiReset + " Type \"mutn\" (no arguments) to view a list of saved entries w/colored markers indicating the ages of stored passwords\n\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -95,7 +96,8 @@ func HelpCopy() {
|
||||
username|-u Copy the username in an entry to your clipboard
|
||||
totp|-t Generate and copy the TOTP token for an entry to your clipboard
|
||||
url|-l Copy the URL in an entry to your clipboard
|
||||
note|-n Copy the first note line in an entry to your clipboard` + "\n\n")
|
||||
note|-n Copy the first note line in an entry to your clipboard
|
||||
menu|-m Open interactive menu to copy any combination of fields` + "\n\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -150,11 +152,11 @@ func Version() {
|
||||
ansiVersionOutline + "<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" +
|
||||
"\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " MUTN v" + MUTNVersion + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " The Cryptic Cuts Update " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " The \"Hardy Herd\" Update " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " Built with libmutton v" + global.LibmuttonVersion + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " Copyright (c) 2024-2025: Randall Winkhart " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " Copyright (c) 2024-2026: Randall Winkhart " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
|
||||
"<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" + back.AnsiReset +
|
||||
"\n For more information, see:\n\n" +
|
||||
|
||||
@@ -2,17 +2,16 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/go-boilerplate/front"
|
||||
"github.com/rwinkhart/go-boilerplate/other"
|
||||
"github.com/rwinkhart/go-boilerplate/security"
|
||||
"github.com/rwinkhart/libmutton/core"
|
||||
"github.com/rwinkhart/libmutton/synccycles"
|
||||
)
|
||||
|
||||
// inputPasswordGen prompts the user for password generation parameters and returns a generated password as a string.
|
||||
func inputPasswordGen() string {
|
||||
func inputPasswordGen() []byte {
|
||||
passLength := front.InputInt("Password length:", 1, -1)
|
||||
fmt.Println()
|
||||
passCharset := uint8(front.InputMenuGen("Password complexity:", []string{"Simple", "Complex", "Ultra Complex (not compatible with many services)"}))
|
||||
@@ -25,20 +24,17 @@ func inputPasswordGen() string {
|
||||
complexity = 0.2 // (ultra) complex
|
||||
// 2 and 3 indicate complex and ultra complex charsets, respectively
|
||||
}
|
||||
return synccycles.StringGen(passLength, complexity, passCharset)
|
||||
return security.BytesGen(passLength, complexity, passCharset)
|
||||
}
|
||||
|
||||
// writeEntryCLI writes an entry to targetLocation and previews it (errors if no data is supplied).
|
||||
func writeEntryCLI(targetLocation string, decryptedEntry []string, hideSecrets bool) {
|
||||
if core.EntryIsNotEmpty(decryptedEntry) {
|
||||
// write the entry to the target location
|
||||
err := core.WriteEntry(targetLocation, []byte(strings.Join(decryptedEntry, "\n")))
|
||||
if err != nil {
|
||||
// writeEntryCLI writes decSlice to realPath and previews it (errors if no data is supplied).
|
||||
func writeEntryCLI(realPath string, decSlice []string, passwordIsNew bool, oldPassword []byte) {
|
||||
if core.EntryIsNotEmpty(decSlice) {
|
||||
if err := core.WriteEntry(realPath, decSlice, passwordIsNew, nil); err != nil {
|
||||
other.PrintError("Failed to write entry: "+err.Error(), back.ErrorWrite)
|
||||
}
|
||||
// preview the entry
|
||||
fmt.Println(back.AnsiBold + "\nEntry Preview:" + back.AnsiReset)
|
||||
EntryReader(decryptedEntry, hideSecrets, true)
|
||||
CopyMenu("", decSlice, oldPassword)
|
||||
} else {
|
||||
other.PrintError("No data supplied for entry", back.ErrorTargetNotFound)
|
||||
}
|
||||
|
||||
@@ -6,4 +6,3 @@ CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -tags=stxAll ./mutn.go
|
||||
|
||||
Additionally, some custom build tags can be used to create different binaries. The following tags are not used in official builds:
|
||||
- `wsl`: Allows creating a Linux binary that can interact with the Windows clipboard (for WSL)
|
||||
- `termux`: Allows creating a Linux binary that can interact with the Termux clipboard (for Android)
|
||||
|
||||
+1
-1
@@ -14,4 +14,4 @@ Please see the [usage guide](https://github.com/rwinkhart/MUTN/blob/main/wiki/us
|
||||
### Dependencies (required for all installations)
|
||||
- A text editor (preferably CLI-based) for writing entry notes
|
||||
- A private key for SSH key-based authentication is required to use MUTN/libmutton in online/synced mode
|
||||
- Linux: xclip (X11) or wl-clipboard (Wayland) for clipboard support
|
||||
- Linux/BSD: xclip (X11) or wl-clipboard (Wayland) for clipboard support
|
||||
|
||||
@@ -15,6 +15,7 @@ Once this has been done, you may proceed with the client setup:
|
||||
2. Run `mutn init` and follow the prompts
|
||||
3. If you already have entries on the server, sync them using `mutn sync`
|
||||
4. Optionally, shell completions (Bash, ZSH, and PowerShell 7+) can be [enabled with your shell's respective method](https://github.com/rwinkhart/MUTN/blob/main/wiki/completions.md)
|
||||
5. Optionally, [import passwords](https://github.com/rwinkhart/libmutton/blob/main/wiki/migration.md) from another password manager. After doing this, you may desire to use the "Age all entries" option from the `mutn tweak` menu to add initial age tracking data to the imported passwords
|
||||
|
||||
### Tips
|
||||
#### Operating on Passwords
|
||||
|
||||
Reference in New Issue
Block a user