From fb3090e2ede1b684ca532ca70451e6a39cea6398 Mon Sep 17 00:00:00 2001 From: Carine Dengler Date: Mon, 24 Jun 2024 13:17:08 +0200 Subject: [PATCH 1/4] feat: clarify size computation --- contracts/evoting/types/ballots.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/contracts/evoting/types/ballots.go b/contracts/evoting/types/ballots.go index 2d7d1093..e8f8eb14 100644 --- a/contracts/evoting/types/ballots.go +++ b/contracts/evoting/types/ballots.go @@ -333,30 +333,41 @@ func (s *Subject) MaxEncodedSize() int { //TODO : optimise by computing max size according to number of choices and maxN for _, rank := range s.Ranks { - size += len(rank.GetID() + "::") + size += len(rank.GetID()) size += len(rank.ID) - // at most 3 bytes (128) + ',' per choice + + // ':' separators ('id:id:choice') + size += 2 + + // 4 bytes per choice (choice and separating comma/newline) size += len(rank.Choices) * 4 } for _, selection := range s.Selects { - size += len(selection.GetID() + "::") + size += len(selection.GetID()) size += len(selection.ID) - // 1 bytes (0/1) + ',' per choice + + // ':' separators ('id:id:choice') + size += 2 + + // 2 bytes per choice (0/1 and separating comma/newline) size += len(selection.Choices) * 2 } for _, text := range s.Texts { - size += len(text.GetID() + "::") + size += len(text.GetID()) size += len(text.ID) - // at most 4 bytes per character + ',' per answer + // ':' separators ('id:id:choice') + size += 2 + + // 4 bytes per character and 1 byte for separating comma/newline maxTextPerAnswer := 4*int(text.MaxLength) + 1 size += maxTextPerAnswer*int(text.MaxN) + int(math.Max(float64(len(text.Choices)-int(text.MaxN)), 0)) } - // Last line has 2 '\n' + // additional '\n' on last line if size != 0 { size++ } From c66decd684342f3973b3d0ad12dd707aa0bf3279 Mon Sep 17 00:00:00 2001 From: Carine Dengler Date: Mon, 24 Jun 2024 13:54:02 +0200 Subject: [PATCH 2/4] fix: use length of encoded ID to determine total length of ballot --- contracts/evoting/types/ballots.go | 12 +++++++++--- contracts/evoting/types/ballots_test.go | 10 +++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/contracts/evoting/types/ballots.go b/contracts/evoting/types/ballots.go index e8f8eb14..770af369 100644 --- a/contracts/evoting/types/ballots.go +++ b/contracts/evoting/types/ballots.go @@ -334,7 +334,9 @@ func (s *Subject) MaxEncodedSize() int { //TODO : optimise by computing max size according to number of choices and maxN for _, rank := range s.Ranks { size += len(rank.GetID()) - size += len(rank.ID) + // the ID arrives Base64-encoded, but rank.ID is decoded + // we need the size of the Base64-encoded string + size += len(base64.StdEncoding.EncodeToString([]byte(rank.ID))) // ':' separators ('id:id:choice') size += 2 @@ -345,7 +347,9 @@ func (s *Subject) MaxEncodedSize() int { for _, selection := range s.Selects { size += len(selection.GetID()) - size += len(selection.ID) + // the ID arrives Base64-encoded, but selection.ID is decoded + // we need the size of the Base64-encoded string + size += len(base64.StdEncoding.EncodeToString([]byte(selection.ID))) // ':' separators ('id:id:choice') size += 2 @@ -356,7 +360,9 @@ func (s *Subject) MaxEncodedSize() int { for _, text := range s.Texts { size += len(text.GetID()) - size += len(text.ID) + // the ID arrives Base64-encoded, but text.ID is decoded + // we need the size of the Base64-encoded string + size += len(base64.StdEncoding.EncodeToString([]byte(text.ID))) // ':' separators ('id:id:choice') size += 2 diff --git a/contracts/evoting/types/ballots_test.go b/contracts/evoting/types/ballots_test.go index 9b292f73..f47bddfd 100644 --- a/contracts/evoting/types/ballots_test.go +++ b/contracts/evoting/types/ballots_test.go @@ -314,13 +314,13 @@ func TestSubject_MaxEncodedSize(t *testing.T) { }}, Selects: []Select{{ - ID: encodedQuestionID(1), + ID: decodedQuestionID(1), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 3, MinN: 0, Choices: make([]Choice, 3), }, { - ID: encodedQuestionID(2), + ID: decodedQuestionID(2), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 5, MinN: 0, @@ -328,7 +328,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { }}, Ranks: []Rank{{ - ID: encodedQuestionID(3), + ID: decodedQuestionID(3), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 4, MinN: 0, @@ -336,7 +336,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { }}, Texts: []Text{{ - ID: encodedQuestionID(4), + ID: decodedQuestionID(4), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 2, MinN: 0, @@ -344,7 +344,7 @@ func TestSubject_MaxEncodedSize(t *testing.T) { Regex: "", Choices: make([]Choice, 2), }, { - ID: encodedQuestionID(5), + ID: decodedQuestionID(5), Title: Title{En: "", Fr: "", De: "", URL: ""}, MaxN: 1, MinN: 0, From c122b116aff044f263bcfcb7d71045a65fe80d1c Mon Sep 17 00:00:00 2001 From: Carine Dengler Date: Tue, 25 Jun 2024 14:20:15 +0200 Subject: [PATCH 3/4] feat: throw error if encoded ballot is longer than maximum ballot size expected by the backend --- .../src/pages/ballot/components/VoteEncode.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/frontend/src/pages/ballot/components/VoteEncode.tsx b/web/frontend/src/pages/ballot/components/VoteEncode.tsx index 85cd60a6..3a7f9c30 100644 --- a/web/frontend/src/pages/ballot/components/VoteEncode.tsx +++ b/web/frontend/src/pages/ballot/components/VoteEncode.tsx @@ -38,7 +38,7 @@ export function voteEncode( encodedBallot += '\n'; - const encodedBallotSize = Buffer.byteLength(encodedBallot); + let encodedBallotSize = Buffer.byteLength(encodedBallot); // add padding if necessary until encodedBallot.length == ballotSize if (encodedBallotSize < ballotSize) { @@ -46,9 +46,18 @@ export function voteEncode( encodedBallot += padding(); } + encodedBallotSize = Buffer.byteLength(encodedBallot); + const chunkSize = 29; + const maxEncodedBallotSize = chunkSize * chunksPerBallot; const ballotChunks: string[] = []; + if (encodedBallotSize > maxEncodedBallotSize) { + throw new Error( + `actual encoded ballot size ${encodedBallotSize} is bigger than maximum ballot size ${maxEncodedBallotSize}` + ); + } + // divide into chunksPerBallot chunks, where 1 character === 1 byte for (let i = 0; i < chunksPerBallot; i += 1) { const start = i * chunkSize; From e0003ec43d9b85f75de23c38541cfd37835ddb45 Mon Sep 17 00:00:00 2001 From: Carine Dengler Date: Tue, 25 Jun 2024 14:38:57 +0200 Subject: [PATCH 4/4] docs: updated documentation on ballot encoding --- docs/ballot_encoding.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/ballot_encoding.md b/docs/ballot_encoding.md index 84b10512..6e1f69e4 100644 --- a/docs/ballot_encoding.md +++ b/docs/ballot_encoding.md @@ -13,7 +13,7 @@ The answers to questions are encoded in the following way, with one question per TYPE = "select"|"text"|"rank" SEP = ":" -ID = 3 bytes, encoded in base64 +ID = 8 bytes UUID encoded in base64 = 12 bytes ANSWERS = [","]* ANSWER = || SELECT_ANSWER = "0"|"1" @@ -39,11 +39,11 @@ For the following questions : A possible encoding of an answer would be (by string concatenation): ``` -"select:3fb2:0,0,0,1,0\n" + +"select:base64(D0Da4H6o):0,0,0,1,0\n" + -"rank:19c7:0,1,2\n" + +"rank:base64(19c7cd13):0,1,2\n" + -"text:cd13:base64("Noémien"),base64("Pierluca")\n" +"text:base64(wSfBs25a):base64("Noémien"),base64("Pierluca")\n" ``` ## Size of the ballot @@ -53,15 +53,15 @@ voting process, it is important that all encrypted ballots have the same size. T the form has an attribute called "BallotSize" which is the size that all ballots should have before they're encrypted. Smaller ballots should therefore be padded in order to reach this size. To denote the end of the ballot and the start of the padding, -we use an empty line (\n\n). For a ballot size of 117, our ballot from the previous example +we use an empty line (\n\n). For a ballot size of 144, our ballot from the previous example would then become: ``` -"select:3fb2:0,0,0,1,0\n" + +"select:base64(D0Da4H6o):0,0,0,1,0\n" + -"rank:19c7:0,1,2\n" + +"rank:base64(19c7cd13):0,1,2\n" + -"text:cd13:base64("Noémien"),base64("Pierluca")\n\n" + +"text:base64(wSfBs25a):base64("Noémien"),base64("Pierluca")\n\n" + "ndtTx5uxmvnllH1T7NgLORuUWbN" ``` @@ -70,4 +70,4 @@ would then become: The encoded ballot must then be divided into chunks of 29 or less bytes since the maximum size supported by the kyber library for the encryption is of 29 bytes. -For the previous example we would then have 5 chunks, the first 4 would contain 29 bytes, while the last chunk would contain a single byte. +For the previous example we would then have 5 chunks, the first 4 would contain 29 bytes, while the last chunk would contain 28 bytes.