diff --git a/mutn.go b/mutn.go index 92b617d..75dfe62 100644 --- a/mutn.go +++ b/mutn.go @@ -54,11 +54,11 @@ func main() { other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard) } case "edit": - cli.EditEntryField(realPath, true, 0) + cli.EditEntryField(realPath, 0) case "gen": - cli.AddEntry(realPath, true, 1) + cli.AddEntry(realPath, 1) case "add": - cli.AddEntry(realPath, true, 0) + cli.AddEntry(realPath, 0) case "shear": err := syncclient.ShearRemoteFromClient(args[1], false) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory if err != nil { @@ -70,11 +70,11 @@ func main() { } else { // handle "show" or "-s" argument for gen, edit, and add switch args[2] { case "edit": - cli.EditEntryField(realPath, false, 0) + cli.EditEntryField(realPath, 0) case "gen": - cli.AddEntry(realPath, false, 1) + cli.AddEntry(realPath, 1) case "add": - cli.AddEntry(realPath, false, 0) + cli.AddEntry(realPath, 0) default: cli.HelpMain() } @@ -97,7 +97,7 @@ func main() { case "note", "-n": field = 4 case "menu", "-m": - cli.CopyMenu(realPath, nil, "") + cli.CopyMenu(args[1], nil, "") default: cli.HelpCopy() } @@ -128,29 +128,29 @@ func main() { cli.HelpEdit() } if argsCount == 4 { - cli.EditEntryField(realPath, true, field) + cli.EditEntryField(realPath, field) } else { switch args[4] { case "show", "-s": - cli.EditEntryField(realPath, false, field) + cli.EditEntryField(realPath, field) default: - cli.EditEntryField(realPath, true, field) + cli.EditEntryField(realPath, field) } } case "gen": if argsCount == 4 { switch args[3] { case "update", "-u": - cli.GenUpdate(realPath, true) + cli.GenUpdate(realPath) default: cli.HelpGen() } } else if args[3] == "update" || args[3] == "-u" { switch args[4] { case "show", "-s": - cli.GenUpdate(realPath, false) + cli.GenUpdate(realPath) default: - cli.GenUpdate(realPath, true) + cli.GenUpdate(realPath) } } cli.HelpGen() @@ -158,17 +158,17 @@ func main() { switch args[3] { case "password", "-pw": if argsCount == 4 { - cli.AddEntry(realPath, true, 0) + cli.AddEntry(realPath, 0) } else { switch args[4] { case "show", "-s": - cli.AddEntry(realPath, false, 0) + cli.AddEntry(realPath, 0) default: - cli.AddEntry(realPath, true, 0) + cli.AddEntry(realPath, 0) } } case "note", "-n": - cli.AddEntry(realPath, 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 { diff --git a/src/cli/add.go b/src/cli/add.go index 418c6ce..5eb39b9 100644 --- a/src/cli/add.go +++ b/src/cli/add.go @@ -9,14 +9,14 @@ import ( // 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(realPath string, hideSecrets bool, entryType uint8) { +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 string if entryType < 2 { @@ -33,20 +33,20 @@ func AddEntry(realPath string, hideSecrets bool, entryType uint8) { 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{password, username, totp, url}, note...) } else { - decryptedEntry = []string{password, username, totp, url} + decSlice = []string{password, username, totp, url} } } else { note, _ := editNote([]string{}) - decryptedEntry = append([]string{"", "", "", ""}, note...) + decSlice = append([]string{"", "", "", ""}, note...) } // write and preview the new entry if password != "" { - writeEntryCLI(realPath, decryptedEntry, hideSecrets, true, "") + writeEntryCLI(realPath, decSlice, true, "") } else { - writeEntryCLI(realPath, decryptedEntry, hideSecrets, false, "") + writeEntryCLI(realPath, decSlice, false, "") } } diff --git a/src/cli/copyMenu.go b/src/cli/copyMenu.go index e7dc036..f38b238 100644 --- a/src/cli/copyMenu.go +++ b/src/cli/copyMenu.go @@ -12,12 +12,14 @@ import ( "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. -// Only one of realPath or decSlice should be provided. -func CopyMenu(realPath string, decSlice []string, oldPassword string) { +// decSlice can be left nil to decrypt the entry specified by vanityPath. +func CopyMenu(vanityPath string, decSlice []string, oldPassword string) { + realPath := global.GetRealPath(vanityPath) var err error if decSlice == nil { // decrypt entry @@ -25,21 +27,36 @@ func CopyMenu(realPath string, decSlice []string, oldPassword string) { if err != nil { other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption) } + } else { + fmt.Print("\n\n") + _, err := syncclient.RunJob(false) + 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 fields []string + var mainFieldPopulated bool for i := range indices { if len(decSlice) > indices[i] && decSlice[indices[i]] != "" { fields = append(fields, fieldStrings[i]) + mainFieldPopulated = true if indices[i] == 0 && oldPassword != "" { fields = append(fields, "Old Password") } } } + // if no non-first-line notes are populated, render notes and exit + if !mainFieldPopulated { + EntryReader(vanityPath, decSlice, true) + fmt.Println("Entry has no copyable fields, exiting...") + 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.SIGTERM) diff --git a/src/cli/edit.go b/src/cli/edit.go index 7275b3a..6be0193 100644 --- a/src/cli/edit.go +++ b/src/cli/edit.go @@ -27,9 +27,9 @@ func RenameCli(oldVanityPath string) { } // EditEntryField edits a field of an entry at realPath (user input). -func EditEntryField(realPath string, hideSecrets bool, field int) { +func EditEntryField(realPath string, field int) { // fetch old entry data (with all required lines present) - decryptedEntry, err := core.GetOldEntryData(realPath, field) + decSlice, err := core.GetOldEntryData(realPath, field) if err != nil { other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead) } @@ -38,49 +38,49 @@ func EditEntryField(realPath string, hideSecrets bool, field int) { var oldPassword string switch field { case 0: - oldPassword = decryptedEntry[field] - decryptedEntry[field] = string(front.InputHidden("Password:")) + oldPassword = decSlice[field] + decSlice[field] = string(front.InputHidden("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.InputHidden("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 if field == 0 { - writeEntryCLI(realPath, decryptedEntry, hideSecrets, true, oldPassword) + writeEntryCLI(realPath, decSlice, true, oldPassword) } else { - writeEntryCLI(realPath, decryptedEntry, hideSecrets, false, oldPassword) + writeEntryCLI(realPath, decSlice, false, oldPassword) } } // GenUpdate generates a new password for an entry at realPath (user input). -func GenUpdate(realPath string, hideSecrets bool) { +func GenUpdate(realPath string) { // fetch old entry data - decryptedEntry, err := core.GetOldEntryData(realPath, 0) + decSlice, err := core.GetOldEntryData(realPath, 0) if err != nil { other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead) } // generate a new password - oldPassword := decryptedEntry[0] - decryptedEntry[0] = inputPasswordGen() + oldPassword := decSlice[0] + decSlice[0] = inputPasswordGen() // write and preview the modified entry - writeEntryCLI(realPath, decryptedEntry, hideSecrets, true, oldPassword) + 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). diff --git a/src/cli/entryReader.go b/src/cli/entryReader.go index 1eee939..e2b3ec3 100644 --- a/src/cli/entryReader.go +++ b/src/cli/entryReader.go @@ -9,59 +9,52 @@ 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" + 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" + 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")) + markdownNotesString, _ := r.Render(strings.Join(decSlice[4:], "\n")) // print the rendered Markdown notes fmt.Print(markdownNotesString) @@ -70,13 +63,6 @@ fieldLoop: break fieldLoop } } - - if syncEnabled { - _, err := syncclient.RunJob(false) - if err != nil { - other.PrintError("Failed to sync entries: "+err.Error(), global.ErrorSyncProcess) - } - } } // EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader. @@ -89,6 +75,5 @@ func EntryReaderDecrypt(realPath string, hideSecrets bool) { if err != nil { other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption) } - EntryReader(decSlice, 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) } diff --git a/src/cli/utilitiesMisc.go b/src/cli/utilitiesMisc.go index dd45933..d207080 100644 --- a/src/cli/utilitiesMisc.go +++ b/src/cli/utilitiesMisc.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "os" "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/front" @@ -29,17 +28,14 @@ func inputPasswordGen() string { } // writeEntryCLI writes an entry to realPath and previews it (errors if no data is supplied). -func writeEntryCLI(realPath string, decSlice []string, hideSecrets bool, passwordIsNew bool, oldPassword string) { +func writeEntryCLI(realPath string, decSlice []string, passwordIsNew bool, oldPassword string) { if core.EntryIsNotEmpty(decSlice) { err := core.WriteEntry(realPath, decSlice, passwordIsNew) if err != nil { other.PrintError("Failed to write entry: "+err.Error(), back.ErrorWrite) } // preview the entry - fmt.Println(back.AnsiBold + "\nEntry Preview:" + back.AnsiReset) - EntryReader(decSlice, hideSecrets, true) CopyMenu("", decSlice, oldPassword) - os.Exit(0) } else { other.PrintError("No data supplied for entry", back.ErrorTargetNotFound) }