diff --git a/README.md b/README.md index 5f4beef..c8fd98d 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,24 @@ Response: ``` +### Crypto Public Main Key + +The vote service can return the public main service from vote-decrypt. The key +is decoded as base64. + +Example: + +``` +curl localhost:9013/internal/vote/public_main_key +``` + +Response: + +``` +dHR0dHR0dHR0ZWtmanRpd28zbmdrZGkxMjNuZmt3a3IK +``` + + ## Configuration The service is configurated with environment variables. See [all environment varialbes](environment.md). diff --git a/environment.md b/environment.md index cbf2964..66e4928 100644 --- a/environment.md +++ b/environment.md @@ -28,3 +28,4 @@ The Service uses the following environment variables: * `VOTE_DATABASE_PORT`: Port of the postgres database used for long polls. The default is `5432`. * `VOTE_DATABASE_NAME`: Name of the database to save long running polls. The default is `openslides`. * `VOTE_SINGLE_INSTANCE`: More performance if the serice is not scalled horizontally. The default is `false`. +* `VOTE_DECRYPT_SERVICE`: Host and port of the decrypt service. Empty string to disable this feature. The default is ``. diff --git a/go.mod b/go.mod index 6d66e41..125722b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( github.com/OpenSlides/openslides-autoupdate-service v0.4.1-0.20240604145056-1e95f3e54394 + github.com/OpenSlides/vote-decrypt v0.0.0-20240606054437-7931271bc5fd github.com/alecthomas/kong v0.9.0 github.com/gomodule/redigo v1.9.2 github.com/jackc/pgx/v5 v5.6.0 @@ -24,6 +25,7 @@ require ( github.com/goccy/go-yaml v1.11.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/imdario/mergo v0.3.15 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -39,17 +41,20 @@ require ( github.com/opencontainers/runc v1.1.12 // indirect github.com/ostcar/topic v0.4.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.2 // 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/crypto v0.23.0 // indirect - golang.org/x/mod v0.9.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d8a8f82..571e4ac 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,10 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 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/OpenSlides/openslides-autoupdate-service v0.4.1-0.20240215065743-ed5556257712 h1:G/shv5IvqnZ2IYaZxRrd7GqhzLcnp0P8Ya5PQa9BGDM= -github.com/OpenSlides/openslides-autoupdate-service v0.4.1-0.20240215065743-ed5556257712/go.mod h1:FNQVoFM2V+FRoIZgU8q5Z0xJIn3UK3wRQYAG8C5iHvw= github.com/OpenSlides/openslides-autoupdate-service v0.4.1-0.20240604145056-1e95f3e54394 h1:Et8Lm+6/rF9ar9kY63XH573zMg3EwIvqZth7TZujyIU= github.com/OpenSlides/openslides-autoupdate-service v0.4.1-0.20240604145056-1e95f3e54394/go.mod h1:Dzd8R5HY2XdVb35J8gWd1GzvxlLDQ44r3JEanLTElmo= +github.com/OpenSlides/vote-decrypt v0.0.0-20240606054437-7931271bc5fd h1:Xdy3y8NkO2E9Fxzcof8TDtbag0jGrl7h1XqCqT5sgdM= +github.com/OpenSlides/vote-decrypt v0.0.0-20240606054437-7931271bc5fd/go.mod h1:we17wXPczXn2yHbZnU3MNJWlnkt6LFQp4/yPwg+FMBM= 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= @@ -34,6 +34,12 @@ 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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 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/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= @@ -42,10 +48,12 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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= @@ -66,6 +74,8 @@ 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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.10.8 h1:3fdt97i/cwSU83+E0hZTC/Xpc9mTZxc6UWSCRcSbxiE= github.com/lib/pq v1.10.8/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -93,13 +103,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= @@ -112,23 +122,21 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec 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.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 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/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -138,29 +146,30 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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= diff --git a/main.go b/main.go index b9d383c..994bef7 100644 --- a/main.go +++ b/main.go @@ -15,10 +15,14 @@ import ( "github.com/OpenSlides/openslides-vote-service/log" "github.com/OpenSlides/openslides-vote-service/vote" "github.com/OpenSlides/openslides-vote-service/vote/http" + "github.com/OpenSlides/vote-decrypt/grpc" "github.com/alecthomas/kong" ) -var envDebugLog = environment.NewVariable("VOTE_DEBUG_LOG", "false", "Show debug log.") +var ( + envDebugLog = environment.NewVariable("VOTE_DEBUG_LOG", "false", "Show debug log.") + envVoteDecryptService = environment.NewVariable("VOTE_DECRYPT_SERVICE", "", "Host and port of the decrypt service. Empty string to disable this feature.") +) //go:generate sh -c "go run main.go build-doc > environment.md" @@ -120,6 +124,8 @@ func initService(lookup environment.Environmenter) (func(context.Context) error, return nil, fmt.Errorf("init vote backend: %w", err) } + decryptAddr := envVoteDecryptService.Value(lookup) + service := func(ctx context.Context) error { fastBackend, err := fastBackendStarter(ctx) if err != nil { @@ -131,7 +137,17 @@ func initService(lookup environment.Environmenter) (func(context.Context) error, return fmt.Errorf("start long backend: %w", err) } - voteService, voteBackground, err := vote.New(ctx, fastBackend, longBackend, database, singleInstance) + var decrypter vote.Decrypter + if decryptAddr != "" { + decr, close, err := grpc.NewClient(decryptAddr) + if err != nil { + return fmt.Errorf("connection to vote decrypt service via grpc: %w", err) + } + defer close() + decrypter = decr + } + + voteService, voteBackground, err := vote.New(ctx, fastBackend, longBackend, database, singleInstance, decrypter) if err != nil { return fmt.Errorf("starting service: %w", err) } diff --git a/system_test/system_test.go b/system_test/system_test.go index f632243..ee0f2f9 100644 --- a/system_test/system_test.go +++ b/system_test/system_test.go @@ -63,7 +63,7 @@ func TestStartVoteStopClear(t *testing.T) { t.Fatalf("Stop poll: %v", err) } - expectBody := `{"votes":[{"request_user_id":1,"vote_user_id":1,"value":"Y","weight":"1.000000"}],"user_ids":[1]}` + expectBody := `{"votes":"[{\"request_user_id\":1,\"vote_user_id\":1,\"value\":\"Y\",\"weight\":\"1.000000\"}]","user_ids":[1]}` if strings.TrimSpace(string(stopBody)) != expectBody { t.Fatalf("Got != expect\n%s\n%s", stopBody, expectBody) } @@ -115,6 +115,7 @@ func startPoll(ctx context.Context, db *postgresTestData, pollID int) error { is_present_in_meeting_ids: [1] meeting_user_ids: [10] meeting/1/id: 5 + organization/1/url: test `, pollID))) diff --git a/vote/http/http.go b/vote/http/http.go index a698db7..9ed5729 100644 --- a/vote/http/http.go +++ b/vote/http/http.go @@ -93,6 +93,7 @@ type voteService interface { voteCounter voter haveIvoteder + publicKeyer } type authenticater interface { @@ -113,6 +114,7 @@ func registerHandlers(service voteService, auth authenticater, ticketProvider fu mux.Handle(internal+"/clear", handleInternal(handleClear(service))) mux.Handle(internal+"/clear_all", handleInternal(handleClearAll(service))) mux.Handle(internal+"/vote_count", handleInternal(handleVoteCount(service, ticketProvider))) + mux.Handle(internal+"/public_main_key", handleExternal(handlePublicMainKey(service))) mux.Handle(external+"", handleExternal(handleVote(service, auth))) mux.Handle(external+"/voted", handleExternal(handleVoted(service, auth))) mux.Handle(external+"/health", handleExternal(handleHealth())) @@ -121,7 +123,7 @@ func registerHandlers(service voteService, auth authenticater, ticketProvider fu } type starter interface { - Start(ctx context.Context, pollID int) error + Start(ctx context.Context, pollID int) ([]byte, []byte, error) } func handleStart(start starter) HandlerFunc { @@ -134,7 +136,28 @@ func handleStart(start starter) HandlerFunc { return vote.WrapError(vote.ErrInvalid, err) } - return start.Start(r.Context(), id) + pubkey, pubKeySig, err := start.Start(r.Context(), id) + if err != nil { + return err + } + + if pubkey == nil && pubKeySig == nil { + // Early exit for non crypto polls. + return nil + } + + content := struct { + PubKey []byte `json:"public_key"` + PubKeySig []byte `json:"public_key_sig"` + }{ + pubkey, + pubKeySig, + } + if err := json.NewEncoder(w).Encode(content); err != nil { + return fmt.Errorf("encoding and sending objects: %w", err) + } + + return nil } } @@ -159,22 +182,20 @@ func handleStop(stop stopper) HandlerFunc { return err } - // Convert vote objects to json.RawMessage - encodableObjects := make([]json.RawMessage, len(result.Votes)) - for i := range result.Votes { - encodableObjects[i] = result.Votes[i] - } - if result.UserIDs == nil { result.UserIDs = []int{} } out := struct { - Votes []json.RawMessage `json:"votes"` - Users []int `json:"user_ids"` + Votes string `json:"votes"` + Signature []byte `json:"signature,omitempty"` + Users []int `json:"user_ids"` + Invalid map[int]string `json:"invalid,omitempty"` }{ - encodableObjects, + result.Votes, + result.Signature, result.UserIDs, + result.Invalid, } if err := json.NewEncoder(w).Encode(out); err != nil { @@ -344,6 +365,28 @@ func handleVoteCount(voteCounter voteCounter, eventer func() (<-chan time.Time, } } +type publicKeyer interface { + CryptoPublicMainKey(ctx context.Context) ([]byte, error) +} + +func handlePublicMainKey(keyer publicKeyer) HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) error { + log.Info("Receiving public main key request") + w.Header().Set("Content-Type", "application/json") + + key, err := keyer.CryptoPublicMainKey(r.Context()) + if err != nil { + return err + } + + if err := json.NewEncoder(w).Encode(key); err != nil { + return err + } + + return nil + } +} + func handleHealth() HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { w.Header().Set("Content-Type", "application/json") diff --git a/vote/http/http_run_test.go b/vote/http/http_run_test.go index 1d36ff5..d035b4b 100644 --- a/vote/http/http_run_test.go +++ b/vote/http/http_run_test.go @@ -2,6 +2,7 @@ package http_test import ( "context" + "encoding/json" "fmt" "net" "net/http" @@ -45,7 +46,8 @@ func TestRun(t *testing.T) { backend := memory.New() ds := dsmock.NewFlow(nil) - service, _, _ := vote.New(ctx, backend, backend, ds, true) + decrypt := new(decrypterStub) + service, _, _ := vote.New(ctx, backend, backend, ds, true, decrypt) httpServer := votehttp.New(environment.ForTests(map[string]string{"VOTE_PORT": "0"})) if err := httpServer.StartListener(); err != nil { @@ -69,6 +71,7 @@ func TestRun(t *testing.T) { "/internal/vote/clear", "/internal/vote/clear_all", "/internal/vote/vote_count", + "/internal/vote/public_main_key", "/system/vote", "/system/vote/voted", "/system/vote/health", @@ -84,3 +87,39 @@ func TestRun(t *testing.T) { } }) } + +type decrypterStub struct{} + +func (d *decrypterStub) Start(ctx context.Context, pollID string) (pubKey []byte, pubKeySig []byte, err error) { + return nil, nil, nil +} + +func (d *decrypterStub) Stop(ctx context.Context, pollID string, voteList [][]byte) (decryptedContent, signature []byte, err error) { + votes := make([]json.RawMessage, len(voteList)) + for i, vote := range voteList { + votes[i] = vote + } + + content := struct { + ID string `json:"id"` + Votes []json.RawMessage `json:"votes"` + }{ + pollID, + votes, + } + + decryptedContent, err = json.Marshal(content) + if err != nil { + return nil, nil, fmt.Errorf("marshal decrypted content: %w", err) + } + + return decryptedContent, []byte("signature"), nil +} + +func (d *decrypterStub) Clear(ctx context.Context, pollID string) error { + return nil +} + +func (d *decrypterStub) PublicMainKey(ctx context.Context) ([]byte, error) { + return []byte("pub_main_key"), nil +} diff --git a/vote/http/http_test.go b/vote/http/http_test.go index 2f3d36b..4937649 100644 --- a/vote/http/http_test.go +++ b/vote/http/http_test.go @@ -20,9 +20,9 @@ type starterStub struct { expectErr error } -func (c *starterStub) Start(ctx context.Context, pollID int) error { +func (c *starterStub) Start(ctx context.Context, pollID int) ([]byte, []byte, error) { c.id = pollID - return c.expectErr + return nil, nil, c.expectErr } func TestHandleStart(t *testing.T) { @@ -118,8 +118,9 @@ type stopperStub struct { id int expectErr error - expectedVotes [][]byte - expectedUserIDs []int + expectedVotes string + expectedSignature []byte + expectedUserIDs []int } func (s *stopperStub) Stop(ctx context.Context, pollID int) (vote.StopResult, error) { @@ -130,8 +131,9 @@ func (s *stopperStub) Stop(ctx context.Context, pollID int) (vote.StopResult, er } return vote.StopResult{ - Votes: s.expectedVotes, - UserIDs: s.expectedUserIDs, + Votes: s.expectedVotes, + Signature: s.expectedSignature, + UserIDs: s.expectedUserIDs, }, nil } @@ -151,7 +153,7 @@ func TestHandleStop(t *testing.T) { }) t.Run("Valid", func(t *testing.T) { - stopper.expectedVotes = [][]byte{[]byte(`"some values"`)} + stopper.expectedVotes = `["some values"]` resp := httptest.NewRecorder() mux.ServeHTTP(resp, httptest.NewRequest("POST", url+"?id=1", nil)) @@ -164,7 +166,7 @@ func TestHandleStop(t *testing.T) { t.Errorf("Stopper was called with id %d, expected 1", stopper.id) } - expect := `{"votes":["some values"],"user_ids":[]}` + expect := `{"votes":"[\"some values\"]","user_ids":[]}` if trimed := strings.TrimSpace(resp.Body.String()); trimed != expect { t.Errorf("Got body:\n`%s`, expected:\n`%s`", trimed, expect) } diff --git a/vote/mock_test.go b/vote/mock_test.go index 4aad81f..463b0cd 100644 --- a/vote/mock_test.go +++ b/vote/mock_test.go @@ -2,6 +2,9 @@ package vote_test import ( "context" + "encoding/json" + "fmt" + "net/http" "testing" "github.com/OpenSlides/openslides-autoupdate-service/pkg/datastore/dskey" @@ -39,3 +42,51 @@ func (g *StubGetter) assertKeys(t *testing.T, keys ...dskey.Key) { } } } + +type decrypterStub struct{} + +func (d *decrypterStub) Start(ctx context.Context, pollID string) (pubKey []byte, pubKeySig []byte, err error) { + return nil, nil, nil +} + +func (d *decrypterStub) Stop(ctx context.Context, pollID string, voteList [][]byte) (decryptedContent, signature []byte, err error) { + votes := make([]json.RawMessage, len(voteList)) + for i, vote := range voteList { + votes[i] = vote + } + + content := struct { + ID string `json:"id"` + Votes []json.RawMessage `json:"votes"` + }{ + pollID, + votes, + } + + decryptedContent, err = json.Marshal(content) + if err != nil { + return nil, nil, fmt.Errorf("marshal decrypted content: %w", err) + } + + return decryptedContent, []byte("signature"), nil +} + +func (d *decrypterStub) Clear(ctx context.Context, pollID string) error { + return nil +} + +func (d *decrypterStub) PublicMainKey(ctx context.Context) ([]byte, error) { + return []byte("pub_main_key"), nil +} + +type autherStub struct { + userID int +} + +func (a *autherStub) Authenticate(w http.ResponseWriter, r *http.Request) (context.Context, error) { + return r.Context(), nil +} + +func (a *autherStub) FromContext(context.Context) int { + return a.userID +} diff --git a/vote/vote.go b/vote/vote.go index e9ba550..1461341 100644 --- a/vote/vote.go +++ b/vote/vote.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "net/url" "sync" "time" @@ -16,6 +17,14 @@ import ( "github.com/OpenSlides/openslides-vote-service/log" ) +// Decrypter decryptes the incomming votes. +type Decrypter interface { + Start(ctx context.Context, pollID string) (pubKey []byte, pubKeySig []byte, err error) + Stop(ctx context.Context, pollID string, voteList [][]byte) (decryptedContent, signature []byte, err error) + Clear(ctx context.Context, pollID string) error + PublicMainKey(ctx context.Context) ([]byte, error) +} + // Vote holds the state of the service. // // Vote has to be initializes with vote.New(). @@ -24,16 +33,18 @@ type Vote struct { longBackend Backend flow flow.Flow - votedMu sync.Mutex - voted map[int][]int // voted holds for all running polls, which user ids have already voted. + decrypter Decrypter + votedMu sync.Mutex + voted map[int][]int // voted holds for all running polls, which user ids have already voted. } // New creates an initializes vote service. -func New(ctx context.Context, fast, long Backend, flow flow.Flow, singleInstance bool) (*Vote, func(context.Context, func(error)), error) { +func New(ctx context.Context, fast, long Backend, flow flow.Flow, singleInstance bool, decrypter Decrypter) (*Vote, func(context.Context, func(error)), error) { v := &Vote{ fastBackend: fast, longBackend: long, flow: flow, + decrypter: decrypter, } if err := v.loadVoted(ctx); err != nil { @@ -70,41 +81,77 @@ func (v *Vote) backend(p pollConfig) Backend { return backend } +func (v *Vote) qualifiedID(ctx context.Context, fetch *dsfetch.Fetch, id int) (string, error) { + rawURL, err := fetch.Organization_Url(1).Value(ctx) + if err != nil { + return "", fmt.Errorf("getting organization url: %v", err) + } + + parsed, err := url.Parse(rawURL) + if err != nil { + return "", fmt.Errorf("invalid url %s: %w", rawURL, err) + } + + return fmt.Sprintf("%s/%d", parsed.Hostname(), id), nil +} + // Start an electronic vote. // // This function is idempotence. If you call it with the same input, you will // get the same output. This means, that when a poll is stopped, Start() will // not throw an error. -func (v *Vote) Start(ctx context.Context, pollID int) error { +func (v *Vote) Start(ctx context.Context, pollID int) (pubkey, pubKeySig []byte, err error) { recorder := dsrecorder.New(v.flow) ds := dsfetch.New(recorder) poll, err := loadPoll(ctx, ds, pollID) if err != nil { - return fmt.Errorf("loading poll: %w", err) + return nil, nil, fmt.Errorf("loading poll: %w", err) } if poll.ptype == "analog" { - return MessageError(ErrInvalid, "Analog poll can not be started") + return nil, nil, MessageError(ErrInvalid, "Analog poll can not be started") } if err := poll.preload(ctx, ds); err != nil { - return fmt.Errorf("preloading data: %w", err) + return nil, nil, fmt.Errorf("preloading data: %w", err) } log.Debug("Preload cache. Received keys: %v", recorder.Keys()) backend := v.backend(poll) if err := backend.Start(ctx, pollID); err != nil { - return fmt.Errorf("starting poll in the backend: %w", err) + return nil, nil, fmt.Errorf("starting poll in the backend: %w", err) } - return nil + if poll.ptype != "cryptographic" { + return nil, nil, nil + } + + defer func() { + if err != nil { + backend.Clear(ctx, pollID) + } + }() + + qid, err := v.qualifiedID(ctx, ds, pollID) + if err != nil { + return nil, nil, fmt.Errorf("building qualified id: %w", err) + } + + pubkey, pubKeySig, err = v.decrypter.Start(ctx, qid) + if err != nil { + return nil, nil, fmt.Errorf("starting poll in decrypter: %w", err) + } + + return pubkey, pubKeySig, nil } // StopResult is the return value from vote.Stop. type StopResult struct { - Votes [][]byte - UserIDs []int + Votes string + Signature []byte + UserIDs []int + Invalid map[int]string } // Stop ends a poll. @@ -129,7 +176,70 @@ func (v *Vote) Stop(ctx context.Context, pollID int) (StopResult, error) { return StopResult{}, fmt.Errorf("fetching vote objects: %w", err) } - return StopResult{ballots, userIDs}, nil + switch poll.ptype { + case "cryptographic": + return v.stopCrypto(ctx, poll, ds, ballots, userIDs) + default: + return v.stopNonCrypt(ballots, userIDs) + } +} + +func (v *Vote) stopNonCrypt(ballots [][]byte, userIDs []int) (StopResult, error) { + encodable := make([]json.RawMessage, len(ballots)) + for i := range ballots { + encodable[i] = ballots[i] + } + + votes, err := json.Marshal(encodable) + if err != nil { + return StopResult{}, fmt.Errorf("encode votes to list: %w", err) + } + + return StopResult{Votes: string(votes), UserIDs: userIDs}, nil +} + +func (v *Vote) stopCrypto(ctx context.Context, poll pollConfig, ds *dsfetch.Fetch, ballots [][]byte, userIDs []int) (StopResult, error) { + qid, err := v.qualifiedID(ctx, ds, poll.id) + if err != nil { + return StopResult{}, fmt.Errorf("building qualified id: %w", err) + } + + voteValue := make([][]byte, len(ballots)) + for i := range ballots { + // This uses the type `[]byte` to decode a base64 value. + var vote struct { + Value []byte `json:"value"` + } + if err := json.Unmarshal(ballots[i], &vote); err != nil { + return StopResult{}, fmt.Errorf("decoding stored vote: %w", err) + } + + voteValue[i] = vote.Value + } + + decrypted, signature, err := v.decrypter.Stop(ctx, qid, voteValue) + if err != nil { + return StopResult{}, fmt.Errorf("decrypting votes: %w", err) + } + + var decryptedContent struct { + ID string `json:"id"` + Votes []struct { + Votes ballotValue `json:"votes"` + } `json:"votes"` + } + if err := json.Unmarshal(decrypted, &decryptedContent); err != nil { + return StopResult{}, fmt.Errorf("encoding decrypted votes: %w", err) + } + + invalid := make(map[int]string) + for i, vote := range decryptedContent.Votes { + if validation := validate(poll, vote.Votes); validation != "" { + invalid[i] = validation + } + } + + return StopResult{Votes: string(decrypted), Signature: signature, UserIDs: userIDs, Invalid: invalid}, nil } // Clear removes all knowlage of a poll. @@ -142,6 +252,20 @@ func (v *Vote) Clear(ctx context.Context, pollID int) error { return fmt.Errorf("clearing longBackend: %w", err) } + ds := dsfetch.New(v.flow) + qid, err := v.qualifiedID(ctx, ds, pollID) + if err != nil { + return fmt.Errorf("building qualified id: %w", err) + } + + if v.decrypter == nil { + return nil + } + + if err := v.decrypter.Clear(ctx, qid); err != nil { + return fmt.Errorf("clearing decrypter: %w", err) + } + v.votedMu.Lock() v.voted[pollID] = nil v.votedMu.Unlock() @@ -150,6 +274,8 @@ func (v *Vote) Clear(ctx context.Context, pollID int) error { } // ClearAll removes all knowlage of all polls and the datastore-cache. +// +// This does not work for the vote decrypter. func (v *Vote) ClearAll(ctx context.Context) error { // Reset the cache if it has the ResetCach() method. type ResetCacher interface { @@ -214,41 +340,43 @@ func (v *Vote) Vote(ctx context.Context, pollID, requestUser int, r io.Reader) e return err } - if validation := validate(poll, vote.Value); validation != "" { - return MessageError(ErrInvalid, validation) - } + var voteWeight string + if poll.ptype != "cryptographic" { + if validation := validate(poll, vote.Value); validation != "" { + return MessageError(ErrInvalid, validation) + } - // voteData.Weight is a DecimalField with 6 zeros. - var voteWeightEnabled bool - var meetingUserVoteWeight string - var userDefaultVoteWeight string - ds.Meeting_UsersEnableVoteWeight(poll.meetingID).Lazy(&voteWeightEnabled) - ds.MeetingUser_VoteWeight(voteMeetingUserID).Lazy(&meetingUserVoteWeight) - ds.User_DefaultVoteWeight(voteUser).Lazy(&userDefaultVoteWeight) + // voteData.Weight is a DecimalField with 6 zeros. + var voteWeightEnabled bool + var meetingUserVoteWeight string + var userDefaultVoteWeight string + ds.Meeting_UsersEnableVoteWeight(poll.meetingID).Lazy(&voteWeightEnabled) + ds.MeetingUser_VoteWeight(voteMeetingUserID).Lazy(&meetingUserVoteWeight) + ds.User_DefaultVoteWeight(voteUser).Lazy(&userDefaultVoteWeight) - if err := ds.Execute(ctx); err != nil { - return fmt.Errorf("getting vote weight: %w", err) - } + if err := ds.Execute(ctx); err != nil { + return fmt.Errorf("getting vote weight: %w", err) + } + + if voteWeightEnabled { + voteWeight = meetingUserVoteWeight + if voteWeight == "" { + voteWeight = userDefaultVoteWeight + } + } - var voteWeight string - if voteWeightEnabled { - voteWeight = meetingUserVoteWeight if voteWeight == "" { - voteWeight = userDefaultVoteWeight + voteWeight = "1.000000" } - } - if voteWeight == "" { - voteWeight = "1.000000" + log.Debug("Using voteWeight %s", voteWeight) } - log.Debug("Using voteWeight %s", voteWeight) - voteData := struct { RequestUser int `json:"request_user_id,omitempty"` VoteUser int `json:"vote_user_id,omitempty"` Value json.RawMessage `json:"value"` - Weight string `json:"weight"` + Weight string `json:"weight,omitempty"` }{ requestUser, voteUser, @@ -494,6 +622,15 @@ func (v *Vote) loadVoted(ctx context.Context) error { return nil } +// CryptoPublicMainKey returns the public main key from vote-decrypt. +func (v *Vote) CryptoPublicMainKey(ctx context.Context) ([]byte, error) { + if v.decrypter == nil { + return nil, fmt.Errorf("decrypt service is not configured") + } + + return v.decrypter.PublicMainKey(ctx) +} + // Backend is a storage for the poll options. type Backend interface { // Start opens the poll for votes. To start a poll that is already started diff --git a/vote/vote_test.go b/vote/vote_test.go index e83e714..737ec8e 100644 --- a/vote/vote_test.go +++ b/vote/vote_test.go @@ -2,8 +2,10 @@ package vote_test import ( "context" + "encoding/base64" "encoding/json" "errors" + "fmt" "reflect" "strings" "testing" @@ -20,9 +22,9 @@ func TestVoteStart(t *testing.T) { t.Run("Unknown poll", func(t *testing.T) { backend := memory.New() ds := dsmock.NewFlow(dsmock.YAMLData("")) - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) - err := v.Start(ctx, 1) + _, _, err := v.Start(ctx, 1) if !errors.Is(err, vote.ErrNotExists) { t.Errorf("Start returned unexpected error: %v", err) } @@ -47,13 +49,13 @@ func TestVoteStart(t *testing.T) { ) counter := ds.Middlewares()[0].(*dsmock.Counter) - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) - if err := v.Start(ctx, 1); err != nil { + if _, _, err := v.Start(ctx, 1); err != nil { t.Errorf("Start returned unexpected error: %v", err) } - if c := counter.Count(); c > 2 { + if c := counter.Count(); c > 3 { t.Errorf("Start used %d requests to the datastore, expected max 2: %v", c, counter.Requests()) } @@ -66,6 +68,7 @@ func TestVoteStart(t *testing.T) { t.Run("Start poll a second time", func(t *testing.T) { backend := memory.New() ds := &StubGetter{data: dsmock.YAMLData(` + organization/1/url: test.com poll: 1: meeting_id: 5 @@ -77,10 +80,10 @@ func TestVoteStart(t *testing.T) { user/1/is_present_in_meeting_ids: [1] meeting/5/id: 5 `)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) v.Start(ctx, 1) - if err := v.Start(ctx, 1); err != nil { + if _, _, err := v.Start(ctx, 1); err != nil { t.Errorf("Start returned unexpected error: %v", err) } }) @@ -88,6 +91,7 @@ func TestVoteStart(t *testing.T) { t.Run("Start a stopped poll", func(t *testing.T) { backend := memory.New() ds := &StubGetter{data: dsmock.YAMLData(` + organization/1/url: test.com poll: 1: meeting_id: 5 @@ -99,14 +103,14 @@ func TestVoteStart(t *testing.T) { user/1/is_present_in_meeting_ids: [1] meeting/5/id: 5 `)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) v.Start(ctx, 1) if _, _, err := backend.Stop(ctx, 1); err != nil { t.Fatalf("Stop returned unexpected error: %v", err) } - if err := v.Start(ctx, 1); err != nil { + if _, _, err := v.Start(ctx, 1); err != nil { t.Errorf("Start returned unexpected error: %v", err) } }) @@ -124,9 +128,9 @@ func TestVoteStart(t *testing.T) { user/1/is_present_in_meeting_ids: [1] `)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) - err := v.Start(ctx, 1) + _, _, err := v.Start(ctx, 1) if err == nil { t.Errorf("Got no error, expected `Some error`") @@ -136,6 +140,7 @@ func TestVoteStart(t *testing.T) { t.Run("Start an poll in `wrong` state", func(t *testing.T) { backend := memory.New() ds := &StubGetter{data: dsmock.YAMLData(` + organization/1/url: test.com poll: 1: meeting_id: 5 @@ -147,9 +152,9 @@ func TestVoteStart(t *testing.T) { user/1/is_present_in_meeting_ids: [1] meeting/5/id: 5 `)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) - err := v.Start(ctx, 1) + _, _, err := v.Start(ctx, 1) if err != nil { t.Errorf("Start returned: %v", err) } @@ -168,9 +173,9 @@ func TestVoteStart(t *testing.T) { user/1/is_present_in_meeting_ids: [1] `)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) - err := v.Start(ctx, 1) + _, _, err := v.Start(ctx, 1) if err == nil { t.Errorf("Got no error, expected `Some error`") @@ -190,9 +195,9 @@ func TestVoteStart(t *testing.T) { user/1/is_present_in_meeting_ids: [1] `)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) - err := v.Start(ctx, 1) + _, _, err := v.Start(ctx, 1) if err == nil { t.Errorf("Got no error, expected `Some error`") @@ -204,8 +209,8 @@ func TestVoteStartDSError(t *testing.T) { ctx := context.Background() backend := memory.New() ds := &StubGetter{err: errors.New("Some error")} - v, _, _ := vote.New(ctx, backend, backend, ds, true) - err := v.Start(ctx, 1) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) + _, _, err := v.Start(ctx, 1) if err == nil { t.Errorf("Got no error, expected `Some error`") @@ -215,8 +220,8 @@ func TestVoteStartDSError(t *testing.T) { func TestVoteStop(t *testing.T) { ctx := context.Background() backend := memory.New() - ds := &StubGetter{data: dsmock.YAMLData(` + organization/1/url: test.com poll: 1: meeting_id: 1 @@ -235,7 +240,7 @@ func TestVoteStop(t *testing.T) { pollmethod: Y `)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) t.Run("Unknown poll", func(t *testing.T) { _, err := v.Stop(ctx, 404) @@ -256,21 +261,24 @@ func TestVoteStop(t *testing.T) { t.Fatalf("Start returned an unexpected error: %v", err) } - backend.Vote(ctx, 2, 1, []byte(`"polldata1"`)) - backend.Vote(ctx, 2, 2, []byte(`"polldata2"`)) + backend.Vote(ctx, 2, 1, []byte(`{"value":"polldata1"}`)) + backend.Vote(ctx, 2, 2, []byte(`{"value":"polldata2"}`)) - result, err := v.Stop(ctx, 2) + stopResult, err := v.Stop(ctx, 2) if err != nil { t.Fatalf("Stop returned unexpected error: %v", err) } - expect := [][]byte{[]byte(`"polldata1"`), []byte(`"polldata2"`)} - if !reflect.DeepEqual(result.Votes, expect) { - t.Errorf("Got:\n`%s`, expected\n`%s`", result.Votes, expect) + votes := stopResult.Votes + userIDs := stopResult.UserIDs + + expected := `[{"value":"polldata1"},{"value":"polldata2"}]` + if votes != expected { + t.Errorf("Got\n`%s`, expected\n`%s`", votes, expected) } - if !reflect.DeepEqual(result.UserIDs, []int{1, 2}) { - t.Errorf("Got users %s, expected [1 2]", result.Votes) + if !reflect.DeepEqual(userIDs, []int{1, 2}) { + t.Errorf("Got users %v, expected [1 2]", userIDs) } err = backend.Vote(ctx, 2, 3, []byte(`"polldata3"`)) @@ -285,25 +293,113 @@ func TestVoteStop(t *testing.T) { t.Fatalf("Start: %v", err) } - result, err := v.Stop(ctx, 3) + stopResult, err := v.Stop(ctx, 3) if err != nil { - t.Fatalf("Stop: %v", err) + t.Fatalf("Stop returned unexpected error: %v", err) } - if len(result.Votes) != 0 { - t.Errorf("Got votes %v, expected []", result.Votes) + votes := stopResult.Votes + userIDs := stopResult.UserIDs + + if votes != `[]` { + t.Errorf("Got votes `%s`, expected `[]`", votes) } - if len(result.UserIDs) != 0 { - t.Errorf("Got userIDs %v, expected []", result.UserIDs) + if len(userIDs) != 0 { + t.Errorf("Got userIDs %v, expected []", userIDs) } }) } +func TestVoteStopCrypto(t *testing.T) { + ctx := context.Background() + ds := &StubGetter{data: dsmock.YAMLData(` + organization/1/url: test.com + poll/1: + meeting_id: 1 + type: cryptographic + pollmethod: YN + global_yes: true + global_no: true + backend: fast + `)} + backend := memory.New() + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) + + if err := backend.Start(ctx, 1); err != nil { + t.Fatalf("Start returned an unexpected error: %v", err) + } + + polldata1 := base64.StdEncoding.EncodeToString([]byte(`{"votes":"Y"}`)) + polldata2 := base64.StdEncoding.EncodeToString([]byte(`{"votes":"N"}`)) + + backend.Vote(ctx, 1, 1, []byte(fmt.Sprintf(`{"value":"%s"}`, polldata1))) + backend.Vote(ctx, 1, 2, []byte(fmt.Sprintf(`{"value":"%s"}`, polldata2))) + + stopResult, err := v.Stop(ctx, 1) + if err != nil { + t.Fatalf("Stop returned unexpected error: %v", err) + } + + expected := vote.StopResult{ + Votes: `{"id":"/1","votes":[{"votes":"Y"},{"votes":"N"}]}`, + Signature: []byte("signature"), + UserIDs: []int{1, 2}, + Invalid: map[int]string{}, + } + + if !reflect.DeepEqual(stopResult, expected) { + t.Errorf("\nGot\t\t\t%v\nexpected\t%v (result.Votes: %s)", stopResult, expected, stopResult.Votes) + } +} + +func TestVoteStopCryptoInvalid(t *testing.T) { + ctx := context.Background() + ds := &StubGetter{data: dsmock.YAMLData(` + organization/1/url: test.com + poll/1: + meeting_id: 1 + type: cryptographic + pollmethod: YN + global_yes: true + global_no: true + backend: fast + `)} + backend := memory.New() + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) + + if err := backend.Start(ctx, 1); err != nil { + t.Fatalf("Start returned an unexpected error: %v", err) + } + + polldata1 := base64.StdEncoding.EncodeToString([]byte(`{"votes":"Y"}`)) + polldata2 := base64.StdEncoding.EncodeToString([]byte(`{"votes":"Invalid"}`)) + + backend.Vote(ctx, 1, 1, []byte(fmt.Sprintf(`{"value":"%s"}`, polldata1))) + backend.Vote(ctx, 1, 2, []byte(fmt.Sprintf(`{"value":"%s"}`, polldata2))) + + stopResult, err := v.Stop(ctx, 1) + if err != nil { + t.Fatalf("Stop returned unexpected error: %v", err) + } + + expected := vote.StopResult{ + Votes: `{"id":"/1","votes":[{"votes":"Y"},{"votes":"Invalid"}]}`, + Signature: []byte("signature"), + UserIDs: []int{1, 2}, + Invalid: map[int]string{1: "Global vote Invalid is not enabled"}, + } + + if !reflect.DeepEqual(stopResult, expected) { + t.Errorf("\nGot\t\t\t%v\nexpected\t%v (result.Votes: %s)", stopResult, expected, stopResult.Votes) + } +} + func TestVoteClear(t *testing.T) { ctx := context.Background() + ds := &StubGetter{data: dsmock.YAMLData(`organization/1/url: test.com`)} backend := memory.New() - v, _, _ := vote.New(ctx, backend, backend, &StubGetter{}, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) if err := v.Clear(ctx, 1); err != nil { t.Fatalf("Clear returned unexpected error: %v", err) @@ -313,7 +409,7 @@ func TestVoteClear(t *testing.T) { func TestVoteClearAll(t *testing.T) { ctx := context.Background() backend := memory.New() - v, _, _ := vote.New(ctx, backend, backend, &StubGetter{}, true) + v, _, _ := vote.New(ctx, backend, backend, &StubGetter{}, true, new(decrypterStub)) if err := v.ClearAll(ctx); err != nil { t.Fatalf("ClearAll returned unexpected error: %v", err) @@ -345,7 +441,8 @@ func TestVoteVote(t *testing.T) { meeting_id: 1 `), } - v, _, _ := vote.New(ctx, backend, backend, ds, true) + + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) t.Run("Poll does not exist in DS", func(t *testing.T) { err := v.Vote(ctx, 404, 1, strings.NewReader(`{"value":"Y"}`)) @@ -380,19 +477,6 @@ func TestVoteVote(t *testing.T) { } }) - t.Run("Invalid format", func(t *testing.T) { - err := v.Vote(ctx, 1, 1, strings.NewReader(`{}`)) - - var errTyped vote.TypeError - if !errors.As(err, &errTyped) { - t.Fatalf("Vote() did not return an TypeError, got: %v", err) - } - - if errTyped != vote.ErrInvalid { - t.Errorf("Got error type `%s`, expected `%s`", errTyped.Type(), vote.ErrInvalid.Type()) - } - }) - t.Run("Valid data", func(t *testing.T) { err := v.Vote(ctx, 1, 1, strings.NewReader(`{"value":"Y"}`)) if err != nil { @@ -448,6 +532,7 @@ func TestVoteNoRequests(t *testing.T) { { "normal vote", `--- + organization/1/url: test.com poll/1: meeting_id: 50 entitled_group_ids: [5] @@ -475,6 +560,7 @@ func TestVoteNoRequests(t *testing.T) { { "delegation vote", `--- + organization/1/url: test.com poll/1: meeting_id: 50 entitled_group_ids: [5] @@ -512,6 +598,7 @@ func TestVoteNoRequests(t *testing.T) { { "vote weight enabled", `--- + organization/1/url: test.com poll/1: meeting_id: 50 entitled_group_ids: [5] @@ -543,6 +630,7 @@ func TestVoteNoRequests(t *testing.T) { { "vote weight enabled and delegated", `--- + organization/1/url: test.com poll/1: meeting_id: 50 entitled_group_ids: [5] @@ -589,9 +677,9 @@ func TestVoteNoRequests(t *testing.T) { counter := ds.Middlewares()[0].(*dsmock.Counter) cachedDS := cache.New(ds) backend := memory.New() - v, _, _ := vote.New(ctx, backend, backend, cachedDS, true) + v, _, _ := vote.New(ctx, backend, backend, cachedDS, true, new(decrypterStub)) - if err := v.Start(ctx, 1); err != nil { + if _, _, err := v.Start(ctx, 1); err != nil { t.Fatalf("Can not start poll: %v", err) } @@ -959,7 +1047,7 @@ func TestVoteDelegationAndGroup(t *testing.T) { backend := memory.New() ds := &StubGetter{data: dsmock.YAMLData(tt.data)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) if err := backend.Start(ctx, 1); err != nil { t.Fatalf("backend.Start(): %v", err) @@ -1113,7 +1201,7 @@ func TestVoteWeight(t *testing.T) { ctx := context.Background() backend := memory.New() ds := &StubGetter{data: dsmock.YAMLData(tt.data)} - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) if err := backend.Start(ctx, 1); err != nil { t.Fatalf("bakckend.Start: %v", err) @@ -1203,7 +1291,7 @@ func TestItLikeBackend(t *testing.T) { `)) - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) if err := backend.Start(ctx, 1); err != nil { t.Fatalf("bakckend.Start: %v", err) } @@ -1228,11 +1316,10 @@ func TestVotedPolls(t *testing.T) { user/5/id: 5 `)) - backend.Start(ctx, 1) backend.Vote(ctx, 1, 5, []byte(`"Y"`)) - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) got, err := v.Voted(ctx, []int{1, 2}, 5) if err != nil { @@ -1267,12 +1354,11 @@ func TestVotedPollsWithDelegation(t *testing.T) { user_id: 7 `)) - backend.Start(ctx, 1) backend.Vote(ctx, 1, 5, []byte(`"Y"`)) backend.Vote(ctx, 1, 6, []byte(`"Y"`)) backend.Vote(ctx, 1, 7, []byte(`"Y"`)) - v, _, _ := vote.New(ctx, backend, backend, ds, true) + v, _, _ := vote.New(ctx, backend, backend, ds, true, new(decrypterStub)) got, err := v.Voted(ctx, []int{1, 2}, 5) if err != nil { @@ -1296,7 +1382,7 @@ func TestVoteCount(t *testing.T) { backend2.Vote(ctx, 42, 2, []byte("vote")) ds := dsmock.NewFlow(dsmock.YAMLData(``)) - v, _, _ := vote.New(ctx, backend1, backend2, ds, true) + v, _, _ := vote.New(ctx, backend1, backend2, ds, true, new(decrypterStub)) count := v.VoteCount(ctx) diff --git a/vote/vote_validate_test.go b/vote/vote_validate_test.go index 87a982d..32f3113 100644 --- a/vote/vote_validate_test.go +++ b/vote/vote_validate_test.go @@ -303,7 +303,7 @@ func TestVoteValidate(t *testing.T) { if tt.expectValid { if validation != "" { - t.Fatalf("Validate returned unexpected message: %v", validation) + t.Fatalf("Validate returned unexpected message: %s", validation) } return }