Enable entry password aging (sync with latest libmutton development version)

This commit is contained in:
2025-11-23 03:34:31 -05:00
parent 300c433909
commit 1a9de90f89
12 changed files with 106 additions and 76 deletions
+9 -11
View File
@@ -23,20 +23,18 @@ Additionally, MUTN is available as a source package ("[mutn](https://aur.archlin
After installing, please review the [usage guide](https://github.com/rwinkhart/MUTN/blob/main/wiki/usage.md). After installing, please review the [usage guide](https://github.com/rwinkhart/MUTN/blob/main/wiki/usage.md).
# Roadmap # Roadmap
### Release v0.3.1
- [x] Add interactive copy menu
- [ ] Send user to this menu after add/gen
- [ ] libmutton v0.5.0
### Release v0.4.0 ### Release v0.4.0
- [ ] libmutton v0.6.0 - Add interactive copy menu
- [ ] Password aging support - Send user to this menu after add/gen
- [ ] Add yellow/red dot indicators to entry list readout for when passwords should be changed - libmutton v0.5.0
- Password aging support
- Add green/yellow/red dot indicators to entry list readout for when passwords should be changed
### Release v0.5.0 ### Release v0.5.0
- [ ] libmutton v0.7.0 - libmutton v0.6.0
- [ ] Implement "netpin" (quick-unlock) - Implement "netpin" (quick-unlock)
### Release v0.6.0 ### Release v0.6.0
- [ ] Switch to fully compliant Markdown (do not preserve new lines) - Switch to fully compliant Markdown (do not preserve new lines)
- [ ] Switch to alternative Markdown renderer (minimark or BEAN) - Switch to alternative Markdown renderer (minimark or BEAN)
### Release v1.0.0 - Distribution packages ### Release v1.0.0 - Distribution packages
- [ ] Create packaging scripts - [ ] Create packaging scripts
- [x] Stable source PKGBUILD - [x] Stable source PKGBUILD
+1 -1
View File
@@ -1,4 +1,4 @@
.TH MUTN 1 "09 November 2025" "v0.3.A" "MUTN man page" .TH MUTN 1 "23 November 2025" "v0.D.0" "MUTN man page"
.SH NAME .SH NAME
\fBmutn\fR - Simple, self-hosted, SSH-synchronized password and note management based on libmutton. It is the successor to sshyp. \fBmutn\fR - Simple, self-hosted, SSH-synchronized password and note management based on libmutton. It is the successor to sshyp.
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.25.4
require ( require (
github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/glamour v0.7.0
github.com/rwinkhart/go-boilerplate v0.1.1-0.20251110055016-10ee4f91fcb6 github.com/rwinkhart/go-boilerplate v0.1.1-0.20251110055016-10ee4f91fcb6
github.com/rwinkhart/libmutton v0.4.3-0.20251114034746-04e1b0dd37d3 github.com/rwinkhart/libmutton v0.4.3-0.20251123082520-3ddce4bdf5a7
golang.org/x/term v0.36.0 golang.org/x/term v0.36.0
) )
+2 -2
View File
@@ -46,8 +46,8 @@ github.com/rwinkhart/go-highlite v0.1.1 h1:9TxbRhYVfD/3YaEgNk1BtEiZ0t8/5mxDUZqNM
github.com/rwinkhart/go-highlite v0.1.1/go.mod h1:mWLMtCWcyV0BG4NeyPyAUGMjPvI0ds6vXmUq89QwBFA= github.com/rwinkhart/go-highlite v0.1.1/go.mod h1:mWLMtCWcyV0BG4NeyPyAUGMjPvI0ds6vXmUq89QwBFA=
github.com/rwinkhart/go-winio v0.1.0 h1:b72agLW+dETGmhR3VbcbwnStfgKfc5AfgJOXBJDkaHg= github.com/rwinkhart/go-winio v0.1.0 h1:b72agLW+dETGmhR3VbcbwnStfgKfc5AfgJOXBJDkaHg=
github.com/rwinkhart/go-winio v0.1.0/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= github.com/rwinkhart/go-winio v0.1.0/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
github.com/rwinkhart/libmutton v0.4.3-0.20251114034746-04e1b0dd37d3 h1:dkPUIG9s8YR2y+Xw3tcJ2ee2FTNemRizfSFuASn2Pjw= github.com/rwinkhart/libmutton v0.4.3-0.20251123082520-3ddce4bdf5a7 h1:Yr8RSWi59FN1gBAS0LZ2gasZZ21GNFDMuUiThi/nQug=
github.com/rwinkhart/libmutton v0.4.3-0.20251114034746-04e1b0dd37d3/go.mod h1:7sMUNpRyYES4Z694TAuzt1MR7Iy4nnVd48uTxcvZeis= github.com/rwinkhart/libmutton v0.4.3-0.20251123082520-3ddce4bdf5a7/go.mod h1:7sMUNpRyYES4Z694TAuzt1MR7Iy4nnVd48uTxcvZeis=
github.com/rwinkhart/peercred-mini v0.1.2 h1:4cGWDbv0whvLeVvbUdx84V/9p+2fS+DEXgrA1KxlRFo= github.com/rwinkhart/peercred-mini v0.1.2 h1:4cGWDbv0whvLeVvbUdx84V/9p+2fS+DEXgrA1KxlRFo=
github.com/rwinkhart/peercred-mini v0.1.2/go.mod h1:LLHG7YshHEpbpJJP+Il9nx2dnGj5O3VGE32rWmflj0c= github.com/rwinkhart/peercred-mini v0.1.2/go.mod h1:LLHG7YshHEpbpJJP+Il9nx2dnGj5O3VGE32rWmflj0c=
github.com/rwinkhart/rcw v0.2.3 h1:g1rVaspZnZM8nWupeSdVO827di4ORMZnczw6iXyo01Y= github.com/rwinkhart/rcw v0.2.3 h1:g1rVaspZnZM8nWupeSdVO827di4ORMZnczw6iXyo01Y=
+36 -28
View File
@@ -11,6 +11,7 @@ import (
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front" "github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/go-boilerplate/other" "github.com/rwinkhart/go-boilerplate/other"
"github.com/rwinkhart/libmutton/age"
"github.com/rwinkhart/libmutton/cfg" "github.com/rwinkhart/libmutton/cfg"
"github.com/rwinkhart/libmutton/clip" "github.com/rwinkhart/libmutton/clip"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
@@ -34,32 +35,32 @@ func main() {
// if the first argument is an entry... // if the first argument is an entry...
if strings.HasPrefix(args[1], "/") { if strings.HasPrefix(args[1], "/") {
// store location of target entry // store realPath of target entry
targetLocation := global.TargetLocationFormat(args[1]) realPath := global.GetRealPath(args[1])
// entry reader shortcut (if no other arguments are supplied) // entry reader shortcut (if no other arguments are supplied)
if argsCount == 2 { if argsCount == 2 {
cli.EntryReaderDecrypt(targetLocation, true) cli.EntryReaderDecrypt(realPath, true)
// perform other operations on the entry (if other arguments are supplied) // perform other operations on the entry (if other arguments are supplied)
} else if argsCount == 3 || (argsCount == 4 && (args[3] == "show" || args[3] == "-s")) { } else if argsCount == 3 || (argsCount == 4 && (args[3] == "show" || args[3] == "-s")) {
if argsCount == 3 { // default to "password" if no field is specified (for copy, edit, and add) if argsCount == 3 { // default to "password" if no field is specified (for copy, edit, and add)
switch args[2] { switch args[2] {
case "show", "-s": case "show", "-s":
cli.EntryReaderDecrypt(targetLocation, false) cli.EntryReaderDecrypt(realPath, false)
case "copy": case "copy":
err := clip.CopyShortcut(targetLocation, 0) err := clip.CopyShortcut(realPath, 0)
if err != nil { if err != nil {
other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard) other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard)
} }
case "edit": case "edit":
cli.EditEntryField(targetLocation, true, 0) cli.EditEntryField(realPath, true, 0)
case "gen": case "gen":
cli.AddEntry(targetLocation, true, 1) cli.AddEntry(realPath, true, 1)
case "add": case "add":
cli.AddEntry(targetLocation, true, 0) cli.AddEntry(realPath, true, 0)
case "shear": case "shear":
err := syncclient.ShearRemoteFromClient(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory 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 { if err != nil {
other.PrintError("Failed to shear target: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to shear target: "+err.Error(), back.ErrorWrite)
} }
@@ -69,11 +70,11 @@ func main() {
} else { // handle "show" or "-s" argument for gen, edit, and add } else { // handle "show" or "-s" argument for gen, edit, and add
switch args[2] { switch args[2] {
case "edit": case "edit":
cli.EditEntryField(targetLocation, false, 0) cli.EditEntryField(realPath, false, 0)
case "gen": case "gen":
cli.AddEntry(targetLocation, false, 1) cli.AddEntry(realPath, false, 1)
case "add": case "add":
cli.AddEntry(targetLocation, false, 0) cli.AddEntry(realPath, false, 0)
default: default:
cli.HelpMain() cli.HelpMain()
} }
@@ -96,11 +97,11 @@ func main() {
case "note", "-n": case "note", "-n":
field = 4 field = 4
case "menu", "-m": case "menu", "-m":
cli.CopyMenu(targetLocation) cli.CopyMenu(realPath)
default: default:
cli.HelpCopy() cli.HelpCopy()
} }
err := clip.CopyShortcut(targetLocation, field) err := clip.CopyShortcut(realPath, field)
if err != nil { if err != nil {
other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard) other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard)
} }
@@ -118,38 +119,38 @@ func main() {
case "note", "-n": case "note", "-n":
field = 4 field = 4
case "rename", "-r": case "rename", "-r":
isAccessible, _ := back.TargetIsFile(targetLocation, true) // error is ignored because dir/file status is irrelevant isAccessible, _ := back.TargetIsFile(realPath, true) // error is ignored because dir/file status is irrelevant
if !isAccessible { if !isAccessible {
other.PrintError("Failed to access location ("+targetLocation+")", back.ErrorTargetNotFound) other.PrintError("Failed to access location ("+realPath+")", back.ErrorTargetNotFound)
} }
cli.RenameCli(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory cli.RenameCli(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory
default: default:
cli.HelpEdit() cli.HelpEdit()
} }
if argsCount == 4 { if argsCount == 4 {
cli.EditEntryField(targetLocation, true, field) cli.EditEntryField(realPath, true, field)
} else { } else {
switch args[4] { switch args[4] {
case "show", "-s": case "show", "-s":
cli.EditEntryField(targetLocation, false, field) cli.EditEntryField(realPath, false, field)
default: default:
cli.EditEntryField(targetLocation, true, field) cli.EditEntryField(realPath, true, field)
} }
} }
case "gen": case "gen":
if argsCount == 4 { if argsCount == 4 {
switch args[3] { switch args[3] {
case "update", "-u": case "update", "-u":
cli.GenUpdate(targetLocation, true) cli.GenUpdate(realPath, true)
default: default:
cli.HelpGen() cli.HelpGen()
} }
} else if args[3] == "update" || args[3] == "-u" { } else if args[3] == "update" || args[3] == "-u" {
switch args[4] { switch args[4] {
case "show", "-s": case "show", "-s":
cli.GenUpdate(targetLocation, false) cli.GenUpdate(realPath, false)
default: default:
cli.GenUpdate(targetLocation, true) cli.GenUpdate(realPath, true)
} }
} }
cli.HelpGen() cli.HelpGen()
@@ -157,17 +158,17 @@ func main() {
switch args[3] { switch args[3] {
case "password", "-pw": case "password", "-pw":
if argsCount == 4 { if argsCount == 4 {
cli.AddEntry(targetLocation, true, 0) cli.AddEntry(realPath, true, 0)
} else { } else {
switch args[4] { switch args[4] {
case "show", "-s": case "show", "-s":
cli.AddEntry(targetLocation, false, 0) cli.AddEntry(realPath, false, 0)
default: default:
cli.AddEntry(targetLocation, true, 0) cli.AddEntry(realPath, true, 0)
} }
} }
case "note", "-n": case "note", "-n":
cli.AddEntry(targetLocation, true, 2) cli.AddEntry(realPath, true, 2)
case "folder", "-f": case "folder", "-f":
err := syncclient.AddFolderRemoteFromClient(args[1]) // pass the incomplete path as the server will have a different home directory err := syncclient.AddFolderRemoteFromClient(args[1]) // pass the incomplete path as the server will have a different home directory
if err != nil { if err != nil {
@@ -204,14 +205,14 @@ func main() {
other.PrintError("Initialization failed: "+err.Error(), 0) other.PrintError("Initialization failed: "+err.Error(), 0)
} }
case "tweak": case "tweak":
choice := front.InputMenuGen("Action:", []string{"Change device ID", "Change master password/Optimize entries", "Set text editor"}) choice := front.InputMenuGen("Action:", []string{"Change device ID", "Change master password/Optimize entries", "Set text editor", "Age all entries"})
switch choice { switch choice {
case 1: case 1:
oldDeviceID, err := global.GetCurrentDeviceID() oldDeviceID, err := global.GetCurrentDeviceID()
if err != nil { if err != nil {
other.PrintError("Failed to get current device ID: "+err.Error(), back.ErrorRead) other.PrintError("Failed to get current device ID: "+err.Error(), back.ErrorRead)
} }
_, _, err = synccycles.DeviceIDGen(oldDeviceID, "") _, _, _, err = synccycles.DeviceIDGen(oldDeviceID, "")
if err != nil { if err != nil {
other.PrintError("Failed to change device ID: "+err.Error(), global.ErrorSyncProcess) other.PrintError("Failed to change device ID: "+err.Error(), global.ErrorSyncProcess)
} }
@@ -230,6 +231,13 @@ func main() {
if err != nil { if err != nil {
other.PrintError("Failed to set text editor: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to set text editor: "+err.Error(), back.ErrorWrite)
} }
case 4:
forceReage := front.InputBinary("Re-age aged entries?")
fmt.Println("Aging entries; this may take awhile - do not terminate this process")
err := age.AgeAllPasswordEntries(forceReage)
if err != nil {
other.PrintError("Failed to age entries: "+err.Error(), 1)
}
} }
case "copy": case "copy":
cli.HelpCopy() cli.HelpCopy()
+1 -1
View File
@@ -13,7 +13,7 @@ var (
) )
const ( const (
MUTNVersion = "0.3.A" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.B.0" -> "0.2.0", "0.2.A" -> "0.2.1" MUTNVersion = "0.D.0" // untagged releases feature a letter suffix corresponding to the eventual release version, e.g "0.B.0" -> "0.2.0", "0.2.A" -> "0.2.1"
ansiBlackOnWhite = "\033[38;5;0;48;5;15m" ansiBlackOnWhite = "\033[38;5;0;48;5;15m"
) )
+11 -6
View File
@@ -7,22 +7,22 @@ import (
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
) )
// AddEntry creates a new entry at targetLocation by taking user input via CLI prompts. // 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). // Requires: entryType (0 = standard password entry, 1 = auto-generated password entry, 2 = note-only entry).
func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) { func AddEntry(realPath string, hideSecrets bool, entryType uint8) {
// ensure targetLocation is valid // ensure realPath is valid
_, err := core.EntryAddPrecheck(targetLocation) _, err := core.EntryAddPrecheck(realPath)
if err != nil { if err != nil {
other.PrintError("Failed to add entry: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to add entry: "+err.Error(), back.ErrorWrite)
} }
var decryptedEntry []string var decryptedEntry []string
var password string
if entryType < 2 { if entryType < 2 {
username := front.Input("Username:") username := front.Input("Username:")
// determine whether to generate the password // determine whether to generate the password
var password string
if entryType == 0 { if entryType == 0 {
password = string(front.InputHidden("Password:")) password = string(front.InputHidden("Password:"))
} else { } else {
@@ -43,5 +43,10 @@ func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) {
} }
// write and preview the new entry // write and preview the new entry
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets) if password != "" {
writeEntryCLI(realPath, decryptedEntry, hideSecrets, true)
} else {
writeEntryCLI(realPath, decryptedEntry, hideSecrets, false)
}
} }
+2 -2
View File
@@ -16,9 +16,9 @@ import (
// CopyMenu decrypts an entry and allows the user to // CopyMenu decrypts an entry and allows the user to
// interactively copy fields without having to re-decrypt each time. // interactively copy fields without having to re-decrypt each time.
func CopyMenu(targetLocation string) { func CopyMenu(realPath string) {
// decrypt entry // decrypt entry
decSlice, err := crypt.DecryptFileToSlice(targetLocation) decSlice, err := crypt.DecryptFileToSlice(realPath)
if err != nil { if err != nil {
other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption) other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption)
} }
+15 -11
View File
@@ -15,10 +15,10 @@ import (
) )
// RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server. // RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server.
func RenameCli(oldLocationIncomplete string) { func RenameCli(oldVanityPath string) {
// prompt user for new location and rename // prompt user for new location and rename
newLocationIncomplete := front.Input("New location:") newVanityPath := front.Input("New location:")
err := syncclient.RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete) err := syncclient.RenameRemoteFromClient(oldVanityPath, newVanityPath)
if err != nil { if err != nil {
other.PrintError("Failed to rename entry: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to rename entry: "+err.Error(), back.ErrorWrite)
} }
@@ -26,10 +26,10 @@ func RenameCli(oldLocationIncomplete string) {
// exit is done from sync.RenameRemoteFromClient // exit is done from sync.RenameRemoteFromClient
} }
// EditEntryField edits a field of an entry at targetLocation (user input). // EditEntryField edits a field of an entry at realPath (user input).
func EditEntryField(targetLocation string, hideSecrets bool, field int) { func EditEntryField(realPath string, hideSecrets bool, field int) {
// fetch old entry data (with all required lines present) // fetch old entry data (with all required lines present)
decryptedEntry, err := core.GetOldEntryData(targetLocation, field) decryptedEntry, err := core.GetOldEntryData(realPath, field)
if err != nil { if err != nil {
other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead) other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead)
} }
@@ -58,13 +58,17 @@ func EditEntryField(targetLocation string, hideSecrets bool, field int) {
} }
// write and preview the modified entry // write and preview the modified entry
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets) if field == 0 {
writeEntryCLI(realPath, decryptedEntry, hideSecrets, true)
} else {
writeEntryCLI(realPath, decryptedEntry, hideSecrets, false)
}
} }
// GenUpdate generates a new password for an entry at targetLocation (user input). // GenUpdate generates a new password for an entry at realPath (user input).
func GenUpdate(targetLocation string, hideSecrets bool) { func GenUpdate(realPath string, hideSecrets bool) {
// fetch old entry data // fetch old entry data
decryptedEntry, err := core.GetOldEntryData(targetLocation, 0) decryptedEntry, err := core.GetOldEntryData(realPath, 0)
if err != nil { if err != nil {
other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead) other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead)
} }
@@ -73,7 +77,7 @@ func GenUpdate(targetLocation string, hideSecrets bool) {
decryptedEntry[0] = inputPasswordGen() decryptedEntry[0] = inputPasswordGen()
// write and preview the modified entry // write and preview the modified entry
writeEntryCLI(targetLocation, decryptedEntry, hideSecrets) writeEntryCLI(realPath, decryptedEntry, hideSecrets, true)
} }
// editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty). // editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty).
+20 -3
View File
@@ -7,6 +7,7 @@ import (
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/other" "github.com/rwinkhart/go-boilerplate/other"
"github.com/rwinkhart/libmutton/age"
"github.com/rwinkhart/libmutton/synccommon" "github.com/rwinkhart/libmutton/synccommon"
) )
@@ -47,7 +48,7 @@ func determineIndentation(skippedDirList []bool, dirList []string, currentDirInd
} }
// printFileEntry handles processing for printing file entries (determines color, wraps lines, and prints). // printFileEntry handles processing for printing file entries (determines color, wraps lines, and prints).
func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlternator int8) (int, int8) { func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlternator int8, agingTimestamp int64) (int, int8) {
// determine color to print fileEntryName (alternate each time function is run) // determine color to print fileEntryName (alternate each time function is run)
var colorCode string var colorCode string
if colorAlternator > 0 { if colorAlternator > 0 {
@@ -57,6 +58,17 @@ func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlter
} }
colorAlternator = -colorAlternator colorAlternator = -colorAlternator
// determine password aging dot
var agingDot string
switch age.TranslateAgeTimestamp(agingTimestamp) {
case 1:
agingDot = back.AnsiGreen + "⁍" + back.AnsiReset
case 2:
agingDot = back.AnsiWarning + "⁍" + back.AnsiReset
case 3:
agingDot = back.AnsiError + "⁍" + back.AnsiReset
}
// trim the containing directory from the entry to determine fileEntryName // trim the containing directory from the entry to determine fileEntryName
fileEntryName := entry[lastSlash:] fileEntryName := entry[lastSlash:]
@@ -72,7 +84,7 @@ func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlter
} }
// print fileEntryName to screen // print fileEntryName to screen
fmt.Printf("%s%s%s ", colorCode, fileEntryName, back.AnsiReset) fmt.Printf("%s%s%s%s ", agingDot, colorCode, fileEntryName, back.AnsiReset)
return charCounter, colorAlternator return charCounter, colorAlternator
} }
@@ -83,6 +95,11 @@ func EntryListGen() {
if err != nil { if err != nil {
other.PrintError("Failed to generate entry list: "+err.Error(), back.ErrorRead) other.PrintError("Failed to generate entry list: "+err.Error(), back.ErrorRead)
} }
var vanityPathsToTimestamps map[string]int64
vanityPathsToTimestamps, err = synccommon.GetEntryAges()
if err != nil {
other.PrintError("Failed to retrieve entry aging data: "+err.Error(), back.ErrorRead)
}
// print header bar w/total entry count // print header bar w/total entry count
fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+back.AnsiReset) fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+back.AnsiReset)
@@ -134,7 +151,7 @@ func EntryListGen() {
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory) fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory)
} }
charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator) charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator, vanityPathsToTimestamps[file])
} }
} }
+3 -3
View File
@@ -83,12 +83,12 @@ fieldLoop:
} }
// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader. // EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader.
func EntryReaderDecrypt(targetLocation string, hideSecrets bool) { func EntryReaderDecrypt(realPath string, hideSecrets bool) {
_, err := back.TargetIsFile(targetLocation, true) _, err := back.TargetIsFile(realPath, true)
if err != nil { // if the location does not exist or is a directory... if err != nil { // if the location does not exist or is a directory...
other.PrintError("Failed to verify target location: "+err.Error(), back.ErrorTargetNotFound) other.PrintError("Failed to verify target location: "+err.Error(), back.ErrorTargetNotFound)
} }
decSlice, err := crypt.DecryptFileToSlice(targetLocation) decSlice, err := crypt.DecryptFileToSlice(realPath)
if err != nil { if err != nil {
other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption) other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption)
} }
+5 -7
View File
@@ -2,7 +2,6 @@ package cli
import ( import (
"fmt" "fmt"
"strings"
"github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front" "github.com/rwinkhart/go-boilerplate/front"
@@ -28,17 +27,16 @@ func inputPasswordGen() string {
return synccycles.StringGen(passLength, complexity, passCharset) return synccycles.StringGen(passLength, complexity, passCharset)
} }
// writeEntryCLI writes an entry to targetLocation and previews it (errors if no data is supplied). // writeEntryCLI writes an entry to realPath and previews it (errors if no data is supplied).
func writeEntryCLI(targetLocation string, decryptedEntry []string, hideSecrets bool) { func writeEntryCLI(realPath string, decSlice []string, hideSecrets bool, passwordIsNew bool) {
if core.EntryIsNotEmpty(decryptedEntry) { if core.EntryIsNotEmpty(decSlice) {
// write the entry to the target location err := core.WriteEntry(realPath, decSlice, passwordIsNew)
err := core.WriteEntry(targetLocation, []byte(strings.Join(decryptedEntry, "\n")))
if err != nil { if err != nil {
other.PrintError("Failed to write entry: "+err.Error(), back.ErrorWrite) other.PrintError("Failed to write entry: "+err.Error(), back.ErrorWrite)
} }
// preview the entry // preview the entry
fmt.Println(back.AnsiBold + "\nEntry Preview:" + back.AnsiReset) fmt.Println(back.AnsiBold + "\nEntry Preview:" + back.AnsiReset)
EntryReader(decryptedEntry, hideSecrets, true) EntryReader(decSlice, hideSecrets, true)
} else { } else {
other.PrintError("No data supplied for entry", back.ErrorTargetNotFound) other.PrintError("No data supplied for entry", back.ErrorTargetNotFound)
} }