Skip to content

Commit

Permalink
api: support encrypted metadata
Browse files Browse the repository at this point in the history
Signed-off-by: p4u <[email protected]>
  • Loading branch information
p4u committed May 7, 2024
1 parent 6bd8def commit 3fec585
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 33 deletions.
25 changes: 13 additions & 12 deletions api/api_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ type ElectionResults struct {

type Election struct {
ElectionSummary
Census *ElectionCensus `json:"census,omitempty"`
MetadataURL string `json:"metadataURL"`
CreationTime time.Time `json:"creationTime"`
VoteMode VoteMode `json:"voteMode,omitempty"`
ElectionMode ElectionMode `json:"electionMode,omitempty"`
TallyMode TallyMode `json:"tallyMode,omitempty"`
Metadata *ElectionMetadata `json:"metadata,omitempty"`
Census *ElectionCensus `json:"census,omitempty"`
MetadataURL string `json:"metadataURL"`
CreationTime time.Time `json:"creationTime"`
VoteMode VoteMode `json:"voteMode,omitempty"`
ElectionMode ElectionMode `json:"electionMode,omitempty"`
TallyMode TallyMode `json:"tallyMode,omitempty"`
Metadata any `json:"metadata,omitempty"`
}

type ElectionKeys struct {
Expand All @@ -79,11 +79,12 @@ type ElectionCensus struct {
}

type ElectionCreate struct {
TxPayload []byte `json:"txPayload,omitempty"`
Metadata []byte `json:"metadata,omitempty"`
TxHash types.HexBytes `json:"txHash" `
ElectionID types.HexBytes `json:"electionID" `
MetadataURL string `json:"metadataURL"`
TxPayload []byte `json:"txPayload,omitempty"`
Metadata []byte `json:"metadata,omitempty"`
TxHash types.HexBytes `json:"txHash" `
ElectionID types.HexBytes `json:"electionID" `
MetadataURL string `json:"metadataURL"`
MetadataEncryptionPrivKey types.HexBytes `json:"metadataEncryptionPrivKey,omitempty"`
}

type ElectionDescription struct {
Expand Down
38 changes: 25 additions & 13 deletions api/elections.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,17 @@ func (a *API) electionHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) e
if err != nil {
log.Warnf("cannot get metadata from %s: %v", election.MetadataURL, err)
} else {
electionMetadata := ElectionMetadata{}
if err := json.Unmarshal(metadataBytes, &electionMetadata); err != nil {
log.Warnf("cannot unmarshal metadata from %s: %v", election.MetadataURL, err)
// if metadata exists, add it to the election
// if the metadata is not encrypted, unmarshal it, otherwise store it as bytes
if !election.ElectionMode.EncryptedMetaData {
electionMetadata := ElectionMetadata{}
if err := json.Unmarshal(metadataBytes, &electionMetadata); err != nil {
log.Warnf("cannot unmarshal metadata from %s: %v", election.MetadataURL, err)
}
election.Metadata = &electionMetadata
} else {
election.Metadata = metadataBytes
}
election.Metadata = &electionMetadata
}
}
data, err := json.Marshal(election)
Expand Down Expand Up @@ -449,21 +455,25 @@ func (a *API) electionCreateHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCo
}

// check if the transaction is of the correct type and extract metadata URI
metadataURI, err := func() (string, error) {
metadataURI, isEncryptedMetadata, err := func() (string, bool, error) {
stx := &models.SignedTx{}
if err := proto.Unmarshal(req.TxPayload, stx); err != nil {
return "", err
return "", false, err
}
tx := &models.Tx{}
if err := proto.Unmarshal(stx.GetTx(), tx); err != nil {
return "", err
return "", false, err
}
if np := tx.GetNewProcess(); np != nil {
if p := np.GetProcess(); p != nil {
return p.GetMetadata(), nil
encryptedMeta := false
if p.GetMode() != nil {
encryptedMeta = p.GetMode().EncryptedMetaData
}
return p.GetMetadata(), encryptedMeta, nil
}
}
return "", ErrCantExtractMetadataURI
return "", false, ErrCantExtractMetadataURI
}()
if err != nil {
return err
Expand All @@ -478,10 +488,12 @@ func (a *API) electionCreateHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCo

var metadataCID string
if req.Metadata != nil {
// if election metadata defined, check the format
metadata := ElectionMetadata{}
if err := json.Unmarshal(req.Metadata, &metadata); err != nil {
return ErrCantParseMetadataAsJSON.WithErr(err)
// if election metadata defined and not encrypted, check the format
if !isEncryptedMetadata {
metadata := ElectionMetadata{}
if err := json.Unmarshal(req.Metadata, &metadata); err != nil {
return ErrCantParseMetadataAsJSON.WithErr(err)
}
}

// set metadataCID from metadata bytes
Expand Down
87 changes: 79 additions & 8 deletions test/api_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package test

import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"math/rand"
"testing"
"time"

qt "github.com/frankban/quicktest"
"github.com/google/uuid"
"go.vocdoni.io/dvote/api"
"go.vocdoni.io/dvote/crypto/ethereum"
"go.vocdoni.io/dvote/crypto/nacl"
"go.vocdoni.io/dvote/data/ipfs"
"go.vocdoni.io/dvote/test/testcommon"
"go.vocdoni.io/dvote/test/testcommon/testutil"
Expand Down Expand Up @@ -82,7 +85,7 @@ func TestAPIcensusAndVote(t *testing.T) {
qt.Assert(t, censusData.Weight.String(), qt.Equals, "1")

electionParams := electionprice.ElectionParameters{ElectionDuration: 100, MaxCensusSize: 100}
election := createElection(t, c, server.Account, electionParams, censusData.CensusRoot, 0, server.VochainAPP.ChainID())
election := createElection(t, c, server.Account, electionParams, censusData.CensusRoot, 0, server.VochainAPP.ChainID(), false)

// Block 2
server.VochainAPP.AdvanceTestBlock()
Expand Down Expand Up @@ -437,7 +440,7 @@ func runAPIElectionCostWithParams(t *testing.T,
qt.Assert(t, requestAccount(t, c, signer.Address().String()).Balance,
qt.Equals, initialBalance)

createElection(t, c, signer, electionParams, censusRoot, startBlock, server.VochainAPP.ChainID())
createElection(t, c, signer, electionParams, censusRoot, startBlock, server.VochainAPP.ChainID(), false)

// Block 3
server.VochainAPP.AdvanceTestBlock()
Expand Down Expand Up @@ -502,13 +505,22 @@ func createElection(t testing.TB, c *testutil.TestHTTPclient,
censusRoot types.HexBytes,
startBlock uint32,
chainID string,
encryptedMetadata bool,
) api.ElectionCreate {
metadataBytes, err := json.Marshal(
&api.ElectionMetadata{
Title: map[string]string{"default": "test election"},
Description: map[string]string{"default": "test election description"},
Version: "1.0",
})
var metadataEncryptionKey []byte
if encryptedMetadata {
sk, err := nacl.Generate(rand.New(rand.NewSource(1)))
qt.Assert(t, err, qt.IsNil)
metadataBytes, err = nacl.Anonymous.Encrypt(metadataBytes, sk.Public())
qt.Assert(t, err, qt.IsNil)
metadataEncryptionKey = sk.Bytes()
}

qt.Assert(t, err, qt.IsNil)
metadataURI := ipfs.CalculateCIDv1json(metadataBytes)
Expand All @@ -523,7 +535,7 @@ func createElection(t testing.TB, c *testutil.TestHTTPclient,
Status: models.ProcessStatus_READY,
CensusRoot: censusRoot,
CensusOrigin: models.CensusOrigin_OFF_CHAIN_TREE_WEIGHTED,
Mode: &models.ProcessMode{AutoStart: true, Interruptible: true},
Mode: &models.ProcessMode{AutoStart: true, Interruptible: true, EncryptedMetaData: encryptedMetadata},
VoteOptions: &models.ProcessVoteOptions{
MaxCount: 1,
MaxValue: 1,
Expand Down Expand Up @@ -555,7 +567,7 @@ func createElection(t testing.TB, c *testutil.TestHTTPclient,
qt.Assert(t, code, qt.Equals, 200)
err = json.Unmarshal(resp, &election)
qt.Assert(t, err, qt.IsNil)

election.MetadataEncryptionPrivKey = metadataEncryptionKey
return election
}

Expand Down Expand Up @@ -708,14 +720,73 @@ func TestAPINextElectionID(t *testing.T) {

// create a new election
electionParams := electionprice.ElectionParameters{ElectionDuration: 100, MaxCensusSize: 100}
response := createElection(t, c, signer, electionParams, censusRoot, 0, server.VochainAPP.ChainID())

electionId := response.ElectionID
response := createElection(t, c, signer, electionParams, censusRoot, 0, server.VochainAPP.ChainID(), false)

// Block 4
server.VochainAPP.AdvanceTestBlock()
waitUntilHeight(t, c, 4)

// check next election id is the same as the election id created
qt.Assert(t, nextElectionID.ElectionID, qt.Equals, electionId.String())
qt.Assert(t, nextElectionID.ElectionID, qt.Equals, response.ElectionID.String())
}

func TestAPIEncryptedMetadata(t *testing.T) {
server := testcommon.APIserver{}
server.Start(t,
api.ChainHandler,
api.CensusHandler,
api.VoteHandler,
api.AccountHandler,
api.ElectionHandler,
api.WalletHandler,
)

token1 := uuid.New()
c := testutil.NewTestHTTPclient(t, server.ListenAddr, &token1)

// Block 1
server.VochainAPP.AdvanceTestBlock()
waitUntilHeight(t, c, 1)

// create a new account
signer := createAccount(t, c, server, 80)

// Block 2
server.VochainAPP.AdvanceTestBlock()
waitUntilHeight(t, c, 2)

// create a new process
censusRoot := createCensus(t, c)

// Block 3
server.VochainAPP.AdvanceTestBlock()
waitUntilHeight(t, c, 3)

// create a new election
electionParams := electionprice.ElectionParameters{ElectionDuration: 100, MaxCensusSize: 100}
electionResponse := createElection(t, c, signer, electionParams, censusRoot, 0, server.VochainAPP.ChainID(), true)

// Block 4
server.VochainAPP.AdvanceTestBlock()
waitUntilHeight(t, c, 4)
resp, code := c.Request("GET", nil, "elections", electionResponse.ElectionID.String())
qt.Assert(t, code, qt.Equals, 200)
var election api.Election
err := json.Unmarshal(resp, &election)
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, election.Metadata, qt.Not(qt.IsNil))

// try to decrypt the metadata
sk, err := nacl.DecodePrivate(electionResponse.MetadataEncryptionPrivKey.String())
qt.Assert(t, err, qt.IsNil)
metadataBytes, err := base64.StdEncoding.DecodeString(election.Metadata.(string))
qt.Assert(t, err, qt.IsNil)
decryptedMetadata, err := sk.Decrypt(metadataBytes)
qt.Assert(t, err, qt.IsNil)

// check the metadata decrypted is the same as the metadata sent
var metadata api.ElectionMetadata
err = json.Unmarshal(decryptedMetadata, &metadata)
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, metadata.Title["default"], qt.Equals, "test election")
}

0 comments on commit 3fec585

Please sign in to comment.