Initial migration from GPG to RCW

This commit is contained in:
2025-05-04 17:41:44 -04:00
parent a95bff8ef4
commit 649b60e2fe
17 changed files with 51 additions and 123 deletions
-6
View File
@@ -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
}
-17
View File
@@ -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))
}
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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 {
-35
View File
@@ -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
}
+6 -42
View File
@@ -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) <github.com/rwinkhart/libmutton>"
}
// DirInit creates the libmutton directories.
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+27
View File
@@ -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
}
+4 -5
View File
@@ -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)
+1
View File
@@ -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
+2
View File
@@ -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=
-3
View File
@@ -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
-4
View File
@@ -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 = <gpg key id>
sshUser = <remote user>
sshIP = <remote ip>
sshPort = <remote ssh port>
@@ -45,6 +44,3 @@ If creating a third-party client that requires extra configuration to be stored,
configKey = <value>
```
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