code that works
This commit is contained in:
parent
8de3930a00
commit
746899a2c3
177
cmd/agedit/cli.go
Normal file
177
cmd/agedit/cli.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.burning.moe/celediel/agedit/internal/config"
|
||||
"git.burning.moe/celediel/agedit/pkg/editor"
|
||||
"git.burning.moe/celediel/agedit/pkg/encrypt"
|
||||
"git.burning.moe/celediel/agedit/pkg/env"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/ilyakaznacheev/cleanenv"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
name = "agedit"
|
||||
usage = "Edit age encrypted files with your $EDITOR"
|
||||
version = "0.0.1"
|
||||
help_template = `NAME:
|
||||
{{.Name}} {{if .Version}}v{{.Version}}{{end}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.HelpName}} {{if .VisibleFlags}}[flags]{{end}} [filename]
|
||||
{{if len .Authors}}
|
||||
AUTHOR:
|
||||
{{range .Authors}}{{ . }}{{end}}
|
||||
{{end}}{{if .Commands}}
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}{{if .Copyright }}
|
||||
COPYRIGHT:
|
||||
{{.Copyright}}
|
||||
{{end}}
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
authors = []*cli.Author{{
|
||||
Name: "Lilian Jónsdóttir",
|
||||
Email: "lilian.jonsdottir@gmail.com",
|
||||
}}
|
||||
|
||||
flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "identity",
|
||||
Usage: "age identity file to use",
|
||||
Aliases: []string{"i"},
|
||||
Action: func(ctx *cli.Context, s string) error {
|
||||
if identity_file := ctx.String("identity"); identity_file != "" {
|
||||
cfg.IdentityFile = identity_file
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "out",
|
||||
Usage: "write to this file instead of the input file",
|
||||
Aliases: []string{"o"},
|
||||
Action: func(ctx *cli.Context, s string) error {
|
||||
output_file = ctx.String("out")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log",
|
||||
Usage: "log level",
|
||||
Value: "warn",
|
||||
Aliases: []string{"l"},
|
||||
Action: func(ctx *cli.Context, s string) error {
|
||||
if lvl, err := log.ParseLevel(ctx.String("log")); err == nil {
|
||||
logger.SetLevel(lvl)
|
||||
// Some extra info for debug level
|
||||
if logger.GetLevel() == log.DebugLevel {
|
||||
logger.SetReportCaller(true)
|
||||
}
|
||||
} else {
|
||||
logger.SetLevel(log.WarnLevel)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "editor",
|
||||
Usage: "specify the editor to use",
|
||||
Aliases: []string{"e"},
|
||||
Action: func(ctx *cli.Context, s string) error {
|
||||
cfg.Editor = ctx.String("editor")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// before validates input, does some setup, and loads config info from file
|
||||
func before(ctx *cli.Context) error {
|
||||
// check input
|
||||
if input_file = strings.Join(ctx.Args().Slice(), " "); input_file == "" {
|
||||
return errors.New("no file to edit, use agedit -h for help")
|
||||
}
|
||||
|
||||
// do some setup
|
||||
cfg = config.Defaults
|
||||
cfg.Editor = env.GetEditor()
|
||||
cfg_dir := env.GetConfigDir("agedit")
|
||||
cfg.IdentityFile = cfg_dir + "identity.key"
|
||||
configFile = cfg_dir + "agedit.yaml"
|
||||
logger = log.NewWithOptions(os.Stderr, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.TimeOnly,
|
||||
})
|
||||
|
||||
// load config from file
|
||||
_, err := os.Open(configFile)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
// or not
|
||||
logger.Debug("couldn't load config file", "file", configFile)
|
||||
} else {
|
||||
err = cleanenv.ReadConfig(configFile, &cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// action does the actual thing
|
||||
func action(ctx *cli.Context) error {
|
||||
if _, err := os.Open(input_file); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if output_file == "" {
|
||||
output_file = input_file
|
||||
logger.Debug("out file not specified, using input", "outfile", output_file)
|
||||
}
|
||||
|
||||
if _, err := os.Open(cfg.IdentityFile); os.IsNotExist(err) {
|
||||
return errors.New("identity file unset, use -i or set one in the config file")
|
||||
}
|
||||
|
||||
if id, err := encrypt.ReadIdentityFromFile(cfg.IdentityFile); err != nil {
|
||||
return err
|
||||
} else {
|
||||
identity = id
|
||||
}
|
||||
logger.Debug("read identity from file", "id", identity.Recipient())
|
||||
|
||||
decrypted, err := encrypt.Decrypt(input_file, identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("decrypted " + input_file + " sucessfully")
|
||||
|
||||
edited, err := editor.EditTempFile(cfg.Editor, string(decrypted), cfg.Prefix, cfg.Suffix, cfg.RandomLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("got data back from editor")
|
||||
|
||||
if string(edited) == string(decrypted) {
|
||||
logger.Warn("No edits made, not writing " + output_file)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = encrypt.Encrypt(edited, output_file, identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("re-encrypted to " + output_file)
|
||||
|
||||
return nil
|
||||
}
|
37
cmd/agedit/main.go
Normal file
37
cmd/agedit/main.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"git.burning.moe/celediel/agedit/internal/config"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
identity *age.X25519Identity
|
||||
logger *log.Logger
|
||||
cfg config.Config
|
||||
configFile string
|
||||
|
||||
input_file, output_file string
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: name,
|
||||
Usage: usage,
|
||||
Version: version,
|
||||
Authors: authors,
|
||||
Flags: flags,
|
||||
Before: before,
|
||||
Action: action,
|
||||
CustomAppHelpTemplate: help_template,
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
29
go.mod
29
go.mod
|
@ -1,3 +1,32 @@
|
|||
module git.burning.moe/celediel/agedit
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
filippo.io/age v1.1.1
|
||||
github.com/charmbracelet/log v0.3.1
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.9.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/crypto v0.4.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||
)
|
||||
|
|
58
go.sum
Normal file
58
go.sum
Normal file
|
@ -0,0 +1,58 @@
|
|||
filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg=
|
||||
filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
|
||||
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
|
||||
github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw=
|
||||
github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
15
internal/config/config.go
Normal file
15
internal/config/config.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
IdentityFile string `json:"identityfile" yaml:"identityfile" toml:"identityfile"`
|
||||
Editor string `json:"editor" yaml:"editor" toml:"editor"`
|
||||
Prefix string `json:"randomfileprefix" yaml:"randomfileprefix" toml:"randomfileprefix"`
|
||||
Suffix string `json:"randomfilesuffix" yaml:"randomfilesuffix" toml:"randomfilesuffix"`
|
||||
RandomLength int `json:"randomfilenamelength" yaml:"randomfilenamelength" toml:"randomfilenamelength"`
|
||||
}
|
||||
|
||||
var Defaults = Config{
|
||||
Prefix: "agedit_",
|
||||
Suffix: ".txt",
|
||||
RandomLength: 13,
|
||||
}
|
70
pkg/editor/editor.go
Normal file
70
pkg/editor/editor.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package editor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"git.burning.moe/celediel/agedit/pkg/tmpfile"
|
||||
)
|
||||
|
||||
// EditFile opens the specified file in the configured editor
|
||||
func EditFile(editor, filename string) error {
|
||||
if editor == "" {
|
||||
return errors.New("editor not set")
|
||||
}
|
||||
|
||||
// TODO: handle editors that require arguments
|
||||
cmd := exec.Command(editor, filename)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EditTempFile creates a temporary file with a random name, opens it in the
|
||||
// editor, and returns the byte slice of its contents.
|
||||
func EditTempFile(editor, start, prefix, suffix string, filename_length int) ([]byte, error) {
|
||||
var (
|
||||
filename string
|
||||
bytes []byte
|
||||
err error
|
||||
file *os.File
|
||||
)
|
||||
|
||||
// generator := tmpfile.NewGenerator("agedit_", ".txt", 13)
|
||||
generator := tmpfile.NewGenerator(prefix, suffix, filename_length)
|
||||
|
||||
filename = generator.GenerateFullPath()
|
||||
if file, err = os.Create(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = os.WriteFile(filename, []byte(start), fs.FileMode(0600)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = EditFile(editor, filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes, err = os.ReadFile(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = file.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = os.Remove(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
98
pkg/encrypt/encrypt.go
Normal file
98
pkg/encrypt/encrypt.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
// Encrypt encrypts bytes into filename
|
||||
func Encrypt(data []byte, filename string, identity *age.X25519Identity) error {
|
||||
var (
|
||||
w io.WriteCloser
|
||||
out = &bytes.Buffer{}
|
||||
err error
|
||||
)
|
||||
|
||||
if identity == nil {
|
||||
return errors.New("nil identity??")
|
||||
}
|
||||
|
||||
if w, err = age.Encrypt(out, identity.Recipient()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.WriteString(w, string(data))
|
||||
if err = w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.Truncate(filename, 0) // in case it exists already
|
||||
if err = os.WriteFile(filename, out.Bytes(), fs.FileMode(0600)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts bytes from filename
|
||||
func Decrypt(filename string, identity *age.X25519Identity) ([]byte, error) {
|
||||
var (
|
||||
f *os.File
|
||||
r io.Reader
|
||||
err error
|
||||
out = &bytes.Buffer{}
|
||||
)
|
||||
if f, err = os.Open(filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r, err = age.Decrypt(f, identity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
// NewIdentity generates a new Age identity
|
||||
func NewIdentity() (*age.X25519Identity, error) {
|
||||
id, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ReadIdentityFromFile reads the identity from the supplied filename
|
||||
func ReadIdentityFromFile(filename string) (*age.X25519Identity, error) {
|
||||
bytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := age.ParseX25519Identity(string(bytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// WriteIdentityToFile writes the supplied identity to the supplied filename
|
||||
func WriteIdentityToFile(id *age.X25519Identity, filename string) error {
|
||||
err := os.WriteFile(filename, []byte(id.String()), fs.FileMode(0600))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
101
pkg/encrypt/encrypt_test.go
Normal file
101
pkg/encrypt/encrypt_test.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package encrypt
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.burning.moe/celediel/agedit/pkg/tmpfile"
|
||||
)
|
||||
|
||||
var generator = tmpfile.NewGenerator("test_", ".txt", 18)
|
||||
|
||||
// TestEncryptionDecryption writes a string to a file, encrypts it, then decrypts it, and reads the string.
|
||||
func TestEncryptionDecryption(t *testing.T) {
|
||||
var (
|
||||
strings_to_write = []string{
|
||||
"hello world",
|
||||
"hola mundo",
|
||||
"مرحبا بالعالم",
|
||||
"こんにちは世界",
|
||||
"你好世界",
|
||||
"Γειά σου Κόσμε",
|
||||
"Привіт Світ",
|
||||
"Բարեւ աշխարհ",
|
||||
"გამარჯობა მსოფლიო",
|
||||
"अभिवादन पृथ्वी",
|
||||
}
|
||||
)
|
||||
|
||||
id, err := NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, str := range strings_to_write {
|
||||
var (
|
||||
outname string = generator.GenerateFullPath()
|
||||
encrypted_outname string = outname + ".age"
|
||||
b []byte
|
||||
err error
|
||||
)
|
||||
|
||||
t.Run("testing writing "+str, func(t *testing.T) {
|
||||
if err = os.WriteFile(outname, []byte(str), fs.FileMode(0600)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b, err = os.ReadFile(outname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = Encrypt(b, encrypted_outname, id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b, err = Decrypt(encrypted_outname, id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(b) != str {
|
||||
t.Fatal(string(b) + " isn't the same as " + str)
|
||||
}
|
||||
|
||||
if err = os.Remove(outname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = os.Remove(encrypted_outname); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewIdentity creats a new identity, writes it to file, then re-reads it back from the file.
|
||||
func TestNewIdentity(t *testing.T) {
|
||||
for range 1000 {
|
||||
outfile := generator.GenerateFullPath()
|
||||
|
||||
identity, err := NewIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = WriteIdentityToFile(identity, outfile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
other_identity, err := ReadIdentityFromFile(outfile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if identity.Recipient().String() != other_identity.Recipient().String() && identity.String() != other_identity.String() {
|
||||
t.Fatal("Identities don't match!", identity.Recipient(), "!=", identity.Recipient())
|
||||
}
|
||||
os.Remove(outfile)
|
||||
}
|
||||
}
|
107
pkg/env/env.go
vendored
Normal file
107
pkg/env/env.go
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetEditor gets the configured editor by checking environmental
|
||||
// variables EDITOR and VISUAL
|
||||
func GetEditor() string {
|
||||
var editor string
|
||||
if os.Getenv("EDITOR") != "" {
|
||||
editor = os.Getenv("EDITOR")
|
||||
} else if os.Getenv("VISUAL") != "" {
|
||||
editor = os.Getenv("VISUAL")
|
||||
} /* else {
|
||||
// TODO: maybe pick something based on the OS
|
||||
} */
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
// GetConfigDir gets a config directory based from environmental variables + the app name
|
||||
//
|
||||
// On Windows, %APPDATA%\agedit is used
|
||||
//
|
||||
// On UNIX-like systems, $XDG_CONFIG_HOME/agedit is tried, if it isn't defined, $HOME/.config/agedit is used
|
||||
func GetConfigDir(appname string) string {
|
||||
var configdir string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
configdir = os.Getenv("APPDATA")
|
||||
default:
|
||||
fallthrough
|
||||
case "darwin":
|
||||
// TODO: figure out the proper Mac OS local directories
|
||||
fallthrough
|
||||
case "linux":
|
||||
if confighome := os.Getenv("XDG_CONFIG_HOME"); confighome != "" {
|
||||
configdir = confighome
|
||||
} else {
|
||||
configdir = make_path(os.Getenv("HOME"), ".config")
|
||||
}
|
||||
}
|
||||
|
||||
return make_path(configdir, appname)
|
||||
}
|
||||
|
||||
// GetConfigDir gets a config directory based from environmental variables + the app name
|
||||
//
|
||||
// On Windows, %LOCALAPPDATA%\agedit is used
|
||||
//
|
||||
// On UNIX-like systems, $XDG_DATA_HOME/agedit is tried, if it isn't defined, $HOME/.local/share/agedit is used
|
||||
func GetDataDir(appname string) string {
|
||||
var datadir string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
datadir = os.Getenv("LOCALAPPDATA")
|
||||
default:
|
||||
fallthrough
|
||||
case "darwin":
|
||||
// TODO: also here
|
||||
fallthrough
|
||||
case "linux":
|
||||
if datahome := os.Getenv("XDG_DATA_HOME"); datahome != "" {
|
||||
datadir = datahome
|
||||
} else {
|
||||
datadir = make_path(os.Getenv("HOME"), "local", "share")
|
||||
}
|
||||
}
|
||||
|
||||
return make_path(datadir, appname)
|
||||
}
|
||||
|
||||
// GetTempDirectory returns the systems temporary directory
|
||||
//
|
||||
// returns %TEMP% on Windows, /tmp on UNIX-like systems
|
||||
func GetTempDirectory() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return os.Getenv("TEMP")
|
||||
default:
|
||||
fallthrough
|
||||
case "darwin":
|
||||
fallthrough
|
||||
case "linux":
|
||||
return "/tmp"
|
||||
}
|
||||
}
|
||||
|
||||
func make_path(paths ...string) string {
|
||||
sep := string(os.PathSeparator)
|
||||
output := strings.Builder{}
|
||||
|
||||
// add / to the start if it's not already there and we're not on Windows
|
||||
if match, err := regexp.Match("^\\w", []byte(paths[0])); err == nil && match && runtime.GOOS != "windows" {
|
||||
output.WriteString(sep)
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
output.WriteString(path + sep)
|
||||
}
|
||||
|
||||
return output.String()
|
||||
}
|
30
pkg/env/env_test.go
vendored
Normal file
30
pkg/env/env_test.go
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
editors = []string{"hx", "nano", "vi", "vim", "nvim", "micro", "emacs", "ed"}
|
||||
)
|
||||
|
||||
func clearEnvForNow() {
|
||||
for _, item := range []string{"EDITOR", "VISUAL"} {
|
||||
os.Setenv(item, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorFromEnv(t *testing.T) {
|
||||
for _, item := range []string{"EDITOR", "VISUAL"} {
|
||||
clearEnvForNow()
|
||||
for _, editor := range editors {
|
||||
if err := os.Setenv(item, editor); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := GetEditor(); got != editor {
|
||||
t.Fatal("got", got, "but wanted", editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
pkg/tmpfile/tmpfile.go
Normal file
47
pkg/tmpfile/tmpfile.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package tmpfile
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.burning.moe/celediel/agedit/pkg/env"
|
||||
)
|
||||
|
||||
const chars string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
type Generator struct {
|
||||
Prefix, Suffix string
|
||||
Length int
|
||||
}
|
||||
|
||||
// GenerateName generates a random temporary filename like agedit_geef0XYC30RGV
|
||||
func (g *Generator) GenerateName() string {
|
||||
return g.Prefix + randomString(chars, g.Length) + g.Suffix
|
||||
}
|
||||
|
||||
// GenerateFullPath generates a random temporary filename and appends it to the OS's temporary directory
|
||||
func (g *Generator) GenerateFullPath() string {
|
||||
return env.GetTempDirectory() + string(os.PathSeparator) + g.GenerateName()
|
||||
}
|
||||
|
||||
// NewGenerator returns a new Generator
|
||||
func NewGenerator(prefix, suffix string, length int) Generator {
|
||||
return Generator{
|
||||
Prefix: prefix,
|
||||
Suffix: suffix,
|
||||
Length: length,
|
||||
}
|
||||
}
|
||||
|
||||
func randomString(set string, length int) string {
|
||||
out := strings.Builder{}
|
||||
for i := 0; i < length; i++ {
|
||||
out.WriteByte(randomChar(set))
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func randomChar(set string) byte {
|
||||
return set[rand.Intn(len(set))]
|
||||
}
|
43
pkg/tmpfile/tmpfile_test.go
Normal file
43
pkg/tmpfile/tmpfile_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package tmpfile
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var generator = NewGenerator("test_", ".txt", 18)
|
||||
|
||||
// TestCanCreateTmpFile tests if temporary files can be created and removed successfully
|
||||
func TestCanCreateTmpFile(t *testing.T) {
|
||||
b := []byte{104, 101, 108, 108, 111, 32, 116, 104, 101, 114, 101}
|
||||
|
||||
for range 1000 {
|
||||
outfile := generator.GenerateFullPath()
|
||||
err := os.WriteFile(outfile, b, fs.FileMode(0600))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err = os.Stat(outfile); err != nil && os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = os.Remove(outfile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUniqueTmpFile generates a large number of random names to make sure they're all unique
|
||||
func TestUniqueTmpFile(t *testing.T) {
|
||||
var generated_names = map[string]string{}
|
||||
|
||||
for range 100000 {
|
||||
name := generator.GenerateName()
|
||||
if val, ok := generated_names[name]; ok {
|
||||
t.Fatal("Non unique name", val)
|
||||
}
|
||||
generated_names[name] = name
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue