mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-27 20:36:29 -04:00
Initial migration from GPG to RCW
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user