diff --git a/main.go b/main.go index 72c7f73..2d90002 100644 --- a/main.go +++ b/main.go @@ -87,16 +87,17 @@ func main() { switch args[3] { case "password", "-p": if argsCount == 4 { - cli.AddPasswordEntry(targetLocation, true) + cli.AddEntry(targetLocation, true, false) } else { switch args[4] { case "show", "-s": - cli.AddPasswordEntry(targetLocation, false) + cli.AddEntry(targetLocation, false, false) default: - cli.AddPasswordEntry(targetLocation, true) + cli.AddEntry(targetLocation, true, false) } } - case "note", "-n": // TODO cli.AddNoteEntry(targetLocation) + case "note", "-n": + cli.AddEntry(targetLocation, true, true) case "folder", "-f": offline.AddFolder(targetLocation) default: diff --git a/src/cli/add.go b/src/cli/add.go index 8de1c5f..476cddb 100644 --- a/src/cli/add.go +++ b/src/cli/add.go @@ -6,21 +6,28 @@ import ( "os" ) -// AddPasswordEntry prompts the user for all information relevant to a password entry and writes the entry to an encrypted file at targetLocation -func AddPasswordEntry(targetLocation string, hidePassword bool) { +// AddEntry creates a new entry at targetLocation by taking user input via CLI prompts +func AddEntry(targetLocation string, hidePassword bool, isNote bool) { _, isAccessible := offline.TargetIsFile(targetLocation, false, 0) if isAccessible { fmt.Println(offline.AnsiError + "Target location already exists" + offline.AnsiReset) os.Exit(1) } - username := input("Username: ") - password := inputHidden("Password: ") - url := input("URL: ") - // TODO implement notes editor - notes := "" + var unencryptedEntry []string + + if !isNote { + username := input("Username: ") + password := inputHidden("Password: ") + url := input("URL: ") + // TODO prompt for optional note + unencryptedEntry = []string{password, username, url} + } else { + note := newNote() + unencryptedEntry = append([]string{"", "", ""}, note...) + fmt.Println() + } - unencryptedEntry := []string{password, username, url, notes} offline.WriteEntry(targetLocation, unencryptedEntry) fmt.Println(ansiBold + "\nEntry Preview:" + offline.AnsiReset) EntryReader(unencryptedEntry, hidePassword) diff --git a/src/cli/entryReader.go b/src/cli/entryReader.go index 0fd3d0c..92efc9a 100644 --- a/src/cli/entryReader.go +++ b/src/cli/entryReader.go @@ -13,7 +13,7 @@ func EntryReader(decryptedEntry []string, hidePassword bool) { fmt.Println() // track if extended notes have been printed (to avoid printing an extra newline) - notesFlag := false + var notesFlag bool for i := range decryptedEntry { @@ -46,26 +46,32 @@ func EntryReader(decryptedEntry []string, hidePassword bool) { // if the fourth field (notes begin) is not empty, print it if decryptedEntry[3] != "" { fmt.Println(ansiDirectoryHeader + "Notes:" + offline.AnsiReset + "\n" + decryptedEntry[3]) - } - default: - // print extended notes line - fmt.Println(decryptedEntry[i]) - - // indicate that extended notes have been printed - if !notesFlag { + // indicate that the first notes line was printed (not empty) notesFlag = true } + default: + // print notes header if first notes line was blank + if !notesFlag { + fmt.Println(ansiDirectoryHeader + "Notes:" + offline.AnsiReset) + // indicate that notes have been printed + notesFlag = true + } + + // print extended notes line + fmt.Println(decryptedEntry[i]) } } - // print trailing newline if extended notes were printed + // print trailing newline if notes were printed if notesFlag { fmt.Println() } os.Exit(0) } +// EntryReaderShortcut is a shortcut for EntryReader that decrypts a GPG-encrypted file and prints the contents func EntryReaderShortcut(targetLocation string, hidePassword bool) { if isFile, _ := offline.TargetIsFile(targetLocation, true, 2); isFile { EntryReader(offline.DecryptGPG(targetLocation), hidePassword) } + // do not exit, as this is the job of EntryReader } diff --git a/src/cli/utilitiesMisc.go b/src/cli/utilitiesMisc.go index 41f53f0..2022bbf 100644 --- a/src/cli/utilitiesMisc.go +++ b/src/cli/utilitiesMisc.go @@ -1,9 +1,12 @@ package cli import ( + "bufio" "fmt" + "github.com/rwinkhart/MUTN/src/offline" "golang.org/x/crypto/ssh/terminal" "os" + "os/exec" ) // input prompts the user for input and returns the input as a string @@ -22,3 +25,34 @@ func inputHidden(prompt string) string { fmt.Println() return password } + +// newNote uses the user-specified text editor to create a new note and returns the note as a slice of strings +func newNote() []string { + tempFile := offline.CreateTempFile() + defer os.Remove(tempFile.Name()) + editor := offline.ReadConfig([]string{"textEditor"})[0] + cmd := exec.Command(editor, tempFile.Name()) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + fmt.Println(offline.AnsiError + "Failed to write note with " + editor + offline.AnsiReset) + os.Exit(1) + } + + file, err := os.Open(tempFile.Name()) + if err != nil { + fmt.Println(offline.AnsiError + "Failed to open temporary file (\"" + tempFile.Name() + "\") " + err.Error() + offline.AnsiReset) + os.Exit(1) + } + defer file.Close() + + var note []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + note = append(note, scanner.Text()) + } + + return offline.RemoveTrailingEmptyStrings(note) +} diff --git a/src/offline/configParser.go b/src/offline/configParser.go index 60acb3e..b2d1b34 100644 --- a/src/offline/configParser.go +++ b/src/offline/configParser.go @@ -6,52 +6,33 @@ import ( "os" ) -var Config = make(map[string]interface{}) - -func ReadConfig() { +// ReadConfig reads the libmutton.ini file and returns a map of the requested values +// requires readMap: a map of section names to key names (indicates requested values) +// returns configMap: a map of key names to values (sections are irrelevant) +func ReadConfig(readKeys []string) []string { cfg, err := ini.Load(home + "/.config/libmutton/libmutton.ini") if err != nil { fmt.Println(AnsiError + "Failed to load libmutton.ini" + AnsiReset) os.Exit(1) } - // OFFLINE - Config["gpgID"] = cfg.Section("OFFLINE").Key("gpgID").String() - Config["textEditor"] = cfg.Section("OFFLINE").Key("textEditor").String() + var config []string - // ONLINE (conditional) - onlineMode, err := cfg.Section("OFFLINE").Key("onlineMode").Bool() - if err != nil { - fmt.Println(AnsiError + "Failed to enable online (synced) mode - [OFFLINE]/onlineMode in libmutton.ini must be a boolean value - continuing in offline mode" + AnsiReset) - onlineMode = false - } - if onlineMode { - Config["sshError"], err = cfg.Section("ONLINE").Key("sshError").Bool() - if err != nil { - cfg.Section("ONLINE").Key("sshError").SetValue("true") - } - Config["netPinEnabled"], err = cfg.Section("ONLINE").Key("netPinEnabled").Bool() - if err != nil { - cfg.Section("ONLINE").Key("netPinEnabled").SetValue("false") - } - Config["remoteUser"] = cfg.Section("ONLINE").Key("remoteUser").String() - Config["remoteIP"] = cfg.Section("ONLINE").Key("remoteIP").String() - Config["remotePort"], err = cfg.Section("ONLINE").Key("remotePort").Int() - if err != nil { - fmt.Println(AnsiError + "Failed to enable online (synced) mode - [ONLINE]/remotePort in libmutton.ini must be an integer value- continuing in offline mode" + AnsiReset) - // remotePort == 0 represents offline-only mode - Config["remotePort"] = 0 - } - Config["identityFile"] = cfg.Section("ONLINE").Key("identityFile").String() - } else { - // remotePort == 0 represents offline-only mode - Config["remotePort"] = 0 + for _, key := range readKeys { + config = append(config, cfg.Section("LIBMUTTON").Key(key).String()) } - // DEBUG config - //for key, value := range Config { - // fmt.Print(key + ": ") - // fmt.Println(value) - //} - + return config } + +// libmuttn.ini layout +// [LIBMUTTON] +// gpgID = +// textEditor = +// onlineMode = +// sshError = +// netPinEnabled = +// remoteUser = +// remoteIP = +// remotePort = +// identityFile = diff --git a/src/offline/gpg.go b/src/offline/gpg.go index aa65163..26ba3bd 100644 --- a/src/offline/gpg.go +++ b/src/offline/gpg.go @@ -27,8 +27,7 @@ func DecryptGPG(targetLocation string) []string { // EncryptGPG encrypts a slice of strings using GPG and returns the encrypted data as a byte slice func EncryptGPG(input []string) []byte { - ReadConfig() - cmd := exec.Command("gpg", "-q", "-r", Config["gpgID"].(string), "-e") + cmd := exec.Command("gpg", "-q", "-r", ReadConfig([]string{"gpgID"})[0], "-e") writeToStdin(cmd, strings.Join(input, "\n")) encryptedBytes, err := cmd.Output() if err != nil { diff --git a/src/offline/utilitiesMisc.go b/src/offline/utilitiesMisc.go index 53557dc..d9c37b6 100644 --- a/src/offline/utilitiesMisc.go +++ b/src/offline/utilitiesMisc.go @@ -43,3 +43,23 @@ func writeToStdin(cmd *exec.Cmd, input string) { io.WriteString(stdin, input) }() } + +// CreateTempFile creates a temporary file and returns a pointer to it +func CreateTempFile() *os.File { + tempFile, err := os.CreateTemp("", "") + if err != nil { + fmt.Println(AnsiError + "Failed to create temporary file: " + err.Error() + AnsiReset) + os.Exit(1) + } + return tempFile +} + +// RemoveTrailingEmptyStrings removes empty strings from the end of a slice +func RemoveTrailingEmptyStrings(slice []string) []string { + for i := len(slice) - 1; i >= 0; i-- { + if slice[i] != "" { + return slice[:i+1] + } + } + return []string{} +}