mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-27 20:36:29 -04:00
Better modularize clipboard code (add "clip" package; break up TOTP code
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
//go:build (android && !termux) || ios
|
//go:build (android && !termux) || ios
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build (!android && !ios) || termux
|
//go:build (!android && !ios) || termux
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package clip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/rwinkhart/go-boilerplate/back"
|
||||||
|
"github.com/rwinkhart/libmutton/crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CopyArgument copies a field from an entry to the clipboard.
|
||||||
|
// If field is -1, it will one-time copy the TOTP code
|
||||||
|
// (instead of keeping the clipboard up-to-date).
|
||||||
|
func CopyArgument(targetLocation string, field int) error {
|
||||||
|
// ensure targetLocation exists and is a file
|
||||||
|
_, err := back.TargetIsFile(targetLocation, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
decSlice, err := crypt.DecryptFileToSlice(targetLocation)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("unable to decrypt entry: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle non-persistent TOTP copy
|
||||||
|
var copySubject string
|
||||||
|
var realField int
|
||||||
|
if field == -1 {
|
||||||
|
realField = 2
|
||||||
|
} else {
|
||||||
|
realField = field
|
||||||
|
}
|
||||||
|
|
||||||
|
// if field exists in entry...
|
||||||
|
if len(decSlice) > realField {
|
||||||
|
if decSlice[realField] == "" {
|
||||||
|
return errors.New("field is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if realField == 2 { // TOTP mode
|
||||||
|
if field != -1 {
|
||||||
|
fmt.Println("Clipboard refreshing with the current TOTP code until this process is closed")
|
||||||
|
}
|
||||||
|
errorChan := make(chan error)
|
||||||
|
go TOTPCopier(decSlice[2], field, errorChan, nil) // "done" is not needed because the process runs until the program is killed
|
||||||
|
err = <-errorChan
|
||||||
|
if err != nil { // block until first successful copy
|
||||||
|
return errors.New("error encountered in TOTP refresh process: " + err.Error())
|
||||||
|
}
|
||||||
|
} else { // other
|
||||||
|
copySubject = decSlice[realField]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("field does not exist in entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy field to clipboard; launch clipboard clearing process
|
||||||
|
err = CopyString(false, copySubject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess.
|
||||||
|
func ClipClearArgument() error {
|
||||||
|
assignedContents := back.ReadFromStdin()
|
||||||
|
if assignedContents == "" {
|
||||||
|
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
||||||
|
}
|
||||||
|
err := clipClearProcess(assignedContents)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build darwin && !ios
|
//go:build darwin && !ios
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/rwinkhart/go-boilerplate/back"
|
"github.com/rwinkhart/go-boilerplate/back"
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyString copies a string to the clipboard.
|
// CopyString copies a string to the clipboard.
|
||||||
func copyString(continuous bool, copySubject string) error {
|
func CopyString(continuous bool, copySubject string) error {
|
||||||
cmd := exec.Command("pbcopy")
|
cmd := exec.Command("pbcopy")
|
||||||
_ = back.WriteToStdin(cmd, copySubject)
|
_ = back.WriteToStdin(cmd, copySubject)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
//go:build (android && !termux) || ios
|
//go:build (android && !termux) || ios
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"golang.design/x/clipboard"
|
"golang.design/x/clipboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyString copies a string to the clipboard.
|
// CopyString copies a string to the clipboard.
|
||||||
func copyString(continuous bool, copySubject string) error {
|
func CopyString(continuous bool, copySubject string) error {
|
||||||
clipboard.Write(clipboard.FmtText, []byte(copySubject))
|
clipboard.Write(clipboard.FmtText, []byte(copySubject))
|
||||||
if !continuous {
|
if !continuous {
|
||||||
LaunchClipClearProcess(copySubject)
|
LaunchClipClearProcess(copySubject)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build android && termux
|
//go:build android && termux
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/rwinkhart/go-boilerplate/back"
|
"github.com/rwinkhart/go-boilerplate/back"
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyString copies a string to the clipboard.
|
// CopyString copies a string to the clipboard.
|
||||||
func copyString(continuous bool, copySubject string) error {
|
func CopyString(continuous bool, copySubject string) error {
|
||||||
cmd := exec.Command("termux-clipboard-set")
|
cmd := exec.Command("termux-clipboard-set")
|
||||||
_ = back.WriteToStdin(cmd, copySubject)
|
_ = back.WriteToStdin(cmd, copySubject)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !windows && !darwin && !android && !ios && !termux && !wsl
|
//go:build !windows && !darwin && !android && !ios && !termux && !wsl
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"github.com/rwinkhart/go-boilerplate/back"
|
"github.com/rwinkhart/go-boilerplate/back"
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyString copies a string to the clipboard.
|
// CopyString copies a string to the clipboard.
|
||||||
func copyString(continuous bool, copySubject string) error {
|
func CopyString(continuous bool, copySubject string) error {
|
||||||
// determine whether to use wl-copy (Wayland) or xclip (X11)
|
// determine whether to use wl-copy (Wayland) or xclip (X11)
|
||||||
var envSet, isWayland bool // track whether environment variables are set
|
var envSet, isWayland bool // track whether environment variables are set
|
||||||
var cmdCopy *exec.Cmd
|
var cmdCopy *exec.Cmd
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build windows || (linux && wsl)
|
//go:build windows || (linux && wsl)
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyString copies a string to the clipboard.
|
// CopyString copies a string to the clipboard.
|
||||||
func copyString(continuous bool, copySubject string) error {
|
func CopyString(continuous bool, copySubject string) error {
|
||||||
cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
|
cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build (windows || darwin || android || ios || termux || wsl) && !interactive
|
//go:build (windows || darwin || android || ios || termux || wsl) && !interactive
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !windows && !darwin && !android && !ios && !termux && !wsl && !interactive
|
//go:build !windows && !darwin && !android && !ios && !termux && !wsl && !interactive
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build (windows || darwin || android || ios || termux || wsl) && interactive
|
//go:build (windows || darwin || android || ios || termux || wsl) && interactive
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
// LaunchClipClearProcess launches the timed clipboard clearing process.
|
// LaunchClipClearProcess launches the timed clipboard clearing process.
|
||||||
// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine.
|
// For interactive GUI/TUI implementations, the clipboard clearing process is launched as a goroutine.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !windows && !darwin && !android && !ios && !termux && !wsl && interactive
|
//go:build !windows && !darwin && !android && !ios && !termux && !wsl && interactive
|
||||||
|
|
||||||
package core
|
package clip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package clip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP).
|
||||||
|
func GenTOTP(secret string, time time.Time, forSteam bool) (string, error) {
|
||||||
|
var totpToken string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if forSteam {
|
||||||
|
totpToken, err = totp.GenerateCodeCustom(secret, time, totp.ValidateOpts{Period: 30, Digits: 5, Encoder: otp.EncoderSteam})
|
||||||
|
} else {
|
||||||
|
totpToken, err = totp.GenerateCode(secret, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("unable to generate TOTP token: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return totpToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOTPCopier is meant to be run as a goroutine to keep
|
||||||
|
// the clipboard up-to-date with the latest TOTP token.
|
||||||
|
// Set oneTime to -1 for a one-time (non-continuous) TOTP copy.
|
||||||
|
func TOTPCopier(secret string, oneTime int, errorChan chan<- error, done <-chan bool) {
|
||||||
|
var forSteam bool
|
||||||
|
if strings.HasPrefix(secret, "steam@") {
|
||||||
|
secret = secret[6:]
|
||||||
|
forSteam = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
currentTime := time.Now()
|
||||||
|
token, err := GenTOTP(secret, currentTime, forSteam)
|
||||||
|
if err != nil {
|
||||||
|
errorChan <- err
|
||||||
|
}
|
||||||
|
err = CopyString(true, token)
|
||||||
|
if err != nil {
|
||||||
|
errorChan <- err
|
||||||
|
}
|
||||||
|
if oneTime != -1 {
|
||||||
|
errorChan <- nil
|
||||||
|
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// exit after sleep if indicated (will not update clipboard again)
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
errorChan <- nil
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-119
@@ -1,119 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pquerna/otp"
|
|
||||||
"github.com/pquerna/otp/totp"
|
|
||||||
"github.com/rwinkhart/go-boilerplate/back"
|
|
||||||
"github.com/rwinkhart/libmutton/crypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CopyArgument copies a field from an entry to the clipboard.
|
|
||||||
// If field is -1, it will one-time copy the TOTP code
|
|
||||||
// (instead of keeping the clipboard up-to-date).
|
|
||||||
func CopyArgument(targetLocation string, field int) error {
|
|
||||||
// ensure targetLocation exists and is a file
|
|
||||||
_, err := back.TargetIsFile(targetLocation, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptedEntry, err := crypt.DecryptFileToSlice(targetLocation)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("unable to decrypt entry: " + err.Error())
|
|
||||||
}
|
|
||||||
var copySubject string // will store data to be copied
|
|
||||||
|
|
||||||
// handle non-persistent TOTP copy
|
|
||||||
var realField int
|
|
||||||
if field == -1 {
|
|
||||||
realField = 2
|
|
||||||
} else {
|
|
||||||
realField = field
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure field exists in entry
|
|
||||||
if len(decryptedEntry) > realField {
|
|
||||||
|
|
||||||
// ensure field is not empty
|
|
||||||
if decryptedEntry[realField] == "" {
|
|
||||||
return errors.New("field is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if realField != 2 {
|
|
||||||
copySubject = decryptedEntry[realField]
|
|
||||||
} else { // TOTP mode
|
|
||||||
var secret string // stores secret for TOTP generation
|
|
||||||
var forSteam bool // indicates whether to generate TOTP in Steam format
|
|
||||||
|
|
||||||
if strings.HasPrefix(decryptedEntry[2], "steam@") {
|
|
||||||
secret = decryptedEntry[2][6:]
|
|
||||||
forSteam = true
|
|
||||||
} else {
|
|
||||||
secret = decryptedEntry[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Clipboard will be kept up to date with the current TOTP code until this process is closed")
|
|
||||||
|
|
||||||
for { // keep token copied to clipboard, refresh on 30-second intervals
|
|
||||||
currentTime := time.Now()
|
|
||||||
token, err := GenTOTP(secret, currentTime, forSteam)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = copyString(true, token)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if field == -1 {
|
|
||||||
return nil // return early (for interactive clients)
|
|
||||||
}
|
|
||||||
// sleep until next 30-second interval
|
|
||||||
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return errors.New("field does not exist in entry")
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy field to clipboard, launch clipboard clearing process
|
|
||||||
err = copyString(false, copySubject)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess.
|
|
||||||
func ClipClearArgument() error {
|
|
||||||
assignedContents := back.ReadFromStdin()
|
|
||||||
if assignedContents == "" {
|
|
||||||
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
|
|
||||||
}
|
|
||||||
err := clipClearProcess(assignedContents)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP).
|
|
||||||
func GenTOTP(secret string, time time.Time, forSteam bool) (string, error) {
|
|
||||||
var totpToken string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if forSteam {
|
|
||||||
totpToken, err = totp.GenerateCodeCustom(secret, time, totp.ValidateOpts{Period: 30, Digits: 5, Encoder: otp.EncoderSteam})
|
|
||||||
} else {
|
|
||||||
totpToken, err = totp.GenerateCode(secret, time)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("unable to generate TOTP token: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return totpToken, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user