mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-08-27 20:36:29 -04:00
Refactor for better modularity (share more code among edit functions, rename "offline" package to "backend"
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// global variables used across multiple files
|
||||
var (
|
||||
home, _ = os.UserHomeDir()
|
||||
)
|
||||
|
||||
// global constants used across multiple files
|
||||
const (
|
||||
AnsiError = "\033[38;5;9m"
|
||||
AnsiReset = "\033[0m"
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build !windows
|
||||
|
||||
package backend
|
||||
|
||||
// EntryRoot path to libmutton entry directory
|
||||
var EntryRoot = home + "/.local/share/libmutton"
|
||||
var ConfigDir = home + "/.config/libmutton"
|
||||
var ConfigPath = ConfigDir + "/libmutton.ini"
|
||||
|
||||
// PathSeparator defines the character used to separate directories in a path (platform-specific)
|
||||
const (
|
||||
PathSeparator = "/"
|
||||
Windows = false // TODO temporary, remove after native sync is implemented
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
//go:build windows
|
||||
|
||||
package backend
|
||||
|
||||
// EntryRoot path to libmutton entry directory
|
||||
var EntryRoot = home + "\\AppData\\Local\\libmutton\\entries"
|
||||
var ConfigDir = home + "\\AppData\\Local\\libmutton"
|
||||
var ConfigPath = ConfigDir + "\\libmutton.ini"
|
||||
|
||||
// PathSeparator defines the character used to separate directories in a path (platform-specific)
|
||||
const (
|
||||
PathSeparator = "\\"
|
||||
Windows = true // TODO temporary, remove after native sync is implemented
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// AddFolder creates a new directory at targetLocation
|
||||
func AddFolder(targetLocation string) {
|
||||
// create the directory specified by targetLocation
|
||||
err := os.Mkdir(targetLocation, 0700)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
fmt.Println(AnsiError + "Directory already exists" + AnsiReset)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Println(AnsiError + "Failed to create directory: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
// TODO If in online mode, create the directory on the server
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/ini.v1"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ReadConfig reads the libmutton.ini file and returns a map of the requested values
|
||||
// requires readMap: a map of section names to key names (indicates requested values)
|
||||
// returns configMap: a map of key names to values (sections are irrelevant)
|
||||
func ReadConfig(readKeys []string) []string {
|
||||
cfg, err := ini.Load(ConfigPath)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to load libmutton.ini" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var config []string
|
||||
|
||||
for _, key := range readKeys {
|
||||
keyConfig := cfg.Section("LIBMUTTON").Key(key).String()
|
||||
|
||||
// ensure specified key has a value
|
||||
if keyConfig == "" {
|
||||
fmt.Println(AnsiError + "Failed to find value for key \"" + key + "\" in section \"[LIBMUTTON]\" in libmutton.ini" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config = append(config, keyConfig)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// libmuttn.ini layout
|
||||
// [LIBMUTTON]
|
||||
// gpgID = <gpg key id>
|
||||
// textEditor = <editor command>
|
||||
// onlineMode = <true/false>
|
||||
// sshError = <true/false>
|
||||
// netPinEnabled = <true/false>
|
||||
// remoteUser = <ssh user>
|
||||
// remoteIP = <ssh ip>
|
||||
// remotePort = <ssh port>
|
||||
// identityFile = <path to private key>
|
||||
@@ -0,0 +1,88 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
steamtotp "github.com/fortis/go-steam-totp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CopyArgument copies a field from an entry to the clipboard
|
||||
func CopyArgument(targetLocation string, field int, executableName string) {
|
||||
if isFile, _ := TargetIsFile(targetLocation, true, 2); isFile {
|
||||
|
||||
decryptedEntry := DecryptGPG(targetLocation)
|
||||
var copySubject string // will store data to be copied
|
||||
|
||||
// ensure field exists in entry
|
||||
if len(decryptedEntry) > field {
|
||||
|
||||
// ensure field is not empty
|
||||
if decryptedEntry[field] == "" {
|
||||
fmt.Println(AnsiError + "Field is empty" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if field != 5 { // TODO Update field after removed from notes (breaking sshyp entry compatibility)
|
||||
copySubject = decryptedEntry[field]
|
||||
} 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[5], "steam@") {
|
||||
secret = decryptedEntry[5][6:]
|
||||
forSteam = true
|
||||
} else {
|
||||
secret = decryptedEntry[5]
|
||||
}
|
||||
|
||||
for { // keep field copied to clipboard, refresh on 30-second intervals
|
||||
currentTime := time.Now()
|
||||
copyField(GenTOTP(secret, currentTime, forSteam), "")
|
||||
// sleep until next 30-second interval
|
||||
time.Sleep(time.Duration(30-(currentTime.Second()%30)) * time.Second)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println(AnsiError + "Field does not exist in entry" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// copy field to clipboard, launch clipboard clearing process
|
||||
copyField(copySubject, executableName)
|
||||
}
|
||||
}
|
||||
|
||||
// ClipClearArgument is called to clear the clipboard after 30 seconds if the contents have not been modified
|
||||
func ClipClearArgument() {
|
||||
// read previous clipboard contents from stdin
|
||||
clipScanner := bufio.NewScanner(os.Stdin)
|
||||
if clipScanner.Scan() {
|
||||
oldContents := clipScanner.Text()
|
||||
clipClear(oldContents)
|
||||
} else {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// GenTOTP generates a TOTP token from a secret (supports standard and Steam TOTP)
|
||||
func GenTOTP(secret string, time time.Time, forSteam bool) string {
|
||||
var totpToken string
|
||||
var err error
|
||||
|
||||
if forSteam {
|
||||
totpToken, err = steamtotp.GenerateAuthCode(secret, time)
|
||||
} else {
|
||||
totpToken, err = totp.GenerateCode(secret, time)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Error generating TOTP code" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return totpToken
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//go:build darwin
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO MacOS support is entirely untested - I would appreciate feedback on this implementation
|
||||
|
||||
// copyField copies a field from an entry to the clipboard
|
||||
func copyField(copySubject string, executableName string) {
|
||||
cmd := exec.Command("pbcopy")
|
||||
writeToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to copy to clipboard: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// launch clipboard clearing process if executableName is provided
|
||||
if executableName != "" {
|
||||
cmd = exec.Command(executableName, "clipclear")
|
||||
writeToStdin(cmd, copySubject)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh
|
||||
}
|
||||
}
|
||||
|
||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds
|
||||
func clipClear(oldContents string) {
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
cmd := exec.Command("pbpaste")
|
||||
newContents, _ := cmd.Output()
|
||||
|
||||
if oldContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||
cmd = exec.Command("pbcopy")
|
||||
writeToStdin(cmd, "")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to clear clipboard: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//go:build termux
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// copyField copies a field from an entry to the clipboard
|
||||
func copyField(copySubject string, executableName string) {
|
||||
cmd := exec.Command("termux-clipboard-set")
|
||||
writeToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to copy to clipboard: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// launch clipboard clearing process if executableName is provided
|
||||
if executableName != "" {
|
||||
cmd = exec.Command(executableName, "clipclear")
|
||||
writeToStdin(cmd, copySubject)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh
|
||||
}
|
||||
}
|
||||
|
||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds
|
||||
func clipClear(oldContents string) {
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
cmd := exec.Command("termux-clipboard-get")
|
||||
newContents, _ := cmd.Output()
|
||||
|
||||
if oldContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||
cmd = exec.Command("termux-clipboard-set")
|
||||
writeToStdin(cmd, "")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to clear clipboard: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//go:build !windows && !darwin && !termux
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// copyField copies a field from an entry to the clipboard
|
||||
func copyField(copySubject string, executableName string) {
|
||||
var envSet bool // track whether environment variables are set
|
||||
var cmd *exec.Cmd
|
||||
// determine whether to use wl-copy (Wayland) or xclip (X11)
|
||||
if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet {
|
||||
cmd = exec.Command("wl-copy")
|
||||
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
|
||||
cmd = exec.Command("xclip", "-sel", "c")
|
||||
} else {
|
||||
fmt.Println(AnsiError + "Clipboard platform could not be determined - note that the clipboard does not function in a raw TTY" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
writeToStdin(cmd, copySubject)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to copy to clipboard: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// launch clipboard clearing process if executableName is provided
|
||||
if executableName != "" {
|
||||
cmd = exec.Command(executableName, "clipclear")
|
||||
writeToStdin(cmd, copySubject)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh
|
||||
}
|
||||
}
|
||||
|
||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds
|
||||
func clipClear(oldContents string) {
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
var envSet bool // track whether environment variables are set
|
||||
var platform bool // track clipboard platform, false for Wayland, true for X11
|
||||
var cmd *exec.Cmd
|
||||
// determine whether to use wl-copy (Wayland) or xclip (X11)
|
||||
if _, envSet = os.LookupEnv("WAYLAND_DISPLAY"); envSet {
|
||||
cmd = exec.Command("wl-paste")
|
||||
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
|
||||
cmd = exec.Command("xclip", "-o", "-sel", "c")
|
||||
platform = true
|
||||
}
|
||||
newContents, _ := cmd.Output()
|
||||
|
||||
if oldContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||
switch platform {
|
||||
case false:
|
||||
cmd = exec.Command("wl-copy", "-c")
|
||||
case true:
|
||||
cmd = exec.Command("xclip", "-i", "/dev/null", "-sel", "c")
|
||||
}
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to clear clipboard: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//go:build windows
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// copyField copies a field from an entry to the clipboard
|
||||
func copyField(copySubject string, executableName string) {
|
||||
cmd := exec.Command("powershell.exe", "-c", fmt.Sprintf("echo '%s' | Set-Clipboard", strings.ReplaceAll(copySubject, "'", "''")))
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to copy to clipboard: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// launch clipboard clearing process if executableName is provided
|
||||
if executableName != "" {
|
||||
cmd = exec.Command(executableName, "clipclear")
|
||||
writeToStdin(cmd, copySubject)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to launch automated clipboard clearing process - does this libmutton implementation support the \"clipclear\" argument?" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0) // only exit if clipboard clearing process is launched, otherwise assume continuous clipboard refresh
|
||||
}
|
||||
}
|
||||
|
||||
// clipClear is called in a separate process to clear the clipboard after 30 seconds
|
||||
func clipClear(oldContents string) {
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
cmd := exec.Command("powershell.exe", "-c", "Get-Clipboard")
|
||||
newContents, _ := cmd.Output()
|
||||
|
||||
if oldContents == strings.TrimRight(string(newContents), "\r\n") {
|
||||
cmd = exec.Command("powershell.exe", "-c", "Set-Clipboard")
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to clear clipboard: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// GetOldEntryData decrypts and returns old entry data (with all required lines present)
|
||||
func GetOldEntryData(targetLocation string, field int) []string {
|
||||
// ensure targetLocation exists
|
||||
TargetIsFile(targetLocation, true, 2)
|
||||
|
||||
// read old entry data
|
||||
unencryptedEntry := DecryptGPG(targetLocation)
|
||||
|
||||
// return the old entry data with all required lines present
|
||||
if field > 0 {
|
||||
return EnsureSliceLength(unencryptedEntry, field)
|
||||
} else {
|
||||
return unencryptedEntry
|
||||
}
|
||||
}
|
||||
|
||||
// Rename renames oldLocation to newLocation
|
||||
func Rename(oldLocation string, newLocation string) {
|
||||
// ensure newLocation does not exist
|
||||
_, isAccessible := TargetIsFile(newLocation, false, 0)
|
||||
if isAccessible {
|
||||
fmt.Println(AnsiError + "\"" + newLocation + "\" already exists" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// rename oldLocation to newLocation
|
||||
err := os.Rename(oldLocation, newLocation)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to rename - does the target containing directory exists?" + AnsiReset)
|
||||
}
|
||||
|
||||
// TODO If in online mode, check if oldLocation is a directory and rename it on the server
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// EnsureSliceLength ensures slice is long enough to contain the specified index
|
||||
func EnsureSliceLength(slice []string, index int) []string {
|
||||
for len(slice) <= index {
|
||||
slice = append(slice, "")
|
||||
}
|
||||
return slice
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO GPG support is a temporary feature - it will be replaced with a different encryption scheme in the future
|
||||
// TODO These functions may continue to exist after that point, but consider them deprecated
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to decrypt \"" + targetLocation + "\" - ensure it is a valid GPG-encrypted file and that you entered your passphrase correctly" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
outputSlice := strings.Split(string(output), "\n")
|
||||
return outputSlice
|
||||
}
|
||||
|
||||
// EncryptGPG encrypts a slice of strings using GPG and returns the encrypted data as a byte slice
|
||||
func EncryptGPG(input []string) []byte {
|
||||
cmd := exec.Command("gpg", "-q", "-r", ReadConfig([]string{"gpgID"})[0], "-e")
|
||||
writeToStdin(cmd, strings.Join(input, "\n"))
|
||||
encryptedBytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to encrypt data - ensure that your GPG key is valid and that you have a valid GPG ID set in libmutton.ini" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
return encryptedBytes
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TempInit ensures libmutton directories exist and writes the libmutton configuration file
|
||||
func TempInit(configFileMap map[string]string) {
|
||||
// create EntryRoot and ConfigDir
|
||||
dirInit()
|
||||
|
||||
// remove existing config file
|
||||
removeFile(ConfigPath)
|
||||
|
||||
if configFileMap["textEditor"] == "" {
|
||||
configFileMap["textEditor"] = textEditorFallback()
|
||||
}
|
||||
|
||||
// create and write config file
|
||||
configFile, _ := os.OpenFile(ConfigPath, os.O_CREATE|os.O_WRONLY, 0600)
|
||||
defer configFile.Close()
|
||||
configFile.WriteString("[LIBMUTTON]\n")
|
||||
for key, value := range configFileMap {
|
||||
configFile.WriteString(key + " = " + value + "\n")
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 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 os.Remove(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"))
|
||||
|
||||
// close gpg-gen file
|
||||
gpgGenTempFile.Close()
|
||||
|
||||
// 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
|
||||
cmd.Run()
|
||||
|
||||
return "libmutton-" + unixTime + " (gpg-libmutton) <github.com/rwinkhart/libmutton>"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//go:build !windows
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const FallbackEditor = "vi" // vi is pre-installed on most UNIX systems
|
||||
|
||||
// dirInit creates the libmutton directories
|
||||
func dirInit() {
|
||||
// create EntryRoot
|
||||
err := os.MkdirAll(EntryRoot, 0700)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to create \"" + EntryRoot + "\":" + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// create config directory
|
||||
err = os.MkdirAll(ConfigDir, 0700)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to create \"" + ConfigDir + "\":" + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// textEditorFallback returns the value of the $EDITOR environment variable, or FallbackEditor if it is not set
|
||||
func textEditorFallback() string {
|
||||
// ensure textEditor is set
|
||||
textEditor := os.Getenv("EDITOR")
|
||||
if textEditor == "" {
|
||||
textEditor = FallbackEditor
|
||||
}
|
||||
return textEditor
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build windows
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const FallbackEditor = "nvim" // since there is no pre-installed CLI editor on Windows, default to the most popular one
|
||||
|
||||
// dirInit creates the libmutton directories
|
||||
func dirInit() {
|
||||
// create EntryRoot (includes config directory on Windows)
|
||||
err := os.MkdirAll(EntryRoot, 0700)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to create \"" + EntryRoot + "\":" + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// textEditorFallback returns FallbackEditor
|
||||
func textEditorFallback() string {
|
||||
return FallbackEditor
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Shear(targetLocation string) {
|
||||
// TODO If in online mode, remove from server and add to shear list
|
||||
TargetIsFile(targetLocation, true, 0) // needed because os.RemoveAll does not return an error if target does not exist
|
||||
err := os.RemoveAll(targetLocation)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to remove target: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build !windows
|
||||
|
||||
package backend
|
||||
|
||||
// TargetLocationFormat returns the target location of an entry formatted for the current platform
|
||||
func TargetLocationFormat(entryName string) string {
|
||||
return EntryRoot + PathSeparator + entryName
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build windows
|
||||
|
||||
package backend
|
||||
|
||||
import "strings"
|
||||
|
||||
// TargetLocationFormat returns the target location of an entry formatted for the current platform
|
||||
func TargetLocationFormat(entryName string) string {
|
||||
return EntryRoot + PathSeparator + strings.ReplaceAll(entryName, "/", PathSeparator)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const extendedCharset = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
|
||||
|
||||
// TargetIsFile TargetStatusCheck checks if the targetLocation is a file, directory, or is inaccessible
|
||||
// failCondition: 0 = fail on inaccessible, 1 = fail on inaccessible/file, 2 = fail on inaccessible/directory
|
||||
// returns: isFile, isAccessible
|
||||
func TargetIsFile(targetLocation string, errorOnFail bool, failCondition uint8) (bool, bool) {
|
||||
targetInfo, err := os.Stat(targetLocation)
|
||||
if err != nil {
|
||||
if errorOnFail {
|
||||
fmt.Println(AnsiError + "Failed to access \"" + targetLocation + "\" - ensure it exists and has the correct permissions" + AnsiReset)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
if targetInfo.IsDir() {
|
||||
if errorOnFail && failCondition == 2 {
|
||||
fmt.Println(AnsiError + "\"" + targetLocation + "\" is a directory" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
return false, true
|
||||
} else {
|
||||
if errorOnFail && failCondition == 1 {
|
||||
fmt.Println(AnsiError + "\"" + targetLocation + "\" is a file" + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
|
||||
// WriteEntry writes entryData to an encrypted file at targetLocation
|
||||
func WriteEntry(targetLocation string, entryData []string) {
|
||||
encryptedBytes := EncryptGPG(entryData)
|
||||
err := os.WriteFile(targetLocation, encryptedBytes, 0600)
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to write to file: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// writeToStdin writes a string to a command's stdin
|
||||
func writeToStdin(cmd *exec.Cmd, input string) {
|
||||
stdin, _ := cmd.StdinPipe()
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
io.WriteString(stdin, input)
|
||||
}()
|
||||
}
|
||||
|
||||
// CreateTempFile creates a temporary file and returns a pointer to it
|
||||
func CreateTempFile() *os.File {
|
||||
tempFile, err := os.CreateTemp("", "*.markdown")
|
||||
if err != nil {
|
||||
fmt.Println(AnsiError + "Failed to create temporary file: " + err.Error() + AnsiReset)
|
||||
os.Exit(1)
|
||||
}
|
||||
return tempFile
|
||||
}
|
||||
|
||||
// removeFile removes a file at targetLocation and does not error if the file does not exist
|
||||
func removeFile(targetLocation string) {
|
||||
// remove existing config file
|
||||
err := os.Remove(targetLocation)
|
||||
if err != nil {
|
||||
// ignore error if file does not exist
|
||||
if !os.IsNotExist(err) {
|
||||
fmt.Println(AnsiError + "Failed to remove \"" + targetLocation + "\":" + err.Error() + AnsiReset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTrailingEmptyStrings removes empty strings from the end of a slice
|
||||
func RemoveTrailingEmptyStrings(slice []string) []string {
|
||||
for i := len(slice) - 1; i >= 0; i-- {
|
||||
if slice[i] != "" {
|
||||
return slice[:i+1]
|
||||
}
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// StringGen generates a random string of a specified length and complexity
|
||||
// complexity: minimum percentage of special characters to be returned in the generated string (only impacts complex strings)
|
||||
func StringGen(length int, complex bool, complexity float64) string {
|
||||
var actualSpecialChars int // track the number of special characters in the generated string
|
||||
var minSpecialChars int // track the minimum number of special characters to accept
|
||||
|
||||
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // default character set used for all strings
|
||||
if complex {
|
||||
minSpecialChars = int(math.Round(float64(length) * complexity)) // determine minimum number of special characters to accept
|
||||
charset = charset + extendedCharset
|
||||
}
|
||||
|
||||
// loop until a string of the desired complexity is generated
|
||||
for {
|
||||
// generate a random string
|
||||
result := make([]byte, length)
|
||||
for i := range result {
|
||||
val, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||
result[i] = charset[val.Int64()]
|
||||
}
|
||||
|
||||
// return early if the string is not complex
|
||||
if !complex {
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// count the number of special characters in the generated string
|
||||
for _, char := range string(result) {
|
||||
if strings.ContainsRune(extendedCharset, char) {
|
||||
actualSpecialChars++
|
||||
}
|
||||
}
|
||||
|
||||
// return the generated string if it contains enough special characters
|
||||
if actualSpecialChars >= minSpecialChars {
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// reset special character counter
|
||||
fmt.Println("Regenerating string until desired complexity is achieved...")
|
||||
actualSpecialChars = 0
|
||||
}
|
||||
}
|
||||
|
||||
// EntryIsNotEmpty iterates through entryData and returns true if any line is not empty
|
||||
func EntryIsNotEmpty(entryData []string) bool {
|
||||
for _, line := range entryData {
|
||||
if line != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user