From 43ab78867da176dfed7d929bf2cb06d1328dc67a Mon Sep 17 00:00:00 2001 From: DTLP Date: Thu, 21 Sep 2023 16:56:41 +0100 Subject: [PATCH] Add lib functionality --- .gitignore | 2 +- cmd/strongbox/main.go | 153 +++++++++++++ example/go.mod | 10 + example/go.sum | 26 +++ example/main.go | 34 +++ go.mod | 18 +- go.sum | 22 +- integration_tests/Dockerfile | 2 +- integration_tests/run.sh | 4 +- keyring.go => strongbox/keyring.go | 32 +-- strongbox.go => strongbox/strongbox.go | 203 ++++-------------- .../strongbox_test.go | 37 ++-- 12 files changed, 334 insertions(+), 209 deletions(-) create mode 100644 cmd/strongbox/main.go create mode 100644 example/go.mod create mode 100644 example/go.sum create mode 100644 example/main.go rename keyring.go => strongbox/keyring.go (65%) rename strongbox.go => strongbox/strongbox.go (62%) rename strongbox_test.go => strongbox/strongbox_test.go (61%) diff --git a/.gitignore b/.gitignore index 5146142..60456be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .docs docs/_index.md docs/README.md -strongbox +bin diff --git a/cmd/strongbox/main.go b/cmd/strongbox/main.go new file mode 100644 index 0000000..87baa37 --- /dev/null +++ b/cmd/strongbox/main.go @@ -0,0 +1,153 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "log" + "os" + "path/filepath" + + "github.com/uw-labs/strongbox/strongbox" +) + +var ( + version = "dev" + commit = "none" + date = "unknown" + builtBy = "unknown" + + keyLoader = strongbox.Key + kr *strongbox.FileKeyRing + prefix = []byte("# STRONGBOX ENCRYPTED RESOURCE ;") + defaultPrefix = []byte("# STRONGBOX ENCRYPTED RESOURCE ; See https://github.com/uw-labs/strongbox\n") + + errKeyNotFound = errors.New("key not found") + + // flags + flagGitConfig = flag.Bool("git-config", false, "Configure git for strongbox use") + flagGenKey = flag.String("gen-key", "", "Generate a new key and add it to your strongbox keyring") + flagDecrypt = flag.Bool("decrypt", false, "Decrypt single resource") + flagKey = flag.String("key", "", "Private key to use to decrypt") + flagKeyRing = flag.String("keyring", "", "strongbox keyring file path, if not set default '$HOME/.strongbox_keyring' will be used") + flagRecursive = flag.Bool("recursive", false, "Recursively decrypt all files under given folder, must be used with -decrypt flag") + + flagClean = flag.String("clean", "", "intended to be called internally by git") + flagSmudge = flag.String("smudge", "", "intended to be called internally by git") + flagDiff = flag.String("diff", "", "intended to be called internally by git") + + flagVersion = flag.Bool("version", false, "Strongbox version") +) + +func usage() { + fmt.Fprintf(os.Stderr, "Usage:\n\n") + fmt.Fprintf(os.Stderr, "\tstrongbox -git-config\n") + fmt.Fprintf(os.Stderr, "\tstrongbox [-keyring ] -gen-key key-name\n") + fmt.Fprintf(os.Stderr, "\tstrongbox [-keyring ] -decrypt -recursive \n") + fmt.Fprintf(os.Stderr, "\tstrongbox -decrypt -recursive -key \n") + fmt.Fprintf(os.Stderr, "\tstrongbox -decrypt -key \n") + fmt.Fprintf(os.Stderr, "\tstrongbox -version\n") + fmt.Fprintf(os.Stderr, "\nif -keyring flag is not set default file '$HOME/.strongbox_keyring' or '$STRONGBOX_HOME/.strongbox_keyring' will be used as keyring\n") + os.Exit(2) +} + +func main() { + log.SetPrefix("strongbox: ") + log.SetFlags(log.LstdFlags | log.Lshortfile) + + flag.Usage = usage + flag.Parse() + + if *flagVersion || (flag.NArg() == 1 && flag.Arg(0) == "version") { + fmt.Printf("version=%s commit=%s date=%s builtBy=%s\n", version, commit, date, builtBy) + return + } + + if *flagGitConfig { + strongbox.GitConfig() + return + } + + if *flagDiff != "" { + err := strongbox.Diff(*flagDiff) + if err != nil { + log.Fatalf("failed to diff: %v", err) + } + return + } + + // Set up keyring file name + home, err := strongbox.DeriveHome() + if err != nil { + log.Fatalf("failed to set up keyring file name: %v", err) + } + kr = strongbox.New(filepath.Join(home, ".strongbox_keyring")) + + // if keyring flag is set replace default keyRing + if *flagKeyRing != "" { + kr = strongbox.New(*flagKeyRing) + // verify keyring is valid + // if err := kr.Load(); err != nil { + // log.Fatalf("unable to load keyring file:%s err:%s", *flagKeyRing, err) + // } + // TODO: verify the keyring file //////////////////////////////////////// + } + + if *flagGenKey != "" { + err := strongbox.GenKey(kr, *flagGenKey) + if err != nil { + log.Fatalf("failed to generate key: %v", err) + } + return + } + + if *flagDecrypt { + // handle recursive + if *flagRecursive { + var err error + + target := flag.Arg(0) + if target == "" { + target, err = os.Getwd() + if err != nil { + log.Fatalf("target path not provided and unable to get cwd err:%s", err) + } + } + // for recursive decryption 'key' flag is optional but if provided + // it should be valid and all encrypted file will be decrypted using it + dk, err := strongbox.Decode([]byte(*flagKey)) + if err != nil && *flagKey != "" { + log.Fatalf("failed to decode given private key %v", err) + } + + if err = strongbox.RecursiveDecrypt(kr, target, dk); err != nil { + log.Fatalln(err) + } + return + } + + if *flagKey == "" { + log.Fatalf("Must provide a `-key` when using -decrypt") + } + err := strongbox.DecryptCLI(*flagKey) + if err != nil { + log.Fatalf("failed to decrypt: %v", err) + } + return + } + + if *flagRecursive { + log.Println("-recursive flag is only supported with -decrypt") + usage() + } + + if *flagClean != "" { + strongbox.Clean(kr, os.Stdin, os.Stdout, *flagClean) + return + } + if *flagSmudge != "" { + strongbox.Smudge(kr, os.Stdin, os.Stdout, *flagSmudge) + return + } +} + diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 0000000..70954fc --- /dev/null +++ b/example/go.mod @@ -0,0 +1,10 @@ +module main.go + +go 1.19 + +require github.com/uw-labs/strongbox/internal/strongbox v1.1.0 + +require ( + github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/example/go.sum b/example/go.sum new file mode 100644 index 0000000..64d4324 --- /dev/null +++ b/example/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 h1:YuDUUFNM21CAbyPOpOP8BicaTD/0klJEKt5p8yuw+uY= +github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115/go.mod h1:LadVJg0XuawGk+8L1rYnIED8451UyNxEMdTWCEt5kmU= +github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA= +github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd/go.mod h1:TlmyIZDpGmwRoTWiakdr+HA1Tukze6C6XbRVidYq02M= +github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff h1:2xRHTvkpJ5zJmglXLRqHiZQNjUoOkhUyhTAhEQvPAWw= +github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff/go.mod h1:gJWba/XXGl0UoOmBQKRWCJdHrr3nE0T65t6ioaj3mLI= +github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 h1:BMb8s3ENQLt5ulwVIHVDWFHp8eIXmbfSExkvdn9qMXI= +github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11/go.mod h1:+DBdDyfoO2McrOyDemRBq0q9CMEByef7sYl7JH5Q3BI= +github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb h1:uSWBjJdMf47kQlXMwWEfmc864bA1wAC+Kl3ApryuG9Y= +github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb/go.mod h1:ivcmUvxXWjb27NsPEaiYK7AidlZXS7oQ5PowUS9z3I4= +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/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/uw-labs/strongbox v1.1.0 h1:gIFhB+YFkY4wbD6ZU4/nZI26d1O6/TnSPg2ADJTV8Z4= +github.com/uw-labs/strongbox v1.1.0/go.mod h1:MeDTE5Nj3SAPmhZXuqju0KcZWJW3D1HPmU14buyWgqU= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..dd9dd97 --- /dev/null +++ b/example/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + "os" + + "github.com/uw-labs/strongbox/strongbox" +) + +var ( + keyPath = "" // Path to strongbox key + targetPath = "" // Path to encrypted file or directory containing encrypted files +) + +func main() { + key, err := os.ReadFile(keyPath) + if err != nil { + log.Fatalf("failed to read key file: %v\n", err) + return + } + + keyBytes := []byte(key) // Convert key string into byte slice + + // Decode the key + dk, err := strongbox.Decode([]byte(keyBytes)) + if err != nil { + log.Fatalf("failed to decode given private key %v", err) + } + + // Decrypt file(s) at the path provided + if err := strongbox.RecursiveDecrypt(nil, targetPath, dk); err != nil { + log.Fatalln(err) + } +} diff --git a/go.mod b/go.mod index 88215af..8831512 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,22 @@ module github.com/uw-labs/strongbox -go 1.21 +go 1.19 require ( github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 - github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd // indirect - github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff // indirect - github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 // indirect - github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb // indirect github.com/stretchr/testify v1.7.0 - golang.org/x/net v0.7.0 // indirect gopkg.in/yaml.v2 v2.4.0 ) require ( - github.com/davecgh/go-spew v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd // indirect + github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff // indirect + github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 // indirect + github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.0 // indirect + golang.org/x/net v0.15.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7ba9094..63171fa 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 h1:YuDUUFNM21CAbyPOpOP8BicaTD/0klJEKt5p8yuw+uY= github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115/go.mod h1:LadVJg0XuawGk+8L1rYnIED8451UyNxEMdTWCEt5kmU= github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA= @@ -10,17 +12,25 @@ github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 h1:BMb8s3ENQLt5ul github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11/go.mod h1:+DBdDyfoO2McrOyDemRBq0q9CMEByef7sYl7JH5Q3BI= github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb h1:uSWBjJdMf47kQlXMwWEfmc864bA1wAC+Kl3ApryuG9Y= github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb/go.mod h1:ivcmUvxXWjb27NsPEaiYK7AidlZXS7oQ5PowUS9z3I4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration_tests/Dockerfile b/integration_tests/Dockerfile index 45e4c76..a822e22 100644 --- a/integration_tests/Dockerfile +++ b/integration_tests/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19-alpine +FROM golang:1.21-alpine RUN apk --no-cache add git diff --git a/integration_tests/run.sh b/integration_tests/run.sh index bc8a2db..0caa3b1 100755 --- a/integration_tests/run.sh +++ b/integration_tests/run.sh @@ -2,8 +2,8 @@ PATH=$PATH:$GOPATH/bin -go get -t . -go install +go get -t ./cmd/strongbox/ +go install ./cmd/strongbox/ go get -t ./integration_tests/ go test -v -tags=integration ./integration_tests/ diff --git a/keyring.go b/strongbox/keyring.go similarity index 65% rename from keyring.go rename to strongbox/keyring.go index a1df914..89c4e8a 100644 --- a/keyring.go +++ b/strongbox/keyring.go @@ -1,4 +1,4 @@ -package main +package strongbox import ( "fmt" @@ -10,13 +10,13 @@ import ( ) type keyRing interface { - Load() error - Save() error - AddKey(name string, keyID []byte, key []byte) - Key(keyID []byte) ([]byte, error) + load() error + save() error + addKey(name string, keyID []byte, key []byte) + key(keyID []byte) ([]byte, error) } -type fileKeyRing struct { +type FileKeyRing struct { fileName string KeyEntries []keyEntry } @@ -27,20 +27,24 @@ type keyEntry struct { Key string `yaml:"key"` } -func (kr *fileKeyRing) AddKey(desc string, keyID []byte, key []byte) { +func New(path string) (*FileKeyRing) { + return &FileKeyRing{fileName : path} +} + +func (kr *FileKeyRing) addKey(desc string, keyID []byte, key []byte) { kr.KeyEntries = append(kr.KeyEntries, keyEntry{ Description: desc, - KeyID: string(encode(keyID[:])), - Key: string(encode(key[:])), + KeyID: string(Encode(keyID[:])), + Key: string(Encode(key[:])), }) } -func (kr *fileKeyRing) Key(keyID []byte) ([]byte, error) { - b64 := string(encode(keyID[:])) +func (kr *FileKeyRing) key(keyID []byte) ([]byte, error) { + b64 := string(Encode(keyID[:])) for _, ke := range kr.KeyEntries { if ke.KeyID == b64 { - dec, err := decode([]byte(ke.Key)) + dec, err := Decode([]byte(ke.Key)) if err != nil { return []byte{}, err } @@ -54,7 +58,7 @@ func (kr *fileKeyRing) Key(keyID []byte) ([]byte, error) { return []byte{}, errKeyNotFound } -func (kr *fileKeyRing) Load() error { +func (kr *FileKeyRing) load() error { bytes, err := os.ReadFile(kr.fileName) if err != nil { @@ -65,7 +69,7 @@ func (kr *fileKeyRing) Load() error { return err } -func (kr *fileKeyRing) Save() error { +func (kr *FileKeyRing) save() error { ser, err := yaml.Marshal(kr) if err != nil { log.Fatal(err) diff --git a/strongbox.go b/strongbox/strongbox.go similarity index 62% rename from strongbox.go rename to strongbox/strongbox.go index dfbdc1a..25eb52e 100644 --- a/strongbox.go +++ b/strongbox/strongbox.go @@ -1,4 +1,4 @@ -package main +package strongbox import ( "bytes" @@ -22,153 +22,33 @@ import ( ) var ( - version = "dev" - commit = "none" - date = "unknown" - builtBy = "unknown" - - keyLoader = key - kr keyRing + keyLoader = Key prefix = []byte("# STRONGBOX ENCRYPTED RESOURCE ;") defaultPrefix = []byte("# STRONGBOX ENCRYPTED RESOURCE ; See https://github.com/uw-labs/strongbox\n") errKeyNotFound = errors.New("key not found") - - // flags - flagGitConfig = flag.Bool("git-config", false, "Configure git for strongbox use") - flagGenKey = flag.String("gen-key", "", "Generate a new key and add it to your strongbox keyring") - flagDecrypt = flag.Bool("decrypt", false, "Decrypt single resource") - flagKey = flag.String("key", "", "Private key to use to decrypt") - flagKeyRing = flag.String("keyring", "", "strongbox keyring file path, if not set default '$HOME/.strongbox_keyring' will be used") - flagRecursive = flag.Bool("recursive", false, "Recursively decrypt all files under given folder, must be used with -decrypt flag") - - flagClean = flag.String("clean", "", "intended to be called internally by git") - flagSmudge = flag.String("smudge", "", "intended to be called internally by git") - flagDiff = flag.String("diff", "", "intended to be called internally by git") - - flagVersion = flag.Bool("version", false, "Strongbox version") ) -func usage() { - fmt.Fprintf(os.Stderr, "Usage:\n\n") - fmt.Fprintf(os.Stderr, "\tstrongbox -git-config\n") - fmt.Fprintf(os.Stderr, "\tstrongbox [-keyring ] -gen-key key-name\n") - fmt.Fprintf(os.Stderr, "\tstrongbox [-keyring ] -decrypt -recursive \n") - fmt.Fprintf(os.Stderr, "\tstrongbox -decrypt -recursive -key \n") - fmt.Fprintf(os.Stderr, "\tstrongbox -decrypt -key \n") - fmt.Fprintf(os.Stderr, "\tstrongbox -version\n") - fmt.Fprintf(os.Stderr, "\nif -keyring flag is not set default file '$HOME/.strongbox_keyring' or '$STRONGBOX_HOME/.strongbox_keyring' will be used as keyring\n") - os.Exit(2) -} - -func main() { - log.SetPrefix("strongbox: ") - log.SetFlags(log.LstdFlags | log.Lshortfile) - - flag.Usage = usage - flag.Parse() - - if *flagVersion || (flag.NArg() == 1 && flag.Arg(0) == "version") { - fmt.Printf("version=%s commit=%s date=%s builtBy=%s\n", version, commit, date, builtBy) - return - } - - if *flagGitConfig { - gitConfig() - return - } - - if *flagDiff != "" { - diff(*flagDiff) - return - } - - // Set up keyring file name - home := deriveHome() - kr = &fileKeyRing{fileName: filepath.Join(home, ".strongbox_keyring")} - - // if keyring flag is set replace default keyRing - if *flagKeyRing != "" { - kr = &fileKeyRing{fileName: *flagKeyRing} - // verify keyring is valid - if err := kr.Load(); err != nil { - log.Fatalf("unable to load keyring file:%s err:%s", *flagKeyRing, err) - } - } - - if *flagGenKey != "" { - genKey(*flagGenKey) - return - } - - if *flagDecrypt { - // handle recursive - if *flagRecursive { - var err error - - target := flag.Arg(0) - if target == "" { - target, err = os.Getwd() - if err != nil { - log.Fatalf("target path not provided and unable to get cwd err:%s", err) - } - } - // for recursive decryption 'key' flag is optional but if provided - // it should be valid and all encrypted file will be decrypted using it - dk, err := decode([]byte(*flagKey)) - if err != nil && *flagKey != "" { - log.Fatalf("Unable to decode given private key %v", err) - } - - if err = recursiveDecrypt(target, dk); err != nil { - log.Fatalln(err) - } - return - } - - if *flagKey == "" { - log.Fatalf("Must provide a `-key` when using -decrypt") - } - decryptCLI() - return - } - - if *flagRecursive { - log.Println("-recursive flag is only supported with -decrypt") - usage() - } - - if *flagClean != "" { - clean(os.Stdin, os.Stdout, *flagClean) - return - } - if *flagSmudge != "" { - smudge(os.Stdin, os.Stdout, *flagSmudge) - return - } -} - -func deriveHome() string { +func DeriveHome() (string, error) { // try explicitly set STRONGBOX_HOME if home := os.Getenv("STRONGBOX_HOME"); home != "" { - return home + return home, nil } // Try user.Current which works in most cases, but may not work with CGO disabled. u, err := user.Current() if err == nil && u.HomeDir != "" { - return u.HomeDir + return u.HomeDir, nil } // try HOME env var if home := os.Getenv("HOME"); home != "" { - return home + return home, nil } - log.Fatal("Could not call os/user.Current() or find $STRONGBOX_HOME or $HOME. Please recompile with CGO enabled or set $STRONGBOX_HOME or $HOME") // not reached - return "" + return "", fmt.Errorf("Could not call os/user.Current() or find $STRONGBOX_HOME or $HOME. Please recompile with CGO enabled or set $STRONGBOX_HOME or $HOME") } -func decryptCLI() { +func DecryptCLI(flagKey string) error { var fn string if flag.Arg(0) == "" { // no file passed, try to read stdin @@ -178,20 +58,21 @@ func decryptCLI() { } fb, err := os.ReadFile(fn) if err != nil { - log.Fatalf("Unable to read file to decrypt %v", err) + return fmt.Errorf("Unable to read file to decrypt %v", err) } - dk, err := decode([]byte(*flagKey)) + dk, err := Decode([]byte(flagKey)) if err != nil { - log.Fatalf("Unable to decode private key %v", err) + return fmt.Errorf("Unable to decode private key %v", err) } out, err := decrypt(fb, dk) if err != nil { - log.Fatalf("Unable to decrypt %v", err) + return fmt.Errorf("Unable to decrypt %v", err) } fmt.Printf("%s", out) + return nil } -func gitConfig() { +func GitConfig() { args := [][]string{ {"config", "--global", "--replace-all", "filter.strongbox.clean", "strongbox -clean %f"}, {"config", "--global", "--replace-all", "filter.strongbox.smudge", "strongbox -smudge %f"}, @@ -208,50 +89,56 @@ func gitConfig() { log.Println("git global configuration updated successfully") } -func genKey(desc string) { - err := kr.Load() +func GenKey(kr keyRing, desc string) error { + + err := kr.load() if err != nil && !os.IsNotExist(err) { - log.Fatal(err) + return fmt.Errorf("unable to load key ring file: %v", err) } key := make([]byte, 32) _, err = rand.Read(key) if err != nil { - log.Fatal(err) + return fmt.Errorf("unable to read key ring file: %v", err) } keyID := sha256.Sum256(key) - kr.AddKey(desc, keyID[:], key) + kr.addKey(desc, keyID[:], key) - err = kr.Save() + err = kr.save() if err != nil { - log.Fatal(err) + return fmt.Errorf("unable to save key ring file: %v", err) } + + return nil } -func diff(filename string) { +func Diff(filename string) error { f, err := os.Open(filename) if err != nil { - log.Fatal(err) + return fmt.Errorf("unable to open file: %v", err) } defer func() { if err = f.Close(); err != nil { - log.Fatal(err) + err = fmt.Errorf("unable to close file") } }() _, err = io.Copy(os.Stdout, f) if err != nil { - log.Fatal(err) + return fmt.Errorf("unable to copy file: %v", err) } + + return nil } -func clean(r io.Reader, w io.Writer, filename string) { +func Clean(kr keyRing,r io.Reader, w io.Writer, filename string) { // Read the file, fail on error in, err := io.ReadAll(r) if err != nil { log.Fatal(err) } + // Check the file is plaintext, if its an encrypted strongbox file, copy as is, and exit 0 if bytes.HasPrefix(in, prefix) { _, err = io.Copy(w, bytes.NewReader(in)) @@ -261,7 +148,7 @@ func clean(r io.Reader, w io.Writer, filename string) { return } // File is plaintext and needs to be encrypted, get the key, fail on error - key, err := keyLoader(filename) + key, err := keyLoader(kr, filename) if err != nil { log.Fatal(err) } @@ -278,7 +165,7 @@ func clean(r io.Reader, w io.Writer, filename string) { } // Called by git on `git checkout` -func smudge(r io.Reader, w io.Writer, filename string) { +func Smudge(kr keyRing, r io.Reader, w io.Writer, filename string) { in, err := io.ReadAll(r) if err != nil { log.Fatal(err) @@ -293,7 +180,7 @@ func smudge(r io.Reader, w io.Writer, filename string) { return } - key, err := keyLoader(filename) + key, err := keyLoader(kr, filename) if err != nil { // don't log error if its keyNotFound switch err { @@ -323,7 +210,7 @@ func smudge(r io.Reader, w io.Writer, filename string) { // otherwise it will find key based on file location // if error is generated in finding key or in decryption then it will continue with next file // function will only return early if it failed to read/write files -func recursiveDecrypt(target string, givenKey []byte) error { +func RecursiveDecrypt(kr keyRing, target string, givenKey []byte) error { var decErrors []string err := filepath.WalkDir(target, func(path string, entry fs.DirEntry, err error) error { // always return on error @@ -359,7 +246,7 @@ func recursiveDecrypt(target string, givenKey []byte) error { key := givenKey if len(key) == 0 { - key, err = keyLoader(path) + key, err = keyLoader(kr, path) if err != nil { // continue with next file decErrors = append(decErrors, fmt.Sprintf("unable to find key file:%s err:%s", path, err)) @@ -413,7 +300,7 @@ func encrypt(b, key []byte) ([]byte, error) { } var buf []byte buf = append(buf, defaultPrefix...) - b64 := encode(out) + b64 := Encode(out) for len(b64) > 0 { l := 76 if len(b64) < 76 { @@ -454,13 +341,13 @@ func decompress(b []byte) []byte { return b } -func encode(decoded []byte) []byte { +func Encode(decoded []byte) []byte { b64 := make([]byte, base64.StdEncoding.EncodedLen(len(decoded))) base64.StdEncoding.Encode(b64, decoded) return b64 } -func decode(encoded []byte) ([]byte, error) { +func Decode(encoded []byte) ([]byte, error) { decoded := make([]byte, len(encoded)) i, err := base64.StdEncoding.Decode(decoded, encoded) if err != nil { @@ -476,7 +363,7 @@ func decrypt(enc []byte, priv []byte) ([]byte, error) { return nil, errors.New("couldn't split on end of line") } b64encoded := spl[1] - b64decoded, err := decode(b64encoded) + b64decoded, err := Decode(b64encoded) if err != nil { return nil, err } @@ -489,18 +376,18 @@ func decrypt(enc []byte, priv []byte) ([]byte, error) { } // key returns private key and error -func key(filename string) ([]byte, error) { +func Key(kr keyRing, filename string) ([]byte, error) { keyID, err := findKey(filename) if err != nil { return []byte{}, err } - err = kr.Load() + err = kr.load() if err != nil { return []byte{}, err } - key, err := kr.Key(keyID) + key, err := kr.key(keyID) if err != nil { return []byte{}, err } @@ -532,7 +419,7 @@ func readKey(filename string) ([]byte, error) { } b64 := strings.TrimSpace(string(fp)) - b, err := decode([]byte(b64)) + b, err := Decode([]byte(b64)) if err != nil { return []byte{}, err } diff --git a/strongbox_test.go b/strongbox/strongbox_test.go similarity index 61% rename from strongbox_test.go rename to strongbox/strongbox_test.go index 12bd854..40efda1 100644 --- a/strongbox_test.go +++ b/strongbox/strongbox_test.go @@ -1,10 +1,9 @@ -package main +package strongbox import ( "bytes" "crypto/rand" "crypto/sha256" - "fmt" "os" "testing" @@ -12,24 +11,26 @@ import ( ) var ( + // keyLoader = TestKeyLoader + testKR keyRing priv, pub []byte plain = []byte("hello world. this is some plain text for testing") ) type mockKeyRing struct{} -func (m *mockKeyRing) Load() error { +func (m *mockKeyRing) load() error { return nil } -func (m *mockKeyRing) Save() error { +func (m *mockKeyRing) save() error { return nil } -func (m *mockKeyRing) AddKey(name string, keyID []byte, key []byte) { +func (m *mockKeyRing) addKey(name string, keyID []byte, key []byte) { } -func (m *mockKeyRing) Key(keyID []byte) ([]byte, error) { +func (m *mockKeyRing) key(keyID []byte) ([]byte, error) { return priv, nil } @@ -46,12 +47,12 @@ func TestMain(m *testing.M) { pub = keyID[:] keyLoader = testKeyLoader - kr = &mockKeyRing{} + testKR = &mockKeyRing{} os.Exit(m.Run()) } -func testKeyLoader(string) ([]byte, error) { +func testKeyLoader(keyRing, string) ([]byte, error) { return priv, nil } @@ -59,10 +60,10 @@ func TestMultipleClean(t *testing.T) { assert := assert.New(t) var cleaned bytes.Buffer - clean(bytes.NewReader(plain), &cleaned, "") + Clean(testKR, bytes.NewReader(plain), &cleaned, "") var doubleCleaned bytes.Buffer - clean(bytes.NewReader(cleaned.Bytes()), &doubleCleaned, "") + Clean(testKR, bytes.NewReader(cleaned.Bytes()), &doubleCleaned, "") assert.Equal(cleaned.String(), doubleCleaned.String()) } @@ -71,7 +72,7 @@ func TestSmudgeAlreadyPlaintext(t *testing.T) { assert := assert.New(t) var smudged bytes.Buffer - smudge(bytes.NewReader(plain), &smudged, "") + Smudge(testKR, bytes.NewReader(plain), &smudged, "") assert.Equal(string(plain), smudged.String()) } @@ -80,14 +81,12 @@ func TestRoundTrip(t *testing.T) { assert := assert.New(t) var cleaned bytes.Buffer - clean(bytes.NewReader(plain), &cleaned, "") - - fmt.Printf("%s", string(cleaned.String())) + Clean(testKR, bytes.NewReader(plain), &cleaned, "") assert.NotEqual(plain, cleaned.Bytes()) var smudged bytes.Buffer - smudge(bytes.NewReader(cleaned.Bytes()), &smudged, "") + Smudge(testKR, bytes.NewReader(cleaned.Bytes()), &smudged, "") assert.Equal(string(plain), smudged.String()) } @@ -96,10 +95,10 @@ func TestDeterministic(t *testing.T) { assert := assert.New(t) var cleaned1 bytes.Buffer - clean(bytes.NewReader(plain), &cleaned1, "") + Clean(testKR, bytes.NewReader(plain), &cleaned1, "") var cleaned2 bytes.Buffer - clean(bytes.NewReader(plain), &cleaned2, "") + Clean(testKR, bytes.NewReader(plain), &cleaned2, "") assert.Equal(cleaned1.String(), cleaned2.String()) } @@ -107,9 +106,9 @@ func TestDeterministic(t *testing.T) { func BenchmarkRoundTripPlain(b *testing.B) { for n := 0; n < b.N; n++ { var cleaned bytes.Buffer - clean(bytes.NewReader(plain), &cleaned, "") + Clean(testKR, bytes.NewReader(plain), &cleaned, "") var smudged bytes.Buffer - smudge(bytes.NewReader(cleaned.Bytes()), &smudged, "") + Smudge(testKR, bytes.NewReader(cleaned.Bytes()), &smudged, "") } }