Initial code commit

This commit is contained in:
2024-01-16 14:35:34 -05:00
parent c20c8bc349
commit af6600de80
12 changed files with 431 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
*
+33
View File
@@ -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
Executable
+4
View File
@@ -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
+41
View File
@@ -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
+30
View File
@@ -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
+15
View File
@@ -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()))
)
+12
View File
@@ -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
)
+12
View File
@@ -0,0 +1,12 @@
//go:build windows
package main
// exported variables
var EntryRoot = home + "\\AppData\\Local\\libmutton\\entries"
// exported constants
const (
PathSeparator = "\\"
Windows = true
)
+153
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
}
}
+91
View File
@@ -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")
}