mirror of
https://github.com/rwinkhart/MUTN.git
synced 2026-08-27 20:36:29 -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