mirror of
https://github.com/rwinkhart/MUTN.git
synced 2026-09-03 23:57:23 -04:00
Initial code commit
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# MUTN Password Manager
|
||||||
|
Pronounced as: "mutton", "muh·tn"
|
||||||
|
|
||||||
|
MUTN is a very simple self-hosted, SSH-synchronized password manager based on libmutton.
|
||||||
|
|
||||||
|
MUTN is an expanded re-implementation of [sshyp](https://github.com/rwinkhart/sshyp) written in Go.
|
||||||
|
|
||||||
|
Though MUTN will feel very familiar to users of sshyp, it is intended to differ and eventually break compatibility.
|
||||||
|
|
||||||
|
# WARNING
|
||||||
|
It is your responsibility to assess the security and stability of MUTN before using it and ensure it meets your needs.
|
||||||
|
I am not responsible for any data loss or breaches of your information resulting from the use of MUTN.
|
||||||
|
MUTN is a new project that is constantly being updated, and though safety and security are priorities, they cannot be guaranteed.
|
||||||
|
|
||||||
|
# Mission Statement
|
||||||
|
MUTN aims to make it as simple as possible to manage passwords and notes via CLI across multiple devices in a secure, self-hosted fashion.
|
||||||
|
|
||||||
|
# Building
|
||||||
|
MUTN is currently in early development and does not yet serve its intended purposes.
|
||||||
|
|
||||||
|
To build a test version of MUTN, simply clone this repository and run `go build main` in the "src" directory.
|
||||||
|
|
||||||
|
# Roadmap
|
||||||
|
Short-term Goals:
|
||||||
|
|
||||||
|
- Re-implement all of sshyp's functionality, including the sshyp-mfa extension from [sshyp-labs](https://github.com/rwinkhart/sshyp-labs)
|
||||||
|
|
||||||
|
Long-term Goals:
|
||||||
|
|
||||||
|
- Migrate away from GPG
|
||||||
|
- Consider using a Go-based SSH package, rather than relying on OpenSSH directly
|
||||||
|
- Break the entry format to allow for storing password aging data (how old a password is)
|
||||||
|
- Add markdown support for notes
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
git add -f extra src/*.go src/*.mod .gitignore commit.sh LICENSE README.md
|
||||||
|
git commit -m "$1"
|
||||||
|
git push
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# mutn(1) completion
|
||||||
|
|
||||||
|
_path_gen() {
|
||||||
|
local mutn_path="$HOME/.local/share/libmutton"
|
||||||
|
local glob_status=$(shopt -p globstar)
|
||||||
|
[ -z "${glob_status##*u*}" ] && shopt -s globstar # if recursive globbing is disabled, enable it
|
||||||
|
local full_paths=("$mutn_path"/**/*.gpg)
|
||||||
|
$glob_status # set recursive globbing to user default
|
||||||
|
for scan_path in "${full_paths[@]}"; do
|
||||||
|
trimmed_paths+=("${scan_path:${#mutn_path}:-4}")
|
||||||
|
done
|
||||||
|
if [ "${trimmed_paths[0]}" == '/**/*' ]; then trimmed_paths[0]=help; fi
|
||||||
|
} &&
|
||||||
|
|
||||||
|
_mutn_completions() {
|
||||||
|
local cur=${COMP_WORDS[COMP_CWORD]}
|
||||||
|
local prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||||
|
|
||||||
|
case $prev in
|
||||||
|
mutn )
|
||||||
|
while read -r; do ITEM=${REPLY// /\\ }; COMPREPLY+=( "$ITEM" ); done < <( compgen -W "$(printf "'%s' " "${trimmed_paths[@]}")" -- "$cur" )
|
||||||
|
;;
|
||||||
|
/* )
|
||||||
|
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "add gen edit copy shear" -- "$cur" )
|
||||||
|
;;
|
||||||
|
add )
|
||||||
|
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 url note" -- "$cur" )
|
||||||
|
;;
|
||||||
|
edit )
|
||||||
|
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "password username url note relocate" -- "$cur" )
|
||||||
|
;;
|
||||||
|
gen )
|
||||||
|
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "update" -- "$cur" )
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
} &&
|
||||||
|
_path_gen && complete -F _mutn_completions mutn
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#compdef mutn
|
||||||
|
|
||||||
|
mutn_path="$HOME/.local/share/libmutton"
|
||||||
|
full_paths=("$mutn_path"/**/*.gpg)
|
||||||
|
trimmed_paths=()
|
||||||
|
for scan_path in "${full_paths[@]}"; do
|
||||||
|
trimmed_paths+=("${${scan_path#$mutn_path}%????}")
|
||||||
|
done
|
||||||
|
[[ -z $trimmed_paths ]] && trimmed_paths=(help)
|
||||||
|
|
||||||
|
case ${words[-2]} in
|
||||||
|
mutn )
|
||||||
|
compadd $trimmed_paths
|
||||||
|
;;
|
||||||
|
/* )
|
||||||
|
compadd {add,gen,edit,copy,shear}
|
||||||
|
;;
|
||||||
|
add )
|
||||||
|
compadd {password,note,folder}
|
||||||
|
;;
|
||||||
|
copy )
|
||||||
|
compadd {password,username,url,note}
|
||||||
|
;;
|
||||||
|
edit )
|
||||||
|
compadd {password,username,url,note,relocate}
|
||||||
|
;;
|
||||||
|
gen )
|
||||||
|
compadd update
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
termy "golang.org/x/crypto/ssh/terminal"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// invisible variables
|
||||||
|
var home, _ = os.UserHomeDir()
|
||||||
|
|
||||||
|
// exported variables
|
||||||
|
var (
|
||||||
|
RootLength = len(EntryRoot)
|
||||||
|
Width, _, _ = termy.GetSize(int(os.Stdout.Fd()))
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// EntryRoot path to libmutton entry directory
|
||||||
|
var EntryRoot = home + "/.local/share/sshyp"
|
||||||
|
|
||||||
|
// exported constants
|
||||||
|
const (
|
||||||
|
PathSeparator = "/"
|
||||||
|
Windows = false
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// exported variables
|
||||||
|
var EntryRoot = home + "\\AppData\\Local\\libmutton\\entries"
|
||||||
|
|
||||||
|
// exported constants
|
||||||
|
const (
|
||||||
|
PathSeparator = "\\"
|
||||||
|
Windows = true
|
||||||
|
)
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fileListRoot []string
|
||||||
|
fileListMain []string
|
||||||
|
dirListMain []string
|
||||||
|
)
|
||||||
|
|
||||||
|
// processing for printing file entries (determines color, line wrapping, and prints)
|
||||||
|
func printFileEntry(entry string, lastSlash int, charCounter int, colorAlternator int8) (int, int8) {
|
||||||
|
// determine color to print fileEntryName (alternate each time function is run)
|
||||||
|
var colorCode string
|
||||||
|
if colorAlternator > 0 {
|
||||||
|
colorCode = ""
|
||||||
|
} else {
|
||||||
|
colorCode = "\033[38;5;8m"
|
||||||
|
}
|
||||||
|
colorAlternator = -colorAlternator
|
||||||
|
|
||||||
|
// trim the containing directory and file extension from the entry to determine fileEntryName
|
||||||
|
fileEntryName := entry[lastSlash:]
|
||||||
|
fileEntryName = fileEntryName[:len(fileEntryName)-4]
|
||||||
|
|
||||||
|
// determine whether to wrap to a new line (+1 is to account for trailing spaces)
|
||||||
|
charCounter += len(fileEntryName) + 1
|
||||||
|
if charCounter >= Width {
|
||||||
|
charCounter = len(fileEntryName) + 1
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// print fileEntryName to screen
|
||||||
|
fmt.Printf("%s%s\033[0m ", colorCode, fileEntryName)
|
||||||
|
|
||||||
|
return charCounter, colorAlternator
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryListGen generates and displays full entry list
|
||||||
|
func EntryListGen() {
|
||||||
|
fmt.Print("\nfor a list of usable commands, run \"mutn help\"\n\n\033[38;5;0;48;5;15mlibmutton entries:\033[0m")
|
||||||
|
|
||||||
|
// walk entry directory
|
||||||
|
_ = filepath.WalkDir(EntryRoot,
|
||||||
|
func(fullPath string, entry fs.DirEntry, err error) error {
|
||||||
|
|
||||||
|
// trim root path from each path before storing
|
||||||
|
trimmedPath := fullPath[RootLength:]
|
||||||
|
|
||||||
|
// create three separate slices for root-level entries, all other entries, and all subdirectories
|
||||||
|
// root-level entries get their own slice so that they can be alphabetically sorted without the chance of directories being placed in from of them
|
||||||
|
if !entry.IsDir() && strings.Count(trimmedPath, PathSeparator) == 1 {
|
||||||
|
fileListRoot = append(fileListRoot, trimmedPath)
|
||||||
|
} else if !entry.IsDir() {
|
||||||
|
fileListMain = append(fileListMain, trimmedPath)
|
||||||
|
} else {
|
||||||
|
dirListMain = append(dirListMain, trimmedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// fileListRoot iteration
|
||||||
|
charCounter := 0 // set to track whether to line-wrap based on character count in line
|
||||||
|
var colorAlternator int8 = 1 // set to allow alternating colors for each printed entry name
|
||||||
|
ran := false // set to track whether the loop has been run yet
|
||||||
|
for _, file := range fileListRoot {
|
||||||
|
|
||||||
|
// print root ("/") header if this is the first run of the loop
|
||||||
|
// must be done within loop in order to allow not printing the header if there are no root-level entries
|
||||||
|
if !ran {
|
||||||
|
ran = true
|
||||||
|
fmt.Print("\n\n\033[38;5;7;48;5;8m/\033[0m\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get index of last occurrence of pathSeparator in trimmed entry path (used to split entry's containing directory and entry's name) and pass to printFileEntry
|
||||||
|
charCounter, colorAlternator = printFileEntry(file, strings.LastIndex(file, PathSeparator)+1, charCounter, colorAlternator)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirListMain iteration
|
||||||
|
nextStartIndex := 0 // set to track where to resume when iterating through fileListMain
|
||||||
|
var containsSubdirectory bool // set to track whether the current directory contains a subdirectory - if it does, empty directory warnings will not be printed
|
||||||
|
for i, directory := range dirListMain {
|
||||||
|
|
||||||
|
// reset formatting variables for new directory
|
||||||
|
charCounter = 0
|
||||||
|
colorAlternator = 1
|
||||||
|
|
||||||
|
// check if next directory is within the current one
|
||||||
|
if len(dirListMain) > i+1 {
|
||||||
|
nextDir := dirListMain[i+1]
|
||||||
|
if directory == nextDir[:strings.LastIndex(nextDir, PathSeparator)] {
|
||||||
|
containsSubdirectory = true
|
||||||
|
} else {
|
||||||
|
containsSubdirectory = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
containsSubdirectory = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileListMain iteration
|
||||||
|
ran = false // set to track whether the loop has been run yet
|
||||||
|
for _, file := range fileListMain[nextStartIndex:] {
|
||||||
|
|
||||||
|
// get index of last occurrence of pathSeparator in trimmed entry path (used to split entry's containing directory and entry's name)
|
||||||
|
lastSlash := strings.LastIndex(file, PathSeparator) + 1
|
||||||
|
|
||||||
|
// print the current file if it belongs in the current directory - otherwise, break the loop and move on to the next directory
|
||||||
|
if file[:lastSlash-1] == directory {
|
||||||
|
|
||||||
|
// print directory header if this is the first run of the loop
|
||||||
|
// must be done within loop in order to allow not printing the header if its directory contains no entries
|
||||||
|
if !ran {
|
||||||
|
ran = true
|
||||||
|
// for consistency, format directories with UNIX-style path separators on all platforms
|
||||||
|
if !Windows {
|
||||||
|
fmt.Printf("\n\n\033[38;5;7;48;5;8m%s/\033[0m\n", directory)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\n\n\033[38;5;7;48;5;8m%s/\033[0m\n", strings.ReplaceAll(directory, PathSeparator, "/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextStartIndex++ // increment to prevent having to iterate through ALL of fileListMain each time
|
||||||
|
charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, colorAlternator)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// display empty directory warning if the last printed directory contained no entries or subdirectories
|
||||||
|
if !ran && !containsSubdirectory {
|
||||||
|
// for consistency, format directories with UNIX-style path separators on all platforms
|
||||||
|
if !Windows {
|
||||||
|
fmt.Printf("\n\n\033[38;5;7;48;5;8m%s/\033[0m\n", directory)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\n\n\033[38;5;7;48;5;8m%s/\033[0m\n", strings.ReplaceAll(directory, PathSeparator, "/"))
|
||||||
|
}
|
||||||
|
fmt.Print("\033[38;5;9m-empty directory-\033[0m")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// print trailing new lines for proper spacing after entry list is complete
|
||||||
|
fmt.Print("\n\n")
|
||||||
|
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
go 1.21.6
|
||||||
|
|
||||||
|
require golang.org/x/crypto v0.18.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
|
golang.org/x/term v0.16.0 // indirect
|
||||||
|
)
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
args := os.Args[1:]
|
||||||
|
argsCount := len(args)
|
||||||
|
|
||||||
|
if argsCount == 0 {
|
||||||
|
|
||||||
|
EntryListGen()
|
||||||
|
|
||||||
|
} else if argsCount == 1 {
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
HelpMain()
|
||||||
|
case "version", "-v":
|
||||||
|
Version()
|
||||||
|
default:
|
||||||
|
HelpMain()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func HelpMain() {
|
||||||
|
fmt.Print("\n\033[1mMUTN | Copyright (c) 2024 Randall Winkhart\033[0m\n" + `
|
||||||
|
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.
|
||||||
|
|
||||||
|
` + "\033[1mUsage:\033[0m" + ` mutn [/<entry name> [argument] [option]] | [argument]
|
||||||
|
|
||||||
|
` + "\033[1mArguments:\033[0m" + `
|
||||||
|
help/--help/-h Bring up this menu
|
||||||
|
version/-v Display version and license information
|
||||||
|
init Set up MUTN
|
||||||
|
tweak Change configuration options
|
||||||
|
add Add an entry
|
||||||
|
gen Generate a new password
|
||||||
|
edit Edit an existing entry
|
||||||
|
copy Copy details of an entry to your clipboard
|
||||||
|
shear Delete an existing entry
|
||||||
|
sync Manually sync the entry directory
|
||||||
|
|
||||||
|
` + "\033[1mOptions:\033[0m" + `
|
||||||
|
add:
|
||||||
|
password/-p Add a password entry
|
||||||
|
note/-n Add a note entry
|
||||||
|
folder/-f Add a new folder for entries
|
||||||
|
edit:
|
||||||
|
rename/relocate/-r Rename or relocate an entry
|
||||||
|
username/-u Change the username of an entry
|
||||||
|
password/-p Change the password of an entry
|
||||||
|
url/-l Change the url attached to an entry
|
||||||
|
note/-n Change the note attached to an entry
|
||||||
|
copy:
|
||||||
|
username/-u Copy the username of an entry to your clipboard
|
||||||
|
password/-p Copy the password of an entry to your clipboard
|
||||||
|
url/-l Copy the url of an entry to your clipboard
|
||||||
|
note/-n Copy the note of an entry to your clipboard
|
||||||
|
gen:
|
||||||
|
update/-u Generate a password for an existing entry
|
||||||
|
|
||||||
|
` + "\033[1mTip 1:\033[0m" + ` You can quickly read an entry with "mutn /<entry name>"
|
||||||
|
` + "\033[1mTip 2:\033[0m" + ` Type "mutn" to view a list of saved entries` + "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Version() {
|
||||||
|
fmt.Print("\n\033[1mMUTN | Copyright (c) 2024 Randall Winkhart\033[0m" + `
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||||
|
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
OR OTHER DEALINGS IN THE SOFTWARE.` +
|
||||||
|
"\n\n---------------------------------------------------------" +
|
||||||
|
"\n\n MUTN is a simple, self-hosted,\n SSH-synchronized password manager based on libmutton\n\n" +
|
||||||
|
" .. \033[38;2;206;48;98m♥♥ ♥♥\033[0m ..\n" +
|
||||||
|
" /()\\''.''. \033[38;2;206;48;98m♥♥♥♥♥♥♥\033[0m .''.''/()\\ _)\n" +
|
||||||
|
" _. : * \033[38;2;206;48;98m♥♥♥♥♥\033[0m * : <[◎]|_|=\n" +
|
||||||
|
" }-}-*] `..'..' \033[38;2;206;48;98m♥♥♥\033[0m `..'..' |\n" +
|
||||||
|
" ◎-◎ // \\\\ \033[38;2;206;48;98m♥\033[0m // \\\\ /|\\\n" +
|
||||||
|
"\033[38;2;1;173;216m<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" +
|
||||||
|
"\\\033[38;5;15;48;5;15m \033[0m\033[38;2;1;173;216m/\n" +
|
||||||
|
"\\\033[38;5;15;48;5;15m \033[0mMUTN Version 0.0.1\033[38;5;15;48;5;15m \033[0m\033[38;2;1;173;216m/\n" +
|
||||||
|
"\\\033[38;5;15;48;5;15m \033[0mThe Butchered Update\033[38;5;15;48;5;15m \033[0m\033[38;2;1;173;216m/\n" +
|
||||||
|
"\\\033[38;5;15;48;5;15m \033[0m\033[38;2;1;173;216m/\n" +
|
||||||
|
"\\\033[38;5;15;48;5;15m \033[0mCopyright (C) 2024 Randall Winkhart\033[38;5;15;48;5;15m \033[0m\033[38;2;1;173;216m/\n" +
|
||||||
|
"\\\033[38;5;15;48;5;15m \033[0m\033[38;2;1;173;216m/\n" +
|
||||||
|
"<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\033[0m\n" +
|
||||||
|
"\nFor more information, see:\n" +
|
||||||
|
"https://github.com/rwinkhart/MUTN\n" +
|
||||||
|
"https://github.com/rwinkhart/libmutton\n\n")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user