From c1a415e5b060998bc8db691e78672a480e9c05a4 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Mon, 18 Mar 2024 16:44:22 -0400 Subject: [PATCH] Implement initial TOTP support (secret in third notes line, will eventually have its own dedicated field) --- go.mod | 2 ++ go.sum | 7 +++++++ main.go | 3 +++ src/offline/copy.go | 37 +++++++++++++++++++++++++++------- src/offline/copyDARWIN.go | 22 +++++++++++--------- src/offline/copyTERMUX.go | 22 +++++++++++--------- src/offline/copyUNIXGeneric.go | 22 +++++++++++--------- src/offline/copyWIN.go | 22 +++++++++++--------- 8 files changed, 90 insertions(+), 47 deletions(-) diff --git a/go.mod b/go.mod index d75b99a..484eeb2 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module github.com/rwinkhart/MUTN go 1.22.0 require ( + github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481 golang.org/x/crypto v0.20.0 gopkg.in/ini.v1 v1.67.0 ) require ( + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/stretchr/testify v1.8.4 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.17.0 // indirect diff --git a/go.sum b/go.sum index 11eb3c9..5b46588 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,14 @@ +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481 h1:FkxbO331O7mS5EJkP+MCi0o2gswh/Aezs+//NmefrR8= +github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= diff --git a/main.go b/main.go index 3cc1661..4e12c4e 100644 --- a/main.go +++ b/main.go @@ -76,6 +76,9 @@ func main() { field = 2 case "note", "-n": field = 3 + case "totp", "-t": + fmt.Println("TOTP code copied to clipboard - until this process is closed, your clipboard will be kept up to date with the current TOTP code") + field = 5 // TODO Update field after removed from notes (breaking sshyp entry compatibility) default: cli.HelpCopy() } diff --git a/src/offline/copy.go b/src/offline/copy.go index 319b155..9763439 100644 --- a/src/offline/copy.go +++ b/src/offline/copy.go @@ -3,9 +3,12 @@ package offline import ( "bufio" "fmt" + "github.com/pquerna/otp/totp" "os" + "time" ) +// CopyArgument copies a field from an entry to the clipboard func CopyArgument(targetLocation string, field int, executableName string) { if isFile, _ := TargetIsFile(targetLocation, true, 2); isFile { @@ -14,23 +17,43 @@ func CopyArgument(targetLocation string, field int, executableName string) { // ensure field exists in entry if len(decryptedEntry) > field { - copySubject = decryptedEntry[field] + + // ensure field is not empty + if decryptedEntry[field] == "" { + fmt.Println(AnsiError + "Field is empty" + AnsiReset) + os.Exit(1) + } + + if field != 5 { // TODO Update field after removed from notes (breaking sshyp entry compatibility) + copySubject = decryptedEntry[field] + } else { // TOTP mode + var err error + var currentSecond int // track current second for TOTP code refresh + var copied bool // track whether the TOTP code has been copied to the clipboard for the first time + for { // keep field copied to clipboard, refresh on 30-second intervals + currentSecond = time.Now().Second() + if currentSecond == 0 || currentSecond == 30 || !copied { + copySubject, err = totp.GenerateCode(decryptedEntry[5], time.Now()) + if err != nil { + fmt.Println(AnsiError + "Error generating TOTP code" + AnsiReset) + os.Exit(1) + } + copyField(copySubject, "") + } + time.Sleep(1 * time.Second) + } + } } else { fmt.Println(AnsiError + "Field does not exist in entry" + AnsiReset) os.Exit(1) } - // ensure field is not blank - if copySubject == "" { - fmt.Println(AnsiError + "Field is empty" + AnsiReset) - os.Exit(1) - } - // copy field to clipboard, launch clipboard clearing process copyField(copySubject, executableName) } } +// ClipClearArgument is called to clear the clipboard after 30 seconds if the contents have not been modified func ClipClearArgument() { // read previous clipboard contents from stdin clipScanner := bufio.NewScanner(os.Stdin) diff --git a/src/offline/copyDARWIN.go b/src/offline/copyDARWIN.go index feafc4d..2f0c1c0 100644 --- a/src/offline/copyDARWIN.go +++ b/src/offline/copyDARWIN.go @@ -12,7 +12,7 @@ import ( // TODO MacOS support is entirely untested - I would appreciate feedback on this implementation -// CopyField copies a field from an entry to the clipboard +// copyField copies a field from an entry to the clipboard func copyField(copySubject string, executableName string) { cmd := exec.Command("pbcopy") writeToStdin(cmd, copySubject) @@ -22,18 +22,20 @@ func copyField(copySubject string, executableName string) { os.Exit(1) } - cmd = exec.Command(executableName, "clipclear") - writeToStdin(cmd, copySubject) - err = cmd.Start() - if err != nil { - fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) - os.Exit(1) + // launch clipboard clearing process if executableName is provided + if executableName != "" { + cmd = exec.Command(executableName, "clipclear") + writeToStdin(cmd, copySubject) + err = cmd.Start() + if err != nil { + fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) + os.Exit(1) + } + os.Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh } - - os.Exit(0) } -// ClipClear is called in a separate process to clear the clipboard after 30 seconds +// clipClear is called in a separate process to clear the clipboard after 30 seconds func clipClear(oldContents string) { time.Sleep(30 * time.Second) diff --git a/src/offline/copyTERMUX.go b/src/offline/copyTERMUX.go index 360cc32..ce22f54 100644 --- a/src/offline/copyTERMUX.go +++ b/src/offline/copyTERMUX.go @@ -10,7 +10,7 @@ import ( "time" ) -// CopyField copies a field from an entry to the clipboard +// copyField copies a field from an entry to the clipboard func copyField(copySubject string, executableName string) { cmd := exec.Command("termux-clipboard-set") writeToStdin(cmd, copySubject) @@ -20,18 +20,20 @@ func copyField(copySubject string, executableName string) { os.Exit(1) } - cmd = exec.Command(executableName, "clipclear") - writeToStdin(cmd, copySubject) - err = cmd.Start() - if err != nil { - fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) - os.Exit(1) + // launch clipboard clearing process if executableName is provided + if executableName != "" { + cmd = exec.Command(executableName, "clipclear") + writeToStdin(cmd, copySubject) + err = cmd.Start() + if err != nil { + fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) + os.Exit(1) + } + os.Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh } - - os.Exit(0) } -// ClipClear is called in a separate process to clear the clipboard after 30 seconds +// clipClear is called in a separate process to clear the clipboard after 30 seconds func clipClear(oldContents string) { time.Sleep(30 * time.Second) diff --git a/src/offline/copyUNIXGeneric.go b/src/offline/copyUNIXGeneric.go index fd95b90..cb13520 100644 --- a/src/offline/copyUNIXGeneric.go +++ b/src/offline/copyUNIXGeneric.go @@ -10,7 +10,7 @@ import ( "time" ) -// CopyField copies a field from an entry to the clipboard +// copyField copies a field from an entry to the clipboard func copyField(copySubject string, executableName string) { var envSet bool // track whether environment variables are set var cmd *exec.Cmd @@ -31,18 +31,20 @@ func copyField(copySubject string, executableName string) { os.Exit(1) } - cmd = exec.Command(executableName, "clipclear") - writeToStdin(cmd, copySubject) - err = cmd.Start() - if err != nil { - fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) - os.Exit(1) + // launch clipboard clearing process if executableName is provided + if executableName != "" { + cmd = exec.Command(executableName, "clipclear") + writeToStdin(cmd, copySubject) + err = cmd.Start() + if err != nil { + fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) + os.Exit(1) + } + os.Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh } - - os.Exit(0) } -// ClipClear is called in a separate process to clear the clipboard after 30 seconds +// clipClear is called in a separate process to clear the clipboard after 30 seconds func clipClear(oldContents string) { time.Sleep(30 * time.Second) diff --git a/src/offline/copyWIN.go b/src/offline/copyWIN.go index 2a6c72b..5058ded 100644 --- a/src/offline/copyWIN.go +++ b/src/offline/copyWIN.go @@ -10,7 +10,7 @@ import ( "time" ) -// CopyField copies a field from an entry to the clipboard +// copyField copies a field from an entry to the clipboard func copyField(copySubject string, executableName string) { cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''"))) err := cmd.Run() @@ -19,18 +19,20 @@ func copyField(copySubject string, executableName string) { os.Exit(1) } - cmd = exec.Command(executableName, "clipclear") - writeToStdin(cmd, copySubject) - err = cmd.Start() - if err != nil { - fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) - os.Exit(1) + // launch clipboard clearing process if executableName is provided + if executableName != "" { + cmd = exec.Command(executableName, "clipclear") + writeToStdin(cmd, copySubject) + err = cmd.Start() + if err != nil { + fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset) + os.Exit(1) + } + os.Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh } - - os.Exit(0) } -// ClipClear is called in a separate process to clear the clipboard after 30 seconds +// clipClear is called in a separate process to clear the clipboard after 30 seconds func clipClear(oldContents string) { time.Sleep(30 * time.Second)