From 649b60e2feda219f7d140b08a5e763d2f7dc93ec Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Sun, 4 May 2025 17:41:44 -0400 Subject: [PATCH] Initial migration from GPG to RCW --- core/2globalsUNIX.go | 6 --- core/2globalsWIN.go | 17 --------- core/copy.go | 4 +- core/copyDARWIN.go | 4 +- core/copyTERMUX.go | 4 +- core/copyUNIX.go | 2 +- core/edit.go | 4 +- core/gpg.go | 35 ----------------- core/init.go | 48 +++--------------------- core/launchClipClearProcessCLIGeneric.go | 2 +- core/launchClipClearProcessCLIUNIX.go | 2 +- core/rcw.go | 27 +++++++++++++ core/utilitiesMisc.go | 9 ++--- go.mod | 1 + go.sum | 2 + wiki/bugs.md | 3 -- wiki/developers.md | 4 -- 17 files changed, 51 insertions(+), 123 deletions(-) delete mode 100644 core/gpg.go create mode 100644 core/rcw.go delete mode 100644 wiki/bugs.md diff --git a/core/2globalsUNIX.go b/core/2globalsUNIX.go index a7fcaeb..d4b1059 100644 --- a/core/2globalsUNIX.go +++ b/core/2globalsUNIX.go @@ -12,9 +12,3 @@ const ( PathSeparator = "/" // Platform-specific path separator IsWindows = false // Platform indicator ) - -// enableVirtualTerminalProcessing is a dummy function on UNIX-like systems (only needed on Windows). -// TODO Remove after migration off of GPG, as pinentry is responsible for disabling ANSI escape sequence interpretation. -func enableVirtualTerminalProcessing() { - return -} diff --git a/core/2globalsWIN.go b/core/2globalsWIN.go index dfc2c49..8e6eb6c 100644 --- a/core/2globalsWIN.go +++ b/core/2globalsWIN.go @@ -2,11 +2,6 @@ package core -import ( - "os" - "syscall" -) - var ( EntryRoot = Home + "\\AppData\\Local\\libmutton\\entries" // Path to libmutton entry directory ConfigDir = Home + "\\AppData\\Local\\libmutton\\config" // Path to libmutton configuration directory @@ -17,15 +12,3 @@ const ( PathSeparator = "\\" // Platform-specific path separator IsWindows = true // Platform indicator ) - -// enableVirtualTerminalProcessing ensures ANSI escape sequences are interpreted properly on Windows. -// TODO Remove after migration off of GPG, as pinentry is responsible for disabling ANSI escape sequence interpretation. -func enableVirtualTerminalProcessing() { - stdout := syscall.Handle(os.Stdout.Fd()) - - var originalMode uint32 - syscall.GetConsoleMode(stdout, &originalMode) - originalMode |= 0x0004 - - syscall.MustLoadDLL("kernel32").MustFindProc("SetConsoleMode").Call(uintptr(stdout), uintptr(originalMode)) -} diff --git a/core/copy.go b/core/copy.go index 40a4938..a763929 100644 --- a/core/copy.go +++ b/core/copy.go @@ -12,10 +12,10 @@ import ( ) // CopyArgument copies a field from an entry to the clipboard. -func CopyArgument(targetLocation string, field int) { +func CopyArgument(targetLocation string, field int, passphrase []byte) { if isFile, _ := TargetIsFile(targetLocation, true, 2); isFile { - decryptedEntry := DecryptGPG(targetLocation) + decryptedEntry := DecryptFileToSlice(targetLocation, passphrase) var copySubject string // will store data to be copied // ensure field exists in entry diff --git a/core/copyDARWIN.go b/core/copyDARWIN.go index d710009..5e858c5 100644 --- a/core/copyDARWIN.go +++ b/core/copyDARWIN.go @@ -9,7 +9,7 @@ import ( // copyString copies a string to the clipboard. func copyString(continuous bool, copySubject string) { cmd := exec.Command("pbcopy") - WriteToStdin(cmd, copySubject) + writeToStdin(cmd, copySubject) err := cmd.Run() if err != nil { PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true) @@ -23,6 +23,6 @@ func copyString(continuous bool, copySubject string) { // getClipCommands returns the commands for pasting and clearing the clipboard contents. func getClipCommands() (*exec.Cmd, *exec.Cmd) { cmdClear := exec.Command("pbcopy") - WriteToStdin(cmdClear, "") + writeToStdin(cmdClear, "") return exec.Command("pbpaste"), cmdClear } diff --git a/core/copyTERMUX.go b/core/copyTERMUX.go index b80f1c4..aaa808e 100644 --- a/core/copyTERMUX.go +++ b/core/copyTERMUX.go @@ -9,7 +9,7 @@ import ( // copyString copies a string to the clipboard. func copyString(continuous bool, copySubject string) { cmd := exec.Command("termux-clipboard-set") - WriteToStdin(cmd, copySubject) + writeToStdin(cmd, copySubject) err := cmd.Run() if err != nil { PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true) @@ -23,6 +23,6 @@ func copyString(continuous bool, copySubject string) { // getClipCommands returns the commands for pasting and clearing the clipboard contents. func getClipCommands() (*exec.Cmd, *exec.Cmd) { cmdClear := exec.Command("termux-clipboard-set") - WriteToStdin(cmdClear, "") + writeToStdin(cmdClear, "") return exec.Command("termux-clipboard-get"), cmdClear } diff --git a/core/copyUNIX.go b/core/copyUNIX.go index f63a76b..ff1099c 100644 --- a/core/copyUNIX.go +++ b/core/copyUNIX.go @@ -21,7 +21,7 @@ func copyString(continuous bool, copySubject string) { PrintError("Clipboard platform could not be determined", ErrorClipboard, true) } - WriteToStdin(cmdCopy, copySubject) + writeToStdin(cmdCopy, copySubject) err := cmdCopy.Run() if err != nil { PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true) diff --git a/core/edit.go b/core/edit.go index 5127f03..3cdac2e 100644 --- a/core/edit.go +++ b/core/edit.go @@ -1,12 +1,12 @@ package core // GetOldEntryData decrypts and returns old entry data (with all required lines present). -func GetOldEntryData(targetLocation string, field int) []string { +func GetOldEntryData(targetLocation string, field int, passphrase []byte) []string { // ensure targetLocation exists TargetIsFile(targetLocation, true, 2) // read old entry data - unencryptedEntry := DecryptGPG(targetLocation) + unencryptedEntry := DecryptFileToSlice(targetLocation, passphrase) // return the old entry data with all required lines present if field > 0 { diff --git a/core/gpg.go b/core/gpg.go deleted file mode 100644 index 642838c..0000000 --- a/core/gpg.go +++ /dev/null @@ -1,35 +0,0 @@ -package core - -import ( - "os/exec" - "strings" -) - -// TODO GPG support is a temporary feature - It will be replaced with a different encryption scheme in the future - -// DecryptGPG decrypts a GPG-encrypted file and returns the contents as a slice of (trimmed) strings. -func DecryptGPG(targetLocation string) []string { - cmd := exec.Command("gpg", "--pinentry-mode", "loopback", "-q", "-d", targetLocation) - output, err := cmd.Output() - - // ensure ANSI escape sequences are interpreted properly on Windows - enableVirtualTerminalProcessing() - - if err != nil { - PrintError("Failed to decrypt \""+targetLocation+"\" - Ensure it is a valid GPG-encrypted file and that you entered your passphrase correctly", ErrorDecryption, true) - } - - return strings.Split(string(output), "\n") -} - -// EncryptGPG encrypts a slice of strings using GPG and returns the encrypted data as a byte slice. -func EncryptGPG(input []string) []byte { - gpgCfg, _ := ParseConfig([][2]string{{"LIBMUTTON", "gpgID"}}, "") - cmd := exec.Command("gpg", "-q", "-r", gpgCfg[0], "-e") - WriteToStdin(cmd, strings.Join(input, "\n")) - encryptedBytes, err := cmd.Output() - if err != nil { - PrintError("Failed to encrypt data - Ensure that you have a valid GPG ID set in libmutton.ini", ErrorEncryption, true) - } - return encryptedBytes -} diff --git a/core/init.go b/core/init.go index 8830f78..8a4d929 100644 --- a/core/init.go +++ b/core/init.go @@ -2,52 +2,16 @@ package core import ( "os" - "os/exec" - "strconv" - "strings" - "time" + + "github.com/rwinkhart/rcw/wrappers" ) -// GpgUIDListGen generates a list of all GPG key IDs on the system and returns them as a slice of strings. -func GpgUIDListGen() []string { - cmd := exec.Command("gpg", "-k", "--with-colons") - gpgOutputBytes, _ := cmd.Output() - gpgOutputLines := strings.Split(string(gpgOutputBytes), "\n") - var uidSlice []string - for _, line := range gpgOutputLines { - if strings.HasPrefix(line, "uid") { - uid := strings.Split(line, ":")[9] - uidSlice = append(uidSlice, uid) - } - } - return uidSlice -} - -// GpgKeyGen generates a new GPG key and returns the key ID. -func GpgKeyGen() string { - gpgGenTempFile := CreateTempFile() - defer func(name string) { - _ = os.Remove(name) // error ignored; if the file could be created, it can probably be removed - }(gpgGenTempFile.Name()) - - // create and write gpg-gen file - unixTime := strconv.FormatInt(time.Now().Unix(), 10) - _, _ = gpgGenTempFile.WriteString(strings.Join([]string{"Key-Type: eddsa", "Key-Curve: ed25519", "Key-Usage: sign", "Subkey-Type: ecdh", "Subkey-Curve: cv25519", "Subkey-Usage: encrypt", "Name-Real: libmutton-" + unixTime, "Name-Comment: gpg-libmutton", "Name-Email: github.com/rwinkhart/libmutton", "Expire-Date: 0"}, "\n")) // error ignored; if the file could be created, it can probably be written to - - // close gpg-gen file - _ = gpgGenTempFile.Close() // error ignored; if the file could be created, it can probably be closed - - // generate GPG key based on gpg-gen file - cmd := exec.Command("gpg", "-q", "--batch", "--generate-key", gpgGenTempFile.Name()) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - err := cmd.Run() +// RCWSanityCheckGen generates the RCW sanity check file for libmutton. +func RCWSanityCheckGen(passphrase []byte) { + err := wrappers.GenSanityCheck(ConfigDir+"/sanity.rcw", passphrase) if err != nil { - PrintError("Failed to generate GPG key: "+err.Error(), ErrorOther, true) + PrintError("Failed to generate sanity check file: "+err.Error(), ErrorWrite, true) } - - return "libmutton-" + unixTime + " (gpg-libmutton) " } // DirInit creates the libmutton directories. diff --git a/core/launchClipClearProcessCLIGeneric.go b/core/launchClipClearProcessCLIGeneric.go index 44a7ba5..e5fc5eb 100644 --- a/core/launchClipClearProcessCLIGeneric.go +++ b/core/launchClipClearProcessCLIGeneric.go @@ -12,7 +12,7 @@ import ( func LaunchClipClearProcess(copySubject string) { executableName := os.Args[0] cmd := exec.Command(executableName, "clipclear") - WriteToStdin(cmd, copySubject) + writeToStdin(cmd, copySubject) err := cmd.Start() if err != nil { PrintError("Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?", ErrorClipboard, true) diff --git a/core/launchClipClearProcessCLIUNIX.go b/core/launchClipClearProcessCLIUNIX.go index 40e83d3..95078f8 100644 --- a/core/launchClipClearProcessCLIUNIX.go +++ b/core/launchClipClearProcessCLIUNIX.go @@ -13,7 +13,7 @@ import ( func LaunchClipClearProcess(copySubject string, isWayland bool) { executableName := os.Args[0] cmd := exec.Command(executableName, "clipclear", strconv.FormatBool(isWayland)) - WriteToStdin(cmd, copySubject) + writeToStdin(cmd, copySubject) err := cmd.Start() if err != nil { PrintError("Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?", ErrorClipboard, true) diff --git a/core/rcw.go b/core/rcw.go new file mode 100644 index 0000000..185e41f --- /dev/null +++ b/core/rcw.go @@ -0,0 +1,27 @@ +package core + +import ( + "os" + "strings" + + "github.com/rwinkhart/rcw/wrappers" +) + +// DecryptFileToSlice decrypts an RCW wrapped file and returns the contents as a slice of (trimmed) strings. +func DecryptFileToSlice(targetLocation string, passphrase []byte) []string { + encBytes, err := os.ReadFile(targetLocation) + if err != nil { + PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), ErrorDecryption, true) + } + decBytes, err := wrappers.Decrypt(encBytes, passphrase) + if err != nil { + PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), ErrorDecryption, true) + } + return strings.Split(string(decBytes), "\n") +} + +// EncryptBytes encrypts a byte slice using RCW and returns the encrypted data. +func EncryptBytes(decBytes []byte, passphrase []byte) []byte { + encBytes := wrappers.Encrypt(decBytes, passphrase) + return encBytes +} diff --git a/core/utilitiesMisc.go b/core/utilitiesMisc.go index dfbebff..fb93abd 100644 --- a/core/utilitiesMisc.go +++ b/core/utilitiesMisc.go @@ -36,17 +36,16 @@ func TargetIsFile(targetLocation string, errorOnFail bool, failCondition uint8) } // WriteEntry writes entryData to an encrypted file at targetLocation. -func WriteEntry(targetLocation string, entryData []string) { - encryptedBytes := EncryptGPG(entryData) +func WriteEntry(targetLocation string, entryData []byte, passphrase []byte) { + encryptedBytes := EncryptBytes(entryData, passphrase) err := os.WriteFile(targetLocation, encryptedBytes, 0600) if err != nil { PrintError("Failed to write to file: "+err.Error(), ErrorWrite, true) } } -// WriteToStdin is a utility function that writes a string to a command's stdin. -// TODO unexport (import?) after migration off of GPG -func WriteToStdin(cmd *exec.Cmd, input string) { +// writeToStdin is a utility function that writes a string to a command's stdin. +func writeToStdin(cmd *exec.Cmd, input string) { stdin, err := cmd.StdinPipe() if err != nil { PrintError("Failed to access stdin for system command: "+err.Error(), ErrorOther, true) diff --git a/go.mod b/go.mod index 4bac151..4b9f13d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727 github.com/pkg/sftp v1.13.9 github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481 + github.com/rwinkhart/rcw v0.0.0-20250504183741-f176d57b6ba5 golang.design/x/clipboard v0.7.0 // only for Android builds golang.org/x/crypto v0.37.0 gopkg.in/ini.v1 v1.67.0 diff --git a/go.sum b/go.sum index a9844f0..a396d1a 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb 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/rwinkhart/rcw v0.0.0-20250504183741-f176d57b6ba5 h1:BX31d76TLGZqeukYECLFPkLD+SS/CAV12Lkdlc6UuGM= +github.com/rwinkhart/rcw v0.0.0-20250504183741-f176d57b6ba5/go.mod h1:giXrq9o5a7bwMSuMvz+5bb2+qbM+zV4pXaG/SceXfLM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/wiki/bugs.md b/wiki/bugs.md deleted file mode 100644 index 8849a4d..0000000 --- a/wiki/bugs.md +++ /dev/null @@ -1,3 +0,0 @@ -## Known Bugs - libmutton -- On Windows, GPG is sometimes (seems unpredictable) incredibly slow to start (often after a reboot), leading to many operations seemingly hanging - - **This will be addressed** in the migration off of GPG that will take place before v1.0.0 diff --git a/wiki/developers.md b/wiki/developers.md index a7070ea..ab4bdbf 100644 --- a/wiki/developers.md +++ b/wiki/developers.md @@ -30,7 +30,6 @@ On UNIX-like systems, this is located at `~/.config/libmutton/libmutton.ini`. On The current base layout of `libmutton.ini` will change leading up to release v1.0.0. As of right now, the specification is as follows: ``` [LIBMUTTON] -gpgID = sshUser = sshIP = sshPort = @@ -45,6 +44,3 @@ If creating a third-party client that requires extra configuration to be stored, configKey = ``` This ensures that a user can use multiple client applications with the same configuration while avoiding conflicts. - -# Relevant Bugs Affecting Third-Party Client Implementations -- Password-protected SSH identity files currently only prompt for password entry in the CLI, and thus they are not yet supported in GUI/TUI implementations