mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-29 21:36:41 -04:00
Initial migration from GPG to RCW
This commit is contained in:
@@ -12,9 +12,3 @@ const (
|
|||||||
PathSeparator = "/" // Platform-specific path separator
|
PathSeparator = "/" // Platform-specific path separator
|
||||||
IsWindows = false // Platform indicator
|
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
|
package core
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
EntryRoot = Home + "\\AppData\\Local\\libmutton\\entries" // Path to libmutton entry directory
|
EntryRoot = Home + "\\AppData\\Local\\libmutton\\entries" // Path to libmutton entry directory
|
||||||
ConfigDir = Home + "\\AppData\\Local\\libmutton\\config" // Path to libmutton configuration directory
|
ConfigDir = Home + "\\AppData\\Local\\libmutton\\config" // Path to libmutton configuration directory
|
||||||
@@ -17,15 +12,3 @@ const (
|
|||||||
PathSeparator = "\\" // Platform-specific path separator
|
PathSeparator = "\\" // Platform-specific path separator
|
||||||
IsWindows = true // Platform indicator
|
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.
|
// 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 {
|
if isFile, _ := TargetIsFile(targetLocation, true, 2); isFile {
|
||||||
|
|
||||||
decryptedEntry := DecryptGPG(targetLocation)
|
decryptedEntry := DecryptFileToSlice(targetLocation, passphrase)
|
||||||
var copySubject string // will store data to be copied
|
var copySubject string // will store data to be copied
|
||||||
|
|
||||||
// ensure field exists in entry
|
// ensure field exists in entry
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import (
|
|||||||
// copyString copies a string to the clipboard.
|
// copyString copies a string to the clipboard.
|
||||||
func copyString(continuous bool, copySubject string) {
|
func copyString(continuous bool, copySubject string) {
|
||||||
cmd := exec.Command("pbcopy")
|
cmd := exec.Command("pbcopy")
|
||||||
WriteToStdin(cmd, copySubject)
|
writeToStdin(cmd, copySubject)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
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.
|
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||||
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||||
cmdClear := exec.Command("pbcopy")
|
cmdClear := exec.Command("pbcopy")
|
||||||
WriteToStdin(cmdClear, "")
|
writeToStdin(cmdClear, "")
|
||||||
return exec.Command("pbpaste"), cmdClear
|
return exec.Command("pbpaste"), cmdClear
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import (
|
|||||||
// copyString copies a string to the clipboard.
|
// copyString copies a string to the clipboard.
|
||||||
func copyString(continuous bool, copySubject string) {
|
func copyString(continuous bool, copySubject string) {
|
||||||
cmd := exec.Command("termux-clipboard-set")
|
cmd := exec.Command("termux-clipboard-set")
|
||||||
WriteToStdin(cmd, copySubject)
|
writeToStdin(cmd, copySubject)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
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.
|
// getClipCommands returns the commands for pasting and clearing the clipboard contents.
|
||||||
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
func getClipCommands() (*exec.Cmd, *exec.Cmd) {
|
||||||
cmdClear := exec.Command("termux-clipboard-set")
|
cmdClear := exec.Command("termux-clipboard-set")
|
||||||
WriteToStdin(cmdClear, "")
|
writeToStdin(cmdClear, "")
|
||||||
return exec.Command("termux-clipboard-get"), 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)
|
PrintError("Clipboard platform could not be determined", ErrorClipboard, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteToStdin(cmdCopy, copySubject)
|
writeToStdin(cmdCopy, copySubject)
|
||||||
err := cmdCopy.Run()
|
err := cmdCopy.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
// GetOldEntryData decrypts and returns old entry data (with all required lines present).
|
// 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
|
// ensure targetLocation exists
|
||||||
TargetIsFile(targetLocation, true, 2)
|
TargetIsFile(targetLocation, true, 2)
|
||||||
|
|
||||||
// read old entry data
|
// read old entry data
|
||||||
unencryptedEntry := DecryptGPG(targetLocation)
|
unencryptedEntry := DecryptFileToSlice(targetLocation, passphrase)
|
||||||
|
|
||||||
// return the old entry data with all required lines present
|
// return the old entry data with all required lines present
|
||||||
if field > 0 {
|
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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
"github.com/rwinkhart/rcw/wrappers"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GpgUIDListGen generates a list of all GPG key IDs on the system and returns them as a slice of strings.
|
// RCWSanityCheckGen generates the RCW sanity check file for libmutton.
|
||||||
func GpgUIDListGen() []string {
|
func RCWSanityCheckGen(passphrase []byte) {
|
||||||
cmd := exec.Command("gpg", "-k", "--with-colons")
|
err := wrappers.GenSanityCheck(ConfigDir+"/sanity.rcw", passphrase)
|
||||||
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()
|
|
||||||
if err != nil {
|
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.
|
// DirInit creates the libmutton directories.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func LaunchClipClearProcess(copySubject string) {
|
func LaunchClipClearProcess(copySubject string) {
|
||||||
executableName := os.Args[0]
|
executableName := os.Args[0]
|
||||||
cmd := exec.Command(executableName, "clipclear")
|
cmd := exec.Command(executableName, "clipclear")
|
||||||
WriteToStdin(cmd, copySubject)
|
writeToStdin(cmd, copySubject)
|
||||||
err := cmd.Start()
|
err := cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PrintError("Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?", ErrorClipboard, true)
|
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) {
|
func LaunchClipClearProcess(copySubject string, isWayland bool) {
|
||||||
executableName := os.Args[0]
|
executableName := os.Args[0]
|
||||||
cmd := exec.Command(executableName, "clipclear", strconv.FormatBool(isWayland))
|
cmd := exec.Command(executableName, "clipclear", strconv.FormatBool(isWayland))
|
||||||
WriteToStdin(cmd, copySubject)
|
writeToStdin(cmd, copySubject)
|
||||||
err := cmd.Start()
|
err := cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PrintError("Failed to launch automated clipboard clearing process - Does this libmutton implementation support the \"clipclear\" argument?", ErrorClipboard, true)
|
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.
|
// WriteEntry writes entryData to an encrypted file at targetLocation.
|
||||||
func WriteEntry(targetLocation string, entryData []string) {
|
func WriteEntry(targetLocation string, entryData []byte, passphrase []byte) {
|
||||||
encryptedBytes := EncryptGPG(entryData)
|
encryptedBytes := EncryptBytes(entryData, passphrase)
|
||||||
err := os.WriteFile(targetLocation, encryptedBytes, 0600)
|
err := os.WriteFile(targetLocation, encryptedBytes, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PrintError("Failed to write to file: "+err.Error(), ErrorWrite, true)
|
PrintError("Failed to write to file: "+err.Error(), ErrorWrite, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteToStdin is a utility function that writes a string to a command's stdin.
|
// 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) {
|
||||||
func WriteToStdin(cmd *exec.Cmd, input string) {
|
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PrintError("Failed to access stdin for system command: "+err.Error(), ErrorOther, true)
|
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/fortis/go-steam-totp v0.0.0-20171114202746-18e928674727
|
||||||
github.com/pkg/sftp v1.13.9
|
github.com/pkg/sftp v1.13.9
|
||||||
github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481
|
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.design/x/clipboard v0.7.0 // only for Android builds
|
||||||
golang.org/x/crypto v0.37.0
|
golang.org/x/crypto v0.37.0
|
||||||
gopkg.in/ini.v1 v1.67.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/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 h1:FkxbO331O7mS5EJkP+MCi0o2gswh/Aezs+//NmefrR8=
|
||||||
github.com/pquerna/otp v1.4.1-0.20231130234153-3357de7c0481/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
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=
|
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:
|
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]
|
[LIBMUTTON]
|
||||||
gpgID = <gpg key id>
|
|
||||||
sshUser = <remote user>
|
sshUser = <remote user>
|
||||||
sshIP = <remote ip>
|
sshIP = <remote ip>
|
||||||
sshPort = <remote ssh port>
|
sshPort = <remote ssh port>
|
||||||
@@ -45,6 +44,3 @@ If creating a third-party client that requires extra configuration to be stored,
|
|||||||
configKey = <value>
|
configKey = <value>
|
||||||
```
|
```
|
||||||
This ensures that a user can use multiple client applications with the same configuration while avoiding conflicts.
|
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