From 6ae96090c046c30b51910e9531521802e5f9162d Mon Sep 17 00:00:00 2001 From: Lasse Gaardsholt Date: Tue, 26 Oct 2021 22:25:11 +0200 Subject: [PATCH] Multiple datastores (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * in-memory works - still working on redis Signed-off-by: Gaardsholt * redis seems to work now Signed-off-by: Gaardsholt * first mock of redis and config test (#1) * changed so that the `SecretStore` will only handle storing the data and not encrypt/decrypt the data Signed-off-by: Gaardsholt * go mod updates Signed-off-by: Gaardsholt * Create CODEOWNERS * I have most likely forgotten something Signed-off-by: Gaardsholt * Multiple datastores (#2) * first mock of redis and config test * added random to hash id to prevent duplicate IDs, even though it might only be theorectical possible * a bit of linting and a bit of error handling * making sure metrics dont block the functionality * added another redis test * added better error handling to crypto package * added test to crypto package Co-authored-by: Lasse Gaardsholt * fixing tests Signed-off-by: Gaardsholt * fixing codeowners * updated readme Signed-off-by: Gaardsholt * Never gonna give, never gonna give Signed-off-by: Gaardsholt * hoo boy Signed-off-by: Gaardsholt Co-authored-by: Peter Brøndum <34370407+brondum@users.noreply.github.com> --- .github/CODEOWNERS | 1 + .gitignore | 43 +++++++ .vscode/launch.json | 5 +- README.md | 31 ++++- api/api.go | 249 +++++++++++++++++++++++++++++++++++++++++ config/config.go | 39 ++++++- config/config_test.go | 35 ++++++ crypto/crypto.go | 24 ++++ crypto/decrypt.go | 31 +++++ crypto/decrypt_test.go | 34 ++++++ crypto/encrypt.go | 34 ++++++ crypto/encrypt_test.go | 22 ++++ crypto/hash.go | 13 +++ datastore/datastore.go | 8 ++ go.mod | 7 ++ go.sum | 20 ++++ main.go | 157 +------------------------- memory/memory.go | 69 ++++++++++++ redis/redis.go | 99 ++++++++++++++++ redis/redis_test.go | 90 +++++++++++++++ scripts/test.sh | 25 +++++ templates/base.html | 4 +- templates/read.html | 2 +- types/secrets.go | 143 ++++------------------- 24 files changed, 898 insertions(+), 287 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .gitignore create mode 100644 api/api.go create mode 100644 config/config_test.go create mode 100644 crypto/crypto.go create mode 100644 crypto/decrypt.go create mode 100644 crypto/decrypt_test.go create mode 100644 crypto/encrypt.go create mode 100644 crypto/encrypt_test.go create mode 100644 crypto/hash.go create mode 100644 datastore/datastore.go create mode 100644 memory/memory.go create mode 100644 redis/redis.go create mode 100644 redis/redis_test.go create mode 100755 scripts/test.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..31ef7f4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Gaardsholt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e34fe7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,go +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,go + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +pass-along + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,go \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 5b0a9f4..d5da9ef 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,10 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/main.go", - "args": [] + "args": [], + "env": { + "DATABASETYPE": "redis" + } } ] } \ No newline at end of file diff --git a/README.md b/README.md index 9655b52..d58dc84 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,34 @@ +> :warning: Very much work in progress ! # Pass-along -> :warning: Very much work in progress ! +The main application uses port `8080`. + + `/healthz` and `/metrics` endpoints uses port `8888`. + + +## Server config + +The following config can be set via environment variables +| Tables | Required | Default | +| ----------------------------- | :------: | --------- | +| [SERVERSALT](#SERVERSALT) | | | +| [DATABASETYPE](#DATABASETYPE) | | in-memory | +| [REDISSERVER](#REDISSERVER) | | localhost | +| [REDISPORT](#REDISPORT) | | 6379 | + + +### SERVERSALT +For extra security you can add your own salt when encrypting the data. + +### DATABASETYPE +Can either be `in-memory` or `redis`. + +### REDISSERVER +Address to your redis server. +### REDISPORT +Used to specify the port your redis server is using. -## TODO: -* Add some server config -* Security review? ## Create a new secret diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..1118f57 --- /dev/null +++ b/api/api.go @@ -0,0 +1,249 @@ +package api + +import ( + "encoding/json" + "fmt" + "html/template" + "mime" + "net/http" + "sync" + "time" + + "github.com/Gaardsholt/pass-along/config" + "github.com/Gaardsholt/pass-along/datastore" + "github.com/Gaardsholt/pass-along/memory" + "github.com/Gaardsholt/pass-along/metrics" + "github.com/Gaardsholt/pass-along/redis" + "github.com/Gaardsholt/pass-along/types" + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog/log" +) + +const ( + ErrServerShuttingDown = "http: Server closed" +) + +var pr *prometheus.Registry +var secretStore datastore.SecretStore +var startupTime time.Time +var templates map[string]*template.Template +var lock = sync.RWMutex{} + +// StartServer starts the internal and external http server and initiates the secrets store +func StartServer() (internalServer *http.Server, externalServer *http.Server) { + startupTime = time.Now() + + databaseType, err := config.Config.GetDatabaseType() + if err != nil { + log.Fatal().Err(err).Msgf("%s", err) + } + + switch databaseType { + case "in-memory": + secretStore, err = memory.New(&lock) + case "redis": + secretStore, err = redis.New() + } + + if err != nil { + log.Fatal().Err(err).Msgf("%s", err) + } + + registerPrometheusMetrics() + createTemplates() + + internal := mux.NewRouter() + external := mux.NewRouter() + // Start of static stuff + fs := http.FileServer(http.Dir("./static")) + external.PathPrefix("/assets").Handler(http.StripPrefix("/assets", fs)) + external.PathPrefix("/robots.txt").Handler(fs) + external.PathPrefix("/favicon.ico").Handler(fs) + // End of static stuff + + external.HandleFunc("/", IndexHandler).Methods("GET") + external.HandleFunc("/", NewHandler).Methods("POST") + external.HandleFunc("/{id}", GetHandler).Methods("GET") + + internal.HandleFunc("/healthz", healthz) + internal.Handle("/metrics", promhttp.HandlerFor(pr, promhttp.HandlerOpts{})).Methods("GET") + + internalPort := 8888 + internalServer = &http.Server{ + Addr: fmt.Sprintf(":%d", internalPort), + Handler: internal, + } + + go func() { + err := internalServer.ListenAndServe() + if err != nil && err.Error() != ErrServerShuttingDown { + log.Fatal().Err(err).Msgf("Unable to run the internal server at port %d", internalPort) + } + }() + + externalPort := 8080 + externalServer = &http.Server{ + Addr: fmt.Sprintf(":%d", externalPort), + Handler: external, + } + go func() { + err := externalServer.ListenAndServe() + if err != nil && err.Error() != ErrServerShuttingDown { + log.Fatal().Err(err).Msgf("Unable to run the external server at port %d", externalPort) + } + }() + log.Info().Msgf("Starting server at port %d with %s as datastore", externalPort, databaseType) + + go secretStore.DeleteExpiredSecrets() + + return +} + +func IndexHandler(w http.ResponseWriter, r *http.Request) { + templates["index"].Execute(w, types.Page{Startup: startupTime}) +} + +// NewHandler creates a new secret in the secretstore +func NewHandler(w http.ResponseWriter, r *http.Request) { + var entry types.Entry + err := json.NewDecoder(r.Body).Decode(&entry) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + log.Debug().Msg("Creating a new secret") + + expires := time.Now().Add( + time.Hour*time.Duration(0) + + time.Minute*time.Duration(0) + + time.Second*time.Duration(entry.ExpiresIn), + ) + + mySecret := types.Secret{ + Content: entry.Content, + Expires: expires, + TimeAdded: time.Now(), + UnlimitedViews: entry.UnlimitedViews, + } + mySecret.UnlimitedViews = entry.UnlimitedViews + id := mySecret.GenerateID() + + encryptedSecret, err := mySecret.Encrypt(id) + if err != nil { + go metrics.SecretsCreatedWithError.Inc() + return + } + + err = secretStore.Add(id, encryptedSecret, entry.ExpiresIn) + if err != nil { + http.Error(w, "failed to add secret, please try again", http.StatusInternalServerError) + log.Error().Err(err).Msg("Unable to add secret") + return + } + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, "%s", id) +} + +// GetHandler retrieves a secret in the secret store +func GetHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + useHtml := false + ctHeader := r.Header.Get("Content-Type") + contentType, _, err := mime.ParseMediaType(ctHeader) + if err != nil || contentType != "application/json" { + useHtml = true + } + + if useHtml { + newError := templates["read"].Execute(w, types.Page{Startup: startupTime}) + if newError != nil { + fmt.Fprintf(w, "%s", newError) + } + return + } + + id := vars["id"] + secretData, gotData := secretStore.Get(id) + if !gotData { + w.WriteHeader(http.StatusGone) + fmt.Fprint(w, "secret not found") + return + } + + s, err := types.Decrypt(secretData, id) + if err != nil { + log.Fatal().Err(err).Msg("Unable to decrypt secret") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err) + return + } + + decryptedSecret := "" + + isNotExpired := s.Expires.UTC().After(time.Now().UTC()) + if isNotExpired { + decryptedSecret = s.Content + go metrics.SecretsRead.Inc() + } else { + gotData = false + go metrics.ExpiredSecretsRead.Inc() + } + + if !isNotExpired || !s.UnlimitedViews { + secretStore.Delete(id) + } + + log.Debug().Msg("Fetching a secret") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "%s", decryptedSecret) +} + +// healthz is a liveness probe. +func healthz(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func registerPrometheusMetrics() { + pr = prometheus.NewRegistry() + // pr.MustRegister(types.NewSecretsInCache(&secretStore)) + pr.MustRegister(metrics.SecretsRead) + pr.MustRegister(metrics.ExpiredSecretsRead) + pr.MustRegister(metrics.NonExistentSecretsRead) + pr.MustRegister(metrics.SecretsCreated) + pr.MustRegister(metrics.SecretsCreatedWithError) + pr.MustRegister(metrics.SecretsDeleted) +} + +func createTemplates() { + templates = make(map[string]*template.Template) + templates["index"] = template.Must(template.ParseFiles("templates/base.html", "templates/index.html")) + templates["read"] = template.Must(template.ParseFiles("templates/base.html", "templates/read.html")) +} + +// func secretCleaner() { +// for { +// time.Sleep(5 * time.Minute) +// secretStore.Lock.RLock() +// for k, v := range secretStore.Data { +// s, err := types.Decrypt(v, k) +// if err != nil { +// continue +// } + +// isNotExpired := s.Expires.UTC().After(time.Now().UTC()) +// if !isNotExpired { +// log.Debug().Msg("Found expired secret, deleting...") +// secretStore.Lock.RUnlock() +// secretStore.Delete(k) +// secretStore.Lock.RLock() +// } +// } +// secretStore.Lock.RUnlock() +// } +// } diff --git a/config/config.go b/config/config.go index f2e2e2e..ce9719c 100644 --- a/config/config.go +++ b/config/config.go @@ -1,21 +1,52 @@ package config import ( + "fmt" "log" "github.com/kelseyhightower/envconfig" ) +// GlobalConfig holds config parameters type GlobalConfig struct { - ServerSalt string `required:"false"` + ServerSalt string `required:"false"` + DatabaseType *string `required:"false" default:"in-memory"` + RedisServer *string `required:"false"` + RedisPort *int `required:"false"` } var Config GlobalConfig //LoadConfig Loads config from env func LoadConfig() { - configErr := envconfig.Process("", &Config) - if configErr != nil { - log.Fatal(configErr) + err := envconfig.Process("", &Config) + if err != nil { + log.Fatal(err) } } + +// GetDatabaseType determines if a correct db is set +func (c GlobalConfig) GetDatabaseType() (string, error) { + switch *c.DatabaseType { + case "in-memory": + return *c.DatabaseType, nil + case "redis": + return *c.DatabaseType, nil + default: + return "", fmt.Errorf("unknown database type") + } +} + +func (c GlobalConfig) GetRedisServer() string { + if c.RedisServer != nil { + return *c.RedisServer + } + return "localhost" +} + +func (c GlobalConfig) GetRedisPort() int { + if c.RedisPort != nil { + return *c.RedisPort + } + return 6379 +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..98ea298 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,35 @@ +package config + +import ( + "os" + "testing" + + "gotest.tools/assert" +) + +// TestLoadConfigAsExpected tests if the config is loaded as expected +func TestLoadConfigAsExpected(t *testing.T) { + // arrange + os.Clearenv() + os.Setenv("SERVERSALT", "somesalt") + os.Setenv("DATABASETYPE", "redis") + + // act + LoadConfig() + + // assert + assert.Equal(t, "redis", *Config.DatabaseType) + assert.Equal(t, "somesalt", Config.ServerSalt) +} + +// TestLoadConfigDefaultDB tests if defaults work when no db env set +func TestLoadConfigDefaultDB(t *testing.T) { + // arrange + os.Clearenv() + + // act + LoadConfig() + + // assert + assert.Equal(t, "in-memory", *Config.DatabaseType) +} diff --git a/crypto/crypto.go b/crypto/crypto.go new file mode 100644 index 0000000..7918130 --- /dev/null +++ b/crypto/crypto.go @@ -0,0 +1,24 @@ +package crypto + +import ( + "bytes" + "crypto/sha512" + "encoding/gob" + + "github.com/Gaardsholt/pass-along/config" + "golang.org/x/crypto/pbkdf2" +) + +func deriveKey(passphrase string) []byte { + return pbkdf2.Key([]byte(passphrase), []byte(config.Config.ServerSalt), 1000, 32, sha512.New) +} + +func GetBytes(key interface{}) ([]byte, error) { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err := enc.Encode(key) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/crypto/decrypt.go b/crypto/decrypt.go new file mode 100644 index 0000000..a23db15 --- /dev/null +++ b/crypto/decrypt.go @@ -0,0 +1,31 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "fmt" +) + +func Decrypt(data []byte, encryptionKey string) (decryptedData []byte, err error) { + key := deriveKey(encryptionKey) + + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + if len(data) < nonceSize { + return nil, fmt.Errorf("nonce mismatch") + } + + nonce, ciphertext := data[:nonceSize], data[nonceSize:] + decryptedData, err = gcm.Open(nil, nonce, ciphertext, nil) + + return +} diff --git a/crypto/decrypt_test.go b/crypto/decrypt_test.go new file mode 100644 index 0000000..d3b368a --- /dev/null +++ b/crypto/decrypt_test.go @@ -0,0 +1,34 @@ +package crypto + +import ( + "testing" + + "gotest.tools/assert" +) + +// TestDecryptAsExpected tests if a string is decrypted correct +func TestDecryptAsExpected(t *testing.T) { + + secretValue := "mysupersecretvalue" + + // arrange + byteArray, err := GetBytes(secretValue) + if err != nil { + t.Error("encode error") + } + encryptedResult, err := Encrypt(secretValue, "encryptionkey") + if err != nil { + t.Error("encryption failed") + } + + // act + result, err := Decrypt(encryptedResult, "encryptionkey") + if err != nil { + t.Error("decryption failed") + } + t.Log(string(byteArray)) + t.Logf("%d", result) + + // assert + assert.Equal(t, string(byteArray), string(result)) +} diff --git a/crypto/encrypt.go b/crypto/encrypt.go new file mode 100644 index 0000000..5191288 --- /dev/null +++ b/crypto/encrypt.go @@ -0,0 +1,34 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "io" +) + +func Encrypt(data interface{}, encryptionKey string) (encryptedSecret []byte, err error) { + byteArray, err := GetBytes(data) + if err != nil { + return nil, err + } + key := deriveKey(encryptionKey) + + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + encryptedSecret = gcm.Seal(nonce, nonce, byteArray, nil) + return +} diff --git a/crypto/encrypt_test.go b/crypto/encrypt_test.go new file mode 100644 index 0000000..65cb17e --- /dev/null +++ b/crypto/encrypt_test.go @@ -0,0 +1,22 @@ +package crypto + +import ( + "testing" + + "gotest.tools/assert" +) + +// TestEncryptAsExpected tests if a string is encrypted, or tries to +func TestEncryptAsExpected(t *testing.T) { + // arrange + data := []byte("mysupersecretvalue") + + // act + result, err := Encrypt(data, "encryptionkey") + if err != nil { + t.Error("encryption failed") + } + + // assert + assert.Assert(t, string(result) != string(data)) +} diff --git a/crypto/hash.go b/crypto/hash.go new file mode 100644 index 0000000..c32ac6d --- /dev/null +++ b/crypto/hash.go @@ -0,0 +1,13 @@ +package crypto + +import ( + "crypto/sha512" + "encoding/base64" + "fmt" +) + +func Hash(data interface{}) string { + checksum := sha512.Sum512([]byte(fmt.Sprintf("%v", data))) + hash := base64.RawURLEncoding.EncodeToString(checksum[:]) + return hash +} diff --git a/datastore/datastore.go b/datastore/datastore.go new file mode 100644 index 0000000..adef46e --- /dev/null +++ b/datastore/datastore.go @@ -0,0 +1,8 @@ +package datastore + +type SecretStore interface { + Add(id string, secret []byte, expiresIn int) error + Get(id string) (secret []byte, gotData bool) + Delete(id string) + DeleteExpiredSecrets() +} diff --git a/go.mod b/go.mod index eae9401..a3e450d 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,28 @@ module github.com/Gaardsholt/pass-along go 1.17 require ( + github.com/alicebob/miniredis/v2 v2.16.0 + github.com/gomodule/redigo v1.8.5 github.com/gorilla/mux v1.8.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/prometheus/client_golang v1.11.0 github.com/rs/zerolog v1.25.0 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 + gotest.tools v2.2.0+incompatible ) require ( + github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/golang/protobuf v1.4.3 // indirect + github.com/google/go-cmp v0.5.5 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect + github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect google.golang.org/protobuf v1.26.0-rc.1 // indirect ) diff --git a/go.sum b/go.sum index d0f8f30..921a8ec 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,22 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.16.0 h1:ALkyFg7bSTEd1Mkrb4ppq4fnwjklA59dVtIehXCUZkU= +github.com/alicebob/miniredis/v2 v2.16.0/go.mod h1:gquAfGbzn92jvtrSC69+6zZnwSODVXVpYDRaGhWaL6I= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -33,6 +41,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc= +github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -66,7 +76,9 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -97,7 +109,11 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg= +github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -123,6 +139,7 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -165,4 +182,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/main.go b/main.go index 0339915..19250cc 100644 --- a/main.go +++ b/main.go @@ -2,76 +2,17 @@ package main import ( "context" - "encoding/json" - "fmt" - "mime" - "net/http" "os" "os/signal" - "sync" "syscall" - "text/template" - "time" + "github.com/Gaardsholt/pass-along/api" "github.com/Gaardsholt/pass-along/config" - "github.com/Gaardsholt/pass-along/metrics" - . "github.com/Gaardsholt/pass-along/types" - "github.com/gorilla/mux" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog/log" ) -var secretStore SecretStore -var templates map[string]*template.Template -var startupTime time.Time -var pr *prometheus.Registry - -var lock = sync.RWMutex{} - func init() { config.LoadConfig() - - startupTime = time.Now() - secretStore = SecretStore{ - Data: make(map[string][]byte), - Lock: &lock, - } - - templates = make(map[string]*template.Template) - templates["index"] = template.Must(template.ParseFiles("templates/base.html", "templates/index.html")) - templates["read"] = template.Must(template.ParseFiles("templates/base.html", "templates/read.html")) - - pr = prometheus.NewRegistry() - pr.MustRegister(NewSecretsInCache(&secretStore)) - pr.MustRegister(metrics.SecretsRead) - pr.MustRegister(metrics.ExpiredSecretsRead) - pr.MustRegister(metrics.NonExistentSecretsRead) - pr.MustRegister(metrics.SecretsCreated) - pr.MustRegister(metrics.SecretsCreatedWithError) - pr.MustRegister(metrics.SecretsDeleted) -} - -func secretCleaner() { - for { - time.Sleep(5 * time.Minute) - secretStore.Lock.RLock() - for k, v := range secretStore.Data { - s, err := Decrypt(v, k) - if err != nil { - continue - } - - isNotExpired := s.Expires.UTC().After(time.Now().UTC()) - if !isNotExpired { - log.Debug().Msg("Found expired secret, deleting...") - secretStore.Lock.RUnlock() - secretStore.Delete(k) - secretStore.Lock.RLock() - } - } - secretStore.Lock.RUnlock() - } } func main() { @@ -79,103 +20,17 @@ func main() { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) - // Start loop that checks for expired secrets and deletes them - go secretCleaner() - - r := mux.NewRouter() - // Start of static stuff - fs := http.FileServer(http.Dir("./static")) - r.PathPrefix("/js/").Handler(fs) - r.PathPrefix("/css/").Handler(fs) - r.PathPrefix("/favicon.ico").Handler(fs) - r.PathPrefix("/robots.txt").Handler(fs) - // End of static stuff - - r.HandleFunc("/", IndexHandler).Methods("GET") - r.HandleFunc("/", NewHandler).Methods("POST") - r.HandleFunc("/healthz", healthz) - r.PathPrefix("/metrics").Handler(promhttp.HandlerFor(pr, promhttp.HandlerOpts{})).Methods("GET") - // r.HandleFunc("/metrics", promhttp.Handler()).Methods("GET") - r.HandleFunc("/{id}", GetHandler).Methods("GET") - - port := 8080 - srv := &http.Server{ - Addr: fmt.Sprintf(":%d", port), - Handler: r, - } - go func() { - log.Fatal().Err(srv.ListenAndServe()).Msgf("Unable to run the server at port %d", port) - }() - log.Info().Msgf("Starting server at port %d", port) + internalServer, externalServer := api.StartServer() killSignal := <-interrupt switch killSignal { case os.Interrupt: - log.Info().Msg("Got SIGINT...") + log.Debug().Msg("Got SIGINT...") case syscall.SIGTERM: - log.Info().Msg("Got SIGTERM...") + log.Debug().Msg("Got SIGTERM...") } log.Info().Msg("The service is shutting down...") - srv.Shutdown(context.Background()) - log.Info().Msg("Done") -} - -func IndexHandler(w http.ResponseWriter, r *http.Request) { - templates["index"].Execute(w, Page{Startup: startupTime}) -} - -func NewHandler(w http.ResponseWriter, r *http.Request) { - var entry Entry - err := json.NewDecoder(r.Body).Decode(&entry) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - log.Debug().Msg("Creating a new secret") - - myId, err := secretStore.Add(entry) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusCreated) - fmt.Fprintf(w, "%s", myId) -} - -func GetHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - useHtml := false - ctHeader := r.Header.Get("Content-Type") - contentType, _, err := mime.ParseMediaType(ctHeader) - if err != nil || contentType != "application/json" { - useHtml = true - } - - if useHtml { - newError := templates["read"].Execute(w, Page{Startup: startupTime}) - if newError != nil { - fmt.Fprintf(w, "%s", newError) - } - return - } - - secretData, gotData := secretStore.Get(vars["id"]) - if !gotData { - w.WriteHeader(http.StatusGone) - fmt.Fprint(w, "secret not found") - return - } - - log.Debug().Msg("Fetching a secret") - - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "%s", secretData) -} - -// healthz is a liveness probe. -func healthz(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) + externalServer.Shutdown(context.Background()) + internalServer.Shutdown(context.Background()) } diff --git a/memory/memory.go b/memory/memory.go new file mode 100644 index 0000000..8322ffa --- /dev/null +++ b/memory/memory.go @@ -0,0 +1,69 @@ +package memory + +import ( + "sync" + "time" + + "github.com/rs/zerolog/log" + + "github.com/Gaardsholt/pass-along/metrics" + "github.com/Gaardsholt/pass-along/types" +) + +type SecretStore struct { + Data map[string][]byte + Lock *sync.RWMutex +} + +func New(lock *sync.RWMutex) (SecretStore, error) { + return SecretStore{ + Data: make(map[string][]byte), + Lock: lock, + }, nil +} + +func (ss SecretStore) Add(id string, secret []byte, expiresIn int) error { + ss.Lock.Lock() + defer ss.Lock.Unlock() + ss.Data[id] = secret + + go metrics.SecretsCreated.Inc() + return nil +} + +func (ss SecretStore) Get(id string) (secret []byte, gotData bool) { + ss.Lock.RLock() + secret, gotData = ss.Data[id] + ss.Lock.RUnlock() + return +} + +func (ss SecretStore) Delete(id string) { + ss.Lock.Lock() + defer ss.Lock.Unlock() + + delete(ss.Data, id) + go metrics.SecretsDeleted.Inc() +} + +func (ss SecretStore) DeleteExpiredSecrets() { + for { + time.Sleep(5 * time.Minute) + ss.Lock.RLock() + for k, v := range ss.Data { + s, err := types.Decrypt(v, k) + if err != nil { + continue + } + + isNotExpired := s.Expires.UTC().After(time.Now().UTC()) + if !isNotExpired { + log.Debug().Msg("Found expired secret, deleting...") + ss.Lock.RUnlock() + ss.Delete(k) + ss.Lock.RLock() + } + } + ss.Lock.RUnlock() + } +} diff --git a/redis/redis.go b/redis/redis.go new file mode 100644 index 0000000..bfc4e09 --- /dev/null +++ b/redis/redis.go @@ -0,0 +1,99 @@ +package redis + +import ( + "fmt" + "sync" + + "github.com/Gaardsholt/pass-along/config" + "github.com/Gaardsholt/pass-along/metrics" + "github.com/gomodule/redigo/redis" + "github.com/rs/zerolog/log" +) + +type SecretStore struct { + Data map[string][]byte + Lock *sync.RWMutex +} + +var pool *redis.Pool + +func New() (ss SecretStore, err error) { + + server := config.Config.GetRedisServer() + port := config.Config.GetRedisPort() + + pool = &redis.Pool{ + MaxIdle: 80, + MaxActive: 12000, // max number of connections + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", fmt.Sprintf("%s:%d", server, port)) + if err != nil { + panic(err.Error()) + } + return c, err + }, + } + + defer func() { + // recover from panic if one occured. Set err to nil otherwise. + r := recover() + if r != nil { + err = fmt.Errorf("%s", r) + } + }() + + conn := pool.Get() + defer conn.Close() + + ss = SecretStore{ + Data: make(map[string][]byte), + } + + return ss, nil +} + +func (ss SecretStore) Add(id string, secret []byte, expiresIn int) error { + conn := pool.Get() + defer conn.Close() + + _, err := conn.Do("HMSET", id, "secret", secret) + if err != nil { + go metrics.SecretsCreatedWithError.Inc() + return err + } + + _, err = conn.Do("EXPIRE", id, expiresIn) + if err != nil { + go metrics.SecretsCreatedWithError.Inc() + return err + } + go metrics.SecretsCreated.Inc() + return nil +} + +func (ss SecretStore) Get(id string) (secret []byte, gotData bool) { + conn := pool.Get() + defer conn.Close() + + secret, err := redis.Bytes(conn.Do("HGET", id, "secret")) + if err != nil { + go metrics.NonExistentSecretsRead.Inc() + return nil, false + } + return secret, true +} + +func (ss SecretStore) Delete(id string) { + conn := pool.Get() + defer conn.Close() + + _, err := conn.Do("HDEL", id, "secret") + if err != nil { + log.Fatal().Err(err).Msgf("Failed to delete secret with id %s", id) + } + go metrics.SecretsDeleted.Inc() +} + +func (ss SecretStore) DeleteExpiredSecrets() { + log.Debug().Msg("Not doing anything as redis will automatically delete expired secrets") +} diff --git a/redis/redis_test.go b/redis/redis_test.go new file mode 100644 index 0000000..bdaa0ad --- /dev/null +++ b/redis/redis_test.go @@ -0,0 +1,90 @@ +package redis + +import ( + "bytes" + "encoding/gob" + "testing" + + "github.com/Gaardsholt/pass-along/types" + "github.com/alicebob/miniredis/v2" + "github.com/gomodule/redigo/redis" + "github.com/rs/zerolog/log" + "gotest.tools/assert" +) + +var secretStore SecretStore + +// GetTestRedisServer creates the test server +func GetTestRedisServer(t *testing.T) SecretStore { + t.Helper() + + s, err := miniredis.Run() + if err != nil { + panic(err) + } + + pool = &redis.Pool{ + MaxIdle: 80, + MaxActive: 12000, // max number of connections + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", s.Addr()) + if err != nil { + panic(err.Error()) + } + return c, err + }, + } + + return SecretStore{ + Data: make(map[string][]byte), + } + +} + +// TestAddAsExpected tests adding and retrieving a secret in redis +func TestAddAsExpected(t *testing.T) { + // arrange + secretStore := GetTestRedisServer(t) + entry := types.Entry{ + Content: "supersecretvalue", + ExpiresIn: 1, + UnlimitedViews: false, + } + + var byteArray bytes.Buffer + err := gob.NewEncoder(&byteArray).Encode(entry) + if err != nil { + log.Fatal().Err(err).Msg("encode error") + } + + id := "1" + + // act + err = secretStore.Add(id, byteArray.Bytes(), entry.ExpiresIn) + if err != nil { + t.Error(err) + } + + content, gotData := secretStore.Get(id) + if gotData == false { + t.Error("no data recieved from redis") + } + + // assert + assert.Equal(t, string(content), byteArray.String()) +} + +// TestGetWithFailure tests getting a non existing secret in redis +func TestGetWithFailure(t *testing.T) { + // arrange + secretStore := GetTestRedisServer(t) + + // act + _, gotData := secretStore.Get("someid") + if gotData == true { + t.Error("data recieved from redis, while none was expected") + } + + // assert + assert.Equal(t, false, gotData) +} diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..ad2d1d2 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +cust_func(){ + for i in {1..1000} + do + echo "$i" + curl -X GET "https://jazz-adapter-api-test.k8s.bestcorp.net/style/12200509" -H "accept: text/plain" & + done +} + +for x in {1..5} +do + echo "loop number $x" + cust_func & # Put a function in the background +done + +wait +printf "\nAll done\n" + + + +# echo "POST http://localhost:8080/\nContent-Type: application/json\n@/Users/lasse.gaardsholt/load-test.json" | vegeta attack -duration=10s -output=create.bin +# vegeta plot -title="Create Results" create.bin > create.html +# open create.html + diff --git a/templates/base.html b/templates/base.html index fc7e06f..0cb1b44 100644 --- a/templates/base.html +++ b/templates/base.html @@ -25,7 +25,7 @@ - + @@ -42,7 +42,7 @@

Copy link

{{.Startup}}
- + \ No newline at end of file diff --git a/templates/read.html b/templates/read.html index fc1f2e5..4546705 100644 --- a/templates/read.html +++ b/templates/read.html @@ -7,5 +7,5 @@

Click here to create your own secret

- + {{end}} \ No newline at end of file diff --git a/types/secrets.go b/types/secrets.go index b875e51..afc410b 100644 --- a/types/secrets.go +++ b/types/secrets.go @@ -2,20 +2,14 @@ package types import ( "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/rand" "crypto/sha512" "encoding/base64" "encoding/gob" "fmt" - "io" - "log" + "math/rand" "time" - "github.com/Gaardsholt/pass-along/config" - "github.com/Gaardsholt/pass-along/metrics" - "golang.org/x/crypto/pbkdf2" + "github.com/Gaardsholt/pass-along/crypto" ) type Secret struct { @@ -25,7 +19,7 @@ type Secret struct { UnlimitedViews bool `json:"unlimited_views"` } -func new(content string, expires time.Time) Secret { +func NewSecret(content string, expires time.Time) Secret { return Secret{ Content: content, Expires: expires, @@ -33,135 +27,36 @@ func new(content string, expires time.Time) Secret { } } -func (s Secret) hash() string { - checksum := sha512.Sum512([]byte(fmt.Sprintf("%v", s))) +func (s Secret) GenerateID() string { + random := randomString(30) + checksum := sha512.Sum512([]byte(fmt.Sprintf("%v%v", s, random))) hash := base64.RawURLEncoding.EncodeToString(checksum[:]) return hash } -func deriveKey(passphrase string) []byte { - return pbkdf2.Key([]byte(passphrase), []byte(config.Config.ServerSalt), 1000, 32, sha512.New) +func (s Secret) Encrypt(encryptionKey string) ([]byte, error) { + return crypto.Encrypt(s, encryptionKey) } -func (s Secret) encrypt(encryptionKey string) ([]byte, error) { - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - err := enc.Encode(s) +func Decrypt(encryptedData []byte, encryptionKey string) (*Secret, error) { + decryptedData, err := crypto.Decrypt(encryptedData, encryptionKey) if err != nil { return nil, err } - key := deriveKey(encryptionKey) - - c, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(c) - if err != nil { - return nil, err - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err = io.ReadFull(rand.Reader, nonce); err != nil { - return nil, err - } - - encryptedSecret := gcm.Seal(nonce, nonce, buf.Bytes(), nil) - - return encryptedSecret, nil -} -func Decrypt(ciphertext []byte, encryptionKey string) (*Secret, error) { - key := deriveKey(encryptionKey) - - c, err := aes.NewCipher(key) + var secret Secret + dec := gob.NewDecoder(bytes.NewReader(decryptedData)) + err = dec.Decode(&secret) if err != nil { return nil, err } - gcm, err := cipher.NewGCM(c) - if err != nil { - return nil, err - } - - nonceSize := gcm.NonceSize() - if len(ciphertext) < nonceSize { - return nil, err - } - - nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, err - } - - p := Secret{} - dec := gob.NewDecoder(bytes.NewReader(plaintext)) - err = dec.Decode(&p) - if err != nil { - return nil, err - } - - return &p, nil -} - -func (ss SecretStore) Add(entry Entry) (id string, err error) { - expires := time.Now().Add( - time.Hour*time.Duration(0) + - time.Minute*time.Duration(0) + - time.Second*time.Duration(entry.ExpiresIn), - ) - - mySecret := new(entry.Content, expires) - mySecret.UnlimitedViews = entry.UnlimitedViews - id = mySecret.hash() - - baah, err := mySecret.encrypt(id) - if err != nil { - metrics.SecretsCreatedWithError.Inc() - return - } - - ss.Lock.Lock() - defer ss.Lock.Unlock() - ss.Data[id] = baah - - metrics.SecretsCreated.Inc() - return -} -func (ss SecretStore) Get(id string) (content string, gotData bool) { - ss.Lock.RLock() - value, gotData := ss.Data[id] - ss.Lock.RUnlock() - if gotData { - s, err := Decrypt(value, id) - if err != nil { - log.Fatal(err) - } - - isNotExpired := s.Expires.UTC().After(time.Now().UTC()) - if isNotExpired { - content = s.Content - metrics.SecretsRead.Inc() - } else { - gotData = false - metrics.ExpiredSecretsRead.Inc() - } - - if !isNotExpired || !s.UnlimitedViews { - ss.Delete(id) - } - return - } - metrics.NonExistentSecretsRead.Inc() - - return + return &secret, nil } -func (ss SecretStore) Delete(id string) { - ss.Lock.Lock() - defer ss.Lock.Unlock() - delete(ss.Data, id) - metrics.SecretsDeleted.Inc() +func randomString(length int) string { + rand.Seed(time.Now().UnixNano()) + b := make([]byte, length) + rand.Read(b) + return fmt.Sprintf("%x", b)[:length] }