From 0a98a5f5bf943d59b0e5aee10ec06f61b9825e02 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Fri, 12 Jul 2024 18:39:04 +0200 Subject: [PATCH 1/2] Mote filesystemstorage --- main.go | 12 ++++++------ store/{ => filesystem}/store.go | 6 +++--- store/{ => filesystem}/store_test.go | 24 ++++++++++++------------ 3 files changed, 21 insertions(+), 21 deletions(-) rename store/{ => filesystem}/store.go (95%) rename store/{ => filesystem}/store_test.go (91%) diff --git a/main.go b/main.go index 98f5351..bf17b88 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( "github.com/OpenSlides/vote-decrypt/crypto" "github.com/OpenSlides/vote-decrypt/decrypt" "github.com/OpenSlides/vote-decrypt/grpc" - "github.com/OpenSlides/vote-decrypt/store" + "github.com/OpenSlides/vote-decrypt/store/filesystem" "github.com/alecthomas/kong" "golang.org/x/sys/unix" ) @@ -30,10 +30,10 @@ func main() { err = runServer(ctx) case "main-key ": - err = runMainKey(ctx) + err = runMainKey() case "pub-key ": - err = runPubKey(ctx) + err = runPubKey() default: panic(fmt.Sprintf("Unknown command: %s", cliCtx.Command())) @@ -76,7 +76,7 @@ func runServer(ctx context.Context) error { decrypter := decrypt.New( cryptoLib, - store.New(cli.Server.Store), + filesystem.New(cli.Server.Store), ) addr := fmt.Sprintf(":%d", cli.Server.Port) @@ -88,7 +88,7 @@ func runServer(ctx context.Context) error { return nil } -func runPubKey(ctx context.Context) error { +func runPubKey() error { key := make([]byte, 32) if _, err := io.ReadFull(cli.PubKey.MainKey, key); err != nil { return fmt.Errorf("reading key: %w", err) @@ -110,7 +110,7 @@ func runPubKey(ctx context.Context) error { return nil } -func runMainKey(ctx context.Context) error { +func runMainKey() error { key := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, key); err != nil { return fmt.Errorf("reading key: %w", err) diff --git a/store/store.go b/store/filesystem/store.go similarity index 95% rename from store/store.go rename to store/filesystem/store.go index fc5fbd7..e42a673 100644 --- a/store/store.go +++ b/store/filesystem/store.go @@ -1,6 +1,6 @@ -// Package store is a storrage backend for vote-decrypt that uses the file +// Package filesystem is a storrage backend for vote-decrypt that uses the file // system. -package store +package filesystem import ( "crypto/subtle" @@ -47,7 +47,7 @@ func (s *Store) SaveKey(id string, key []byte) error { defer s.mu.Unlock() if s.path == "" { - return fmt.Errorf("No data dir provided. Check the environment variable VOTE_DECRYPT_STORE") + return fmt.Errorf("no data dir provided. Check the environment variable VOTE_DECRYPT_STORE") } if err := os.MkdirAll(s.path, os.ModePerm); err != nil { diff --git a/store/store_test.go b/store/filesystem/store_test.go similarity index 91% rename from store/store_test.go rename to store/filesystem/store_test.go index 6de64a3..54fe5ad 100644 --- a/store/store_test.go +++ b/store/filesystem/store_test.go @@ -1,4 +1,4 @@ -package store_test +package filesystem_test import ( "bytes" @@ -9,13 +9,13 @@ import ( "testing" "github.com/OpenSlides/vote-decrypt/errorcode" - "github.com/OpenSlides/vote-decrypt/store" + "github.com/OpenSlides/vote-decrypt/store/filesystem" ) func TestSaveKey(t *testing.T) { t.Run("valid", func(t *testing.T) { tmpPath := t.TempDir() - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if err := s.SaveKey("test/5", []byte("key")); err != nil { t.Fatalf("SaveKey: %v", err) @@ -44,7 +44,7 @@ func TestSaveKey(t *testing.T) { t.Run("file exists", func(t *testing.T) { tmpPath := t.TempDir() os.WriteFile(path.Join(tmpPath, "test_5.key"), []byte("old key"), 0400) - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if err := s.SaveKey("test/5", []byte("key")); err != errorcode.Exist { t.Errorf("SaveKey returned error `%v`, expected `%v`", err, errorcode.Exist) @@ -56,7 +56,7 @@ func TestLoadKey(t *testing.T) { t.Run("valid", func(t *testing.T) { tmpPath := t.TempDir() os.WriteFile(path.Join(tmpPath, "test_5.key"), []byte("key"), 0400) - s := store.New(tmpPath) + s := filesystem.New(tmpPath) got, err := s.LoadKey("test/5") if err != nil { @@ -70,7 +70,7 @@ func TestLoadKey(t *testing.T) { t.Run("key unknown", func(t *testing.T) { tmpPath := t.TempDir() - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if _, err := s.LoadKey("test/5"); err != errorcode.NotExist { t.Errorf("LoadKey retunred `%v`, expected `%v`", err, errorcode.NotExist) @@ -82,7 +82,7 @@ func TestValidateSignature(t *testing.T) { t.Run("firt time", func(t *testing.T) { tmpPath := t.TempDir() os.WriteFile(path.Join(tmpPath, "test_5.key"), []byte("key"), 0400) - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if err := s.ValidateSignature("test/5", []byte("hash")); err != nil { t.Errorf("ValidateSignature: %v", err) @@ -112,7 +112,7 @@ func TestValidateSignature(t *testing.T) { tmpPath := t.TempDir() os.WriteFile(path.Join(tmpPath, "test_5.key"), []byte("key"), 0400) os.WriteFile(path.Join(tmpPath, "test_5.hash"), []byte("hash"), 0400) - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if err := s.ValidateSignature("test/5", []byte("hash")); err != nil { t.Fatalf("ValidateSignature: %v", err) @@ -123,7 +123,7 @@ func TestValidateSignature(t *testing.T) { tmpPath := t.TempDir() os.WriteFile(path.Join(tmpPath, "test_5.key"), []byte("key"), 0400) os.WriteFile(path.Join(tmpPath, "test_5.hash"), []byte("hash"), 0400) - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if err := s.ValidateSignature("test/5", []byte("invalid")); err != errorcode.Invalid { t.Fatalf("ValidateSignature returned `%v`, expected `%s`", err, errorcode.Invalid) @@ -132,7 +132,7 @@ func TestValidateSignature(t *testing.T) { t.Run("unknown poll", func(t *testing.T) { tmpPath := t.TempDir() - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if err := s.ValidateSignature("test/5", []byte("hash")); err != errorcode.NotExist { t.Fatalf("ValidateSignature returned `%v`, expected `%s`", err, errorcode.NotExist) @@ -147,7 +147,7 @@ func TestClearPoll(t *testing.T) { hashFile := path.Join(tmpPath, "test_5.hash") os.WriteFile(keyFile, []byte("key"), 0400) os.WriteFile(hashFile, []byte("hash"), 0400) - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if err := s.ClearPoll("test/5"); err != nil { t.Fatalf("ClearPoll: %v", err) @@ -164,7 +164,7 @@ func TestClearPoll(t *testing.T) { t.Run("files not exist", func(t *testing.T) { tmpPath := t.TempDir() - s := store.New(tmpPath) + s := filesystem.New(tmpPath) if err := s.ClearPoll("test/5"); err != nil { t.Fatalf("ClearPoll: %v", err) From 762acd32885e471060c6c45bf01bf82618e5d467 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Fri, 12 Jul 2024 20:10:15 +0200 Subject: [PATCH 2/2] Postgres storage --- README.md | 2 - decrypt/decrypt.go | 18 ++-- decrypt/mock_test.go | 9 +- go.mod | 34 +++++++- go.sum | 127 ++++++++++++++++++++++++++- main.go | 23 ++++- store/filesystem/store.go | 9 +- store/filesystem/store_test.go | 28 +++--- store/postgres/schema.sql | 13 +++ store/postgres/store.go | 152 +++++++++++++++++++++++++++++++++ store/postgres/store_test.go | 152 +++++++++++++++++++++++++++++++++ 11 files changed, 532 insertions(+), 35 deletions(-) create mode 100644 store/postgres/schema.sql create mode 100644 store/postgres/store.go create mode 100644 store/postgres/store_test.go diff --git a/README.md b/README.md index e86f54d..152a58a 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,5 @@ The service uses the following enironment variables: ## TODOs: * Fix the Stop method to hash the input instead of the output. -* Fix more timing attacks. -* Write a postgres storage backend. * Write errors messages as output. * Use the main key to encrypt the stored data (poll keys and poll hashes) diff --git a/decrypt/decrypt.go b/decrypt/decrypt.go index 8ae3d46..a76fed6 100644 --- a/decrypt/decrypt.go +++ b/decrypt/decrypt.go @@ -71,7 +71,7 @@ func (d *Decrypt) Start(ctx context.Context, pollID string) (pubKey []byte, pubK } // TODO: Load Key and CreatePoll Key have probably be atomic. - pollKey, err := d.store.LoadKey(pollID) + pollKey, err := d.store.LoadKey(ctx, pollID) if err != nil { if !errors.Is(err, errorcode.NotExist) { return nil, nil, fmt.Errorf("loading poll key: %w", err) @@ -83,7 +83,7 @@ func (d *Decrypt) Start(ctx context.Context, pollID string) (pubKey []byte, pubK } pollKey = key - if err := d.store.SaveKey(pollID, key); err != nil { + if err := d.store.SaveKey(ctx, pollID, key); err != nil { return nil, nil, fmt.Errorf("saving poll key: %w", err) } } @@ -107,7 +107,7 @@ func (d *Decrypt) Start(ctx context.Context, pollID string) (pubKey []byte, pubK // // TODO: This implementation is wrong. Not the output has to be hashed and saved, but the input. func (d *Decrypt) Stop(ctx context.Context, pollID string, voteList [][]byte) (decryptedContent, signature []byte, err error) { - pollKey, err := d.store.LoadKey(pollID) + pollKey, err := d.store.LoadKey(ctx, pollID) if err != nil { return nil, nil, fmt.Errorf("loading poll key: %w", err) } @@ -131,7 +131,7 @@ func (d *Decrypt) Stop(ctx context.Context, pollID string, voteList [][]byte) (d // This has to be the last step of this function to protect agains timing // attacks. All other steps have to be run, even when the calll is doomed to // fail in this step - if err := d.store.ValidateSignature(pollID, signature); err != nil { + if err := d.store.ValidateSignature(ctx, pollID, signature); err != nil { if errors.Is(err, errorcode.Invalid) { return nil, nil, fmt.Errorf("stop was called with different parameters before") } @@ -143,7 +143,7 @@ func (d *Decrypt) Stop(ctx context.Context, pollID string, voteList [][]byte) (d // Clear stops a poll by removing the generated cryptographic key. func (d *Decrypt) Clear(ctx context.Context, pollID string) error { - if err := d.store.ClearPoll(pollID); err != nil { + if err := d.store.ClearPoll(ctx, pollID); err != nil { return fmt.Errorf("clearing poll from store: %w", err) } return nil @@ -261,12 +261,12 @@ type Store interface { // SaveKey stores the private key. // // Has to return an error `errorcode.Exist` if the key is already known. - SaveKey(id string, key []byte) error + SaveKey(ctx context.Context, id string, key []byte) error // LoadKey returns the private key from the store. // // If the poll is unknown return `errorcode.NotExist` - LoadKey(id string) (key []byte, err error) + LoadKey(ctx context.Context, id string) (key []byte, err error) // ValidateSignature makes sure, that no other signature is saved for a // poll. Saves the signature for future calls. @@ -275,12 +275,12 @@ type Store interface { // call. // // Has to return `errorcode.NotExist` when the id does not exist. - ValidateSignature(id string, hash []byte) error + ValidateSignature(ctx context.Context, id string, hash []byte) error // ClearPoll removes all data for the poll. // // Does not return an error if poll does not exist. - ClearPoll(id string) error + ClearPoll(ctx context.Context, id string) error } // jsonListToContent creates one byte slice from a list of votes in json format. diff --git a/decrypt/mock_test.go b/decrypt/mock_test.go index 608161e..827f775 100644 --- a/decrypt/mock_test.go +++ b/decrypt/mock_test.go @@ -2,6 +2,7 @@ package decrypt_test import ( "bytes" + "context" "fmt" "sync" @@ -53,7 +54,7 @@ func NewStoreMock() *StoreMock { } } -func (s *StoreMock) SaveKey(id string, key []byte) error { +func (s *StoreMock) SaveKey(_ context.Context, id string, key []byte) error { s.mu.Lock() defer s.mu.Unlock() @@ -68,7 +69,7 @@ func (s *StoreMock) SaveKey(id string, key []byte) error { // LoadKey returns the private key from the store. // // If the poll is unknown return (nil, nil) -func (s *StoreMock) LoadKey(id string) ([]byte, error) { +func (s *StoreMock) LoadKey(_ context.Context, id string) ([]byte, error) { s.mu.Lock() defer s.mu.Unlock() @@ -83,7 +84,7 @@ func (s *StoreMock) LoadKey(id string) ([]byte, error) { // poll. Saves the signature for future calls. // // Has to return an error if the id is unknown in the store. -func (s *StoreMock) ValidateSignature(id string, signature []byte) error { +func (s *StoreMock) ValidateSignature(_ context.Context, id string, signature []byte) error { s.mu.Lock() defer s.mu.Unlock() @@ -106,7 +107,7 @@ func (s *StoreMock) ValidateSignature(id string, signature []byte) error { } // Clear removes all data for the poll. -func (s *StoreMock) ClearPoll(id string) error { +func (s *StoreMock) ClearPoll(_ context.Context, id string) error { s.mu.Lock() defer s.mu.Unlock() diff --git a/go.mod b/go.mod index a89c9d2..cb43d81 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.22 require ( github.com/alecthomas/kong v0.9.0 github.com/golang/protobuf v1.5.4 + github.com/jackc/pgx/v5 v5.6.0 + github.com/ory/dockertest/v3 v3.10.0 golang.org/x/crypto v0.25.0 golang.org/x/sys v0.22.0 google.golang.org/grpc v1.65.0 @@ -12,7 +14,37 @@ require ( ) require ( - golang.org/x/net v0.25.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/containerd/continuity v0.4.3 // indirect + github.com/docker/cli v27.0.3+incompatible // indirect + github.com/docker/docker v27.0.3+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runc v1.1.13 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 782bafe..ac3055f 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,149 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +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/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ= +github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= +github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/main.go b/main.go index bf17b88..d893f02 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "github.com/OpenSlides/vote-decrypt/decrypt" "github.com/OpenSlides/vote-decrypt/grpc" "github.com/OpenSlides/vote-decrypt/store/filesystem" + "github.com/OpenSlides/vote-decrypt/store/postgres" "github.com/alecthomas/kong" "golang.org/x/sys/unix" ) @@ -49,8 +50,9 @@ var cli struct { Server struct { MainKey *os.File `arg:"" help:"Path to the main key file."` - Port int `help:"Port for the server. Defaults to 9014." short:"p" env:"VOTE_DECRYPT_PORT" default:"9014"` - Store string `help:"Path for the file system storage of poll keys." env:"VOTE_DECRYPT_STORE" default:"vote_data"` + Port int `help:"Port for the server. Defaults to 9014." short:"p" env:"VOTE_DECRYPT_PORT" default:"9014"` + Store string `help:"Path for the file system storage of poll keys." env:"VOTE_DECRYPT_STORE" default:"vote_data"` + PostgresConf string `help:"Config for Postgres" env:"VOTE_DECRYPT_POSTGRES"` } `cmd:"" help:"Starts the vote decrypt grpc server." default:"withargs"` MainKey struct { @@ -74,9 +76,24 @@ func runServer(ctx context.Context) error { fmt.Printf("Public Main Key: %s\n", base64.StdEncoding.EncodeToString(cryptoLib.PublicMainKey())) + var store decrypt.Store + switch { + case cli.Server.PostgresConf != "": + pg, err := postgres.New(ctx, cli.Server.PostgresConf) + if err != nil { + return fmt.Errorf("connecting to postgres: %w", err) + } + + store = pg + + default: + store = filesystem.New(cli.Server.Store) + + } + decrypter := decrypt.New( cryptoLib, - filesystem.New(cli.Server.Store), + store, ) addr := fmt.Sprintf(":%d", cli.Server.Port) diff --git a/store/filesystem/store.go b/store/filesystem/store.go index e42a673..298295a 100644 --- a/store/filesystem/store.go +++ b/store/filesystem/store.go @@ -3,6 +3,7 @@ package filesystem import ( + "context" "crypto/subtle" "errors" "fmt" @@ -42,7 +43,7 @@ func New(path string) *Store { // SaveKey stores the private key. // // Has to return an error, if a key already exists. -func (s *Store) SaveKey(id string, key []byte) error { +func (s *Store) SaveKey(_ context.Context, id string, key []byte) error { s.mu.Lock() defer s.mu.Unlock() @@ -78,7 +79,7 @@ func (s *Store) SaveKey(id string, key []byte) error { // LoadKey returns the private key from the store. // // If the poll is unknown return (nil, nil) -func (s *Store) LoadKey(id string) ([]byte, error) { +func (s *Store) LoadKey(_ context.Context, id string) ([]byte, error) { s.mu.Lock() defer s.mu.Unlock() @@ -97,7 +98,7 @@ func (s *Store) LoadKey(id string) ([]byte, error) { // poll. Saves the signature for future calls. // // Has to return an error if the id is unknown in the store. -func (s *Store) ValidateSignature(id string, hash []byte) error { +func (s *Store) ValidateSignature(_ context.Context, id string, hash []byte) error { s.mu.Lock() defer s.mu.Unlock() @@ -145,7 +146,7 @@ func (s *Store) checkHash(id string, hash []byte) error { } // ClearPoll removes all data for the poll. -func (s *Store) ClearPoll(id string) error { +func (s *Store) ClearPoll(_ context.Context, id string) error { s.mu.Lock() defer s.mu.Unlock() diff --git a/store/filesystem/store_test.go b/store/filesystem/store_test.go index 54fe5ad..cc281e6 100644 --- a/store/filesystem/store_test.go +++ b/store/filesystem/store_test.go @@ -2,6 +2,7 @@ package filesystem_test import ( "bytes" + "context" "errors" "io/fs" "os" @@ -13,11 +14,12 @@ import ( ) func TestSaveKey(t *testing.T) { + ctx := context.Background() t.Run("valid", func(t *testing.T) { tmpPath := t.TempDir() s := filesystem.New(tmpPath) - if err := s.SaveKey("test/5", []byte("key")); err != nil { + if err := s.SaveKey(ctx, "test/5", []byte("key")); err != nil { t.Fatalf("SaveKey: %v", err) } @@ -46,19 +48,21 @@ func TestSaveKey(t *testing.T) { os.WriteFile(path.Join(tmpPath, "test_5.key"), []byte("old key"), 0400) s := filesystem.New(tmpPath) - if err := s.SaveKey("test/5", []byte("key")); err != errorcode.Exist { + if err := s.SaveKey(ctx, "test/5", []byte("key")); err != errorcode.Exist { t.Errorf("SaveKey returned error `%v`, expected `%v`", err, errorcode.Exist) } }) } func TestLoadKey(t *testing.T) { + ctx := context.Background() + t.Run("valid", func(t *testing.T) { tmpPath := t.TempDir() os.WriteFile(path.Join(tmpPath, "test_5.key"), []byte("key"), 0400) s := filesystem.New(tmpPath) - got, err := s.LoadKey("test/5") + got, err := s.LoadKey(ctx, "test/5") if err != nil { t.Fatalf("LoadKey returns: %v", err) } @@ -72,19 +76,21 @@ func TestLoadKey(t *testing.T) { tmpPath := t.TempDir() s := filesystem.New(tmpPath) - if _, err := s.LoadKey("test/5"); err != errorcode.NotExist { + if _, err := s.LoadKey(ctx, "test/5"); err != errorcode.NotExist { t.Errorf("LoadKey retunred `%v`, expected `%v`", err, errorcode.NotExist) } }) } func TestValidateSignature(t *testing.T) { + ctx := context.Background() + t.Run("firt time", func(t *testing.T) { tmpPath := t.TempDir() os.WriteFile(path.Join(tmpPath, "test_5.key"), []byte("key"), 0400) s := filesystem.New(tmpPath) - if err := s.ValidateSignature("test/5", []byte("hash")); err != nil { + if err := s.ValidateSignature(ctx, "test/5", []byte("hash")); err != nil { t.Errorf("ValidateSignature: %v", err) } @@ -114,7 +120,7 @@ func TestValidateSignature(t *testing.T) { os.WriteFile(path.Join(tmpPath, "test_5.hash"), []byte("hash"), 0400) s := filesystem.New(tmpPath) - if err := s.ValidateSignature("test/5", []byte("hash")); err != nil { + if err := s.ValidateSignature(ctx, "test/5", []byte("hash")); err != nil { t.Fatalf("ValidateSignature: %v", err) } }) @@ -125,7 +131,7 @@ func TestValidateSignature(t *testing.T) { os.WriteFile(path.Join(tmpPath, "test_5.hash"), []byte("hash"), 0400) s := filesystem.New(tmpPath) - if err := s.ValidateSignature("test/5", []byte("invalid")); err != errorcode.Invalid { + if err := s.ValidateSignature(ctx, "test/5", []byte("invalid")); err != errorcode.Invalid { t.Fatalf("ValidateSignature returned `%v`, expected `%s`", err, errorcode.Invalid) } }) @@ -134,13 +140,15 @@ func TestValidateSignature(t *testing.T) { tmpPath := t.TempDir() s := filesystem.New(tmpPath) - if err := s.ValidateSignature("test/5", []byte("hash")); err != errorcode.NotExist { + if err := s.ValidateSignature(ctx, "test/5", []byte("hash")); err != errorcode.NotExist { t.Fatalf("ValidateSignature returned `%v`, expected `%s`", err, errorcode.NotExist) } }) } func TestClearPoll(t *testing.T) { + ctx := context.Background() + t.Run("remove files", func(t *testing.T) { tmpPath := t.TempDir() keyFile := path.Join(tmpPath, "test_5.key") @@ -149,7 +157,7 @@ func TestClearPoll(t *testing.T) { os.WriteFile(hashFile, []byte("hash"), 0400) s := filesystem.New(tmpPath) - if err := s.ClearPoll("test/5"); err != nil { + if err := s.ClearPoll(ctx, "test/5"); err != nil { t.Fatalf("ClearPoll: %v", err) } @@ -166,7 +174,7 @@ func TestClearPoll(t *testing.T) { tmpPath := t.TempDir() s := filesystem.New(tmpPath) - if err := s.ClearPoll("test/5"); err != nil { + if err := s.ClearPoll(ctx, "test/5"); err != nil { t.Fatalf("ClearPoll: %v", err) } }) diff --git a/store/postgres/schema.sql b/store/postgres/schema.sql new file mode 100644 index 0000000..6d4c888 --- /dev/null +++ b/store/postgres/schema.sql @@ -0,0 +1,13 @@ +CREATE SCHEMA IF NOT EXISTS vote_decrypt; + +CREATE TABLE IF NOT EXISTS vote_decrypt.poll( + -- The poll id, is a string like 'mydomain.com/42 + id TEXT PRIMARY KEY, + + -- Key data + key BYTEA NOT NULL, + + -- Hash of the signed poll data + hash BYTEA Null +); + diff --git a/store/postgres/store.go b/store/postgres/store.go new file mode 100644 index 0000000..dc14470 --- /dev/null +++ b/store/postgres/store.go @@ -0,0 +1,152 @@ +// Package postgres is a storrage backend for vote-decrypt that uses postgres. +package postgres + +import ( + "context" + "crypto/subtle" + _ "embed" // Needed for file embedding + "fmt" + "time" + + "github.com/OpenSlides/vote-decrypt/errorcode" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +//go:embed schema.sql +var schema string + +// Store implements the decrypt.Store interface by writing the data to postgres. +type Store struct { + pool *pgxpool.Pool +} + +// New initializes a new Store. +func New(ctx context.Context, connString string) (*Store, error) { + conf, err := pgxpool.ParseConfig(connString) + if err != nil { + return nil, fmt.Errorf("invalid connection url: %w", err) + } + + // Fix issue with gbBouncer. The documentation says, that this make the + // connection slower. We have to test the performance. Maybe it is better to + // remove the connection pool here or not use bgBouncer at all. + // + // See https://github.com/OpenSlides/openslides-vote-service/pull/66 + conf.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol + + pool, err := pgxpool.NewWithConfig(ctx, conf) + if err != nil { + return nil, fmt.Errorf("creating connection pool: %w", err) + } + + s := Store{ + pool: pool, + } + + s.Wait(ctx) + + if err := s.Migrate(ctx); err != nil { + return nil, fmt.Errorf("migrating database: %w", err) + } + + return &s, nil +} + +// Wait blocks until a connection to postgres can be established. +func (s *Store) Wait(ctx context.Context) { + for ctx.Err() == nil { + err := s.pool.Ping(ctx) + if err == nil { + return + } + fmt.Printf("Waiting for postgres: %v", err) + time.Sleep(500 * time.Millisecond) + } +} + +// Migrate creates the database schema. +func (s *Store) Migrate(ctx context.Context) error { + if _, err := s.pool.Exec(ctx, schema); err != nil { + return fmt.Errorf("creating schema: %w", err) + } + return nil +} + +// Close closes all connections. It blocks, until all connection are closed. +func (s *Store) Close() { + s.pool.Close() +} + +// SaveKey stores the private key. +// +// Has to return an error, if a key already exists. +func (s *Store) SaveKey(ctx context.Context, id string, key []byte) error { + sql := `INSERT INTO vote_decrypt.poll (id, key) VALUES ($1, $2);` + if _, err := s.pool.Exec(ctx, sql, id, key); err != nil { + return fmt.Errorf("insert key: %w", err) + } + + return nil +} + +// LoadKey returns the private key from the store. +// +// If the poll is unknown return (nil, nil) +func (s *Store) LoadKey(ctx context.Context, id string) ([]byte, error) { + sql := `SELECT key FROM vote_decrypt.poll where id = $1` + + var key []byte + if err := s.pool.QueryRow(ctx, sql, id).Scan(&key); err != nil { + return nil, fmt.Errorf("fetching key: %w", err) + } + + return key, nil +} + +// ValidateSignature makes sure, that no other signature is saved for a +// poll. Saves the signature for future calls. +// +// Has to return an error if the id is unknown in the store. +func (s *Store) ValidateSignature(ctx context.Context, id string, hash []byte) error { + err := pgx.BeginTxFunc( + ctx, + s.pool, + pgx.TxOptions{ + IsoLevel: "REPEATABLE READ", + }, + func(tx pgx.Tx) error { + sql := `SELECT hash FROM vote_decrypt.poll where id = $1` + var currentHash []byte + if err := s.pool.QueryRow(ctx, sql, id).Scan(¤tHash); err != nil { + return fmt.Errorf("fetching key: %w", err) + } + + if currentHash != nil { + if subtle.ConstantTimeCompare(hash, currentHash) != 1 { + return errorcode.Invalid + } + return nil + } + + sql = "UPDATE vote_decrypt.poll SET hash = $2 WHERE id = $1 AND hash IS NULL;" + if _, err := s.pool.Exec(ctx, sql, id, hash); err != nil { + return fmt.Errorf("write hash: %w", err) + } + + return nil + + }, + ) + + return err +} + +// ClearPoll removes all data for the poll. +func (s *Store) ClearPoll(ctx context.Context, id string) error { + sql := "DELETE FROM vote_decrypt.poll WHERE id = $1" + if _, err := s.pool.Exec(ctx, sql, id); err != nil { + return fmt.Errorf("deleting data of poll %s: %w", id, err) + } + return nil +} diff --git a/store/postgres/store_test.go b/store/postgres/store_test.go new file mode 100644 index 0000000..9030ba6 --- /dev/null +++ b/store/postgres/store_test.go @@ -0,0 +1,152 @@ +package postgres_test + +import ( + "context" + "fmt" + "testing" + + "github.com/OpenSlides/vote-decrypt/store/postgres" + "github.com/ory/dockertest/v3" +) + +func startPostgres(t *testing.T) (string, func()) { + t.Helper() + + pool, err := dockertest.NewPool("") + if err != nil { + t.Fatalf("Could not connect to docker: %s", err) + } + + runOpts := dockertest.RunOptions{ + Repository: "postgres", + Tag: "13", + Env: []string{ + "POSTGRES_USER=postgres", + "POSTGRES_PASSWORD=password", + "POSTGRES_DB=database", + }, + } + + resource, err := pool.RunWithOptions(&runOpts) + if err != nil { + t.Fatalf("Could not start postgres container: %s", err) + } + + return resource.GetPort("5432/tcp"), func() { + if err = pool.Purge(resource); err != nil { + t.Fatalf("Could not purge postgres container: %s", err) + } + } +} + +func TestPostgres(t *testing.T) { + ctx := context.Background() + port, close := startPostgres(t) + defer close() + + addr := fmt.Sprintf(`user=postgres password='password' host=localhost port=%s dbname=database`, port) + p, err := postgres.New(ctx, addr) + if err != nil { + t.Fatalf("Creating postgres backend returned: %v", err) + } + defer p.Close() + + t.Logf("Postgres port: %s", port) + + t.Run("SaveKey", func(t *testing.T) { + t.Run("Saving key twice", func(t *testing.T) { + if err := p.SaveKey(ctx, "poll1", []byte("my-key")); err != nil { + t.Fatalf("SaveKey: %v", err) + } + + err := p.SaveKey(ctx, "poll1", []byte("other key in same poll")) + + if err == nil { + t.Errorf("saveKey called on the same id did not return an error.") + } + }) + }) + + t.Run("LoadKey", func(t *testing.T) { + t.Run("load existing poll", func(t *testing.T) { + if err := p.SaveKey(ctx, "poll2", []byte("my-key")); err != nil { + t.Fatalf("SaveKey: %v", err) + } + + got, err := p.LoadKey(ctx, "poll2") + if err != nil { + t.Fatalf("LoadKey: %v", err) + } + + if string(got) != "my-key" { + t.Errorf("got key %v, expected %v", got, "my-key") + } + }) + + t.Run("load unknown poll", func(t *testing.T) { + _, err := p.LoadKey(ctx, "unknown") + if err == nil { + t.Errorf("expect an error") + } + }) + }) + + t.Run("ValidateSignature", func(t *testing.T) { + t.Run("save signature", func(t *testing.T) { + if err := p.SaveKey(ctx, "poll3", []byte("my-key")); err != nil { + t.Fatalf("SaveKey: %v", err) + } + + if err := p.ValidateSignature(ctx, "poll3", []byte("my-hash")); err != nil { + t.Fatalf("ValidateSignature: %v", err) + } + }) + + t.Run("save signature twice", func(t *testing.T) { + if err := p.SaveKey(ctx, "poll4", []byte("my-key")); err != nil { + t.Fatalf("SaveKey: %v", err) + } + + if err := p.ValidateSignature(ctx, "poll4", []byte("my-hash")); err != nil { + t.Fatalf("ValidateSignature: %v", err) + } + + if err := p.ValidateSignature(ctx, "poll4", []byte("my-hash")); err != nil { + t.Fatalf("ValidateSignature: %v", err) + } + }) + + t.Run("save different signature", func(t *testing.T) { + if err := p.SaveKey(ctx, "poll5", []byte("my-key")); err != nil { + t.Fatalf("SaveKey: %v", err) + } + + if err := p.ValidateSignature(ctx, "poll5", []byte("my-hash")); err != nil { + t.Fatalf("ValidateSignature: %v", err) + } + + err := p.ValidateSignature(ctx, "poll5", []byte("WRONG")) + if err == nil { + t.Errorf("ValidateSignature for a different sigature did not return an error") + } + }) + }) + + t.Run("ClearPoll", func(t *testing.T) { + t.Run("Saving key twice with clear in between", func(t *testing.T) { + if err := p.SaveKey(ctx, "poll6", []byte("my-key")); err != nil { + t.Fatalf("SaveKey: %v", err) + } + + if err := p.ClearPoll(ctx, "poll6"); err != nil { + t.Fatalf("ClearPoll: %v", err) + } + + if err := p.SaveKey(ctx, "poll6", []byte("my-key")); err != nil { + t.Fatalf("SaveKey: %v", err) + } + + }) + }) + +}