Skip to content

Commit 5b03fe8

Browse files
authored
Merge pull request #168 from c4dt/167
fix: use size of Base64-encoded ID to calculate ballot length for decryption
2 parents 0ce24ab + e0003ec commit 5b03fe8

File tree

4 files changed

+51
-25
lines changed

4 files changed

+51
-25
lines changed

contracts/evoting/types/ballots.go

+27-10
Original file line numberDiff line numberDiff line change
@@ -333,30 +333,47 @@ func (s *Subject) MaxEncodedSize() int {
333333

334334
//TODO : optimise by computing max size according to number of choices and maxN
335335
for _, rank := range s.Ranks {
336-
size += len(rank.GetID() + "::")
337-
size += len(rank.ID)
338-
// at most 3 bytes (128) + ',' per choice
336+
size += len(rank.GetID())
337+
// the ID arrives Base64-encoded, but rank.ID is decoded
338+
// we need the size of the Base64-encoded string
339+
size += len(base64.StdEncoding.EncodeToString([]byte(rank.ID)))
340+
341+
// ':' separators ('id:id:choice')
342+
size += 2
343+
344+
// 4 bytes per choice (choice and separating comma/newline)
339345
size += len(rank.Choices) * 4
340346
}
341347

342348
for _, selection := range s.Selects {
343-
size += len(selection.GetID() + "::")
344-
size += len(selection.ID)
345-
// 1 bytes (0/1) + ',' per choice
349+
size += len(selection.GetID())
350+
// the ID arrives Base64-encoded, but selection.ID is decoded
351+
// we need the size of the Base64-encoded string
352+
size += len(base64.StdEncoding.EncodeToString([]byte(selection.ID)))
353+
354+
// ':' separators ('id:id:choice')
355+
size += 2
356+
357+
// 2 bytes per choice (0/1 and separating comma/newline)
346358
size += len(selection.Choices) * 2
347359
}
348360

349361
for _, text := range s.Texts {
350-
size += len(text.GetID() + "::")
351-
size += len(text.ID)
362+
size += len(text.GetID())
363+
// the ID arrives Base64-encoded, but text.ID is decoded
364+
// we need the size of the Base64-encoded string
365+
size += len(base64.StdEncoding.EncodeToString([]byte(text.ID)))
366+
367+
// ':' separators ('id:id:choice')
368+
size += 2
352369

353-
// at most 4 bytes per character + ',' per answer
370+
// 4 bytes per character and 1 byte for separating comma/newline
354371
maxTextPerAnswer := 4*int(text.MaxLength) + 1
355372
size += maxTextPerAnswer*int(text.MaxN) +
356373
int(math.Max(float64(len(text.Choices)-int(text.MaxN)), 0))
357374
}
358375

359-
// Last line has 2 '\n'
376+
// additional '\n' on last line
360377
if size != 0 {
361378
size++
362379
}

contracts/evoting/types/ballots_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -314,37 +314,37 @@ func TestSubject_MaxEncodedSize(t *testing.T) {
314314
}},
315315

316316
Selects: []Select{{
317-
ID: encodedQuestionID(1),
317+
ID: decodedQuestionID(1),
318318
Title: Title{En: "", Fr: "", De: "", URL: ""},
319319
MaxN: 3,
320320
MinN: 0,
321321
Choices: make([]Choice, 3),
322322
}, {
323-
ID: encodedQuestionID(2),
323+
ID: decodedQuestionID(2),
324324
Title: Title{En: "", Fr: "", De: "", URL: ""},
325325
MaxN: 5,
326326
MinN: 0,
327327
Choices: make([]Choice, 5),
328328
}},
329329

330330
Ranks: []Rank{{
331-
ID: encodedQuestionID(3),
331+
ID: decodedQuestionID(3),
332332
Title: Title{En: "", Fr: "", De: "", URL: ""},
333333
MaxN: 4,
334334
MinN: 0,
335335
Choices: make([]Choice, 4),
336336
}},
337337

338338
Texts: []Text{{
339-
ID: encodedQuestionID(4),
339+
ID: decodedQuestionID(4),
340340
Title: Title{En: "", Fr: "", De: "", URL: ""},
341341
MaxN: 2,
342342
MinN: 0,
343343
MaxLength: 10,
344344
Regex: "",
345345
Choices: make([]Choice, 2),
346346
}, {
347-
ID: encodedQuestionID(5),
347+
ID: decodedQuestionID(5),
348348
Title: Title{En: "", Fr: "", De: "", URL: ""},
349349
MaxN: 1,
350350
MinN: 0,

docs/ballot_encoding.md

+9-9
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The answers to questions are encoded in the following way, with one question per
1313
1414
TYPE = "select"|"text"|"rank"
1515
SEP = ":"
16-
ID = 3 bytes, encoded in base64
16+
ID = 8 bytes UUID encoded in base64 = 12 bytes
1717
ANSWERS = <answer>[","<answer>]*
1818
ANSWER = <select_answer>|<text_answer>|<rank_answer>
1919
SELECT_ANSWER = "0"|"1"
@@ -39,11 +39,11 @@ For the following questions :
3939
A possible encoding of an answer would be (by string concatenation):
4040

4141
```
42-
"select:3fb2:0,0,0,1,0\n" +
42+
"select:base64(D0Da4H6o):0,0,0,1,0\n" +
4343
44-
"rank:19c7:0,1,2\n" +
44+
"rank:base64(19c7cd13):0,1,2\n" +
4545
46-
"text:cd13:base64("Noémien"),base64("Pierluca")\n"
46+
"text:base64(wSfBs25a):base64("Noémien"),base64("Pierluca")\n"
4747
```
4848

4949
## Size of the ballot
@@ -53,15 +53,15 @@ voting process, it is important that all encrypted ballots have the same size. T
5353
the form has an attribute called "BallotSize" which is the size
5454
that all ballots should have before they're encrypted. Smaller ballots should therefore be
5555
padded in order to reach this size. To denote the end of the ballot and the start of the padding,
56-
we use an empty line (\n\n). For a ballot size of 117, our ballot from the previous example
56+
we use an empty line (\n\n). For a ballot size of 144, our ballot from the previous example
5757
would then become:
5858

5959
```
60-
"select:3fb2:0,0,0,1,0\n" +
60+
"select:base64(D0Da4H6o):0,0,0,1,0\n" +
6161
62-
"rank:19c7:0,1,2\n" +
62+
"rank:base64(19c7cd13):0,1,2\n" +
6363
64-
"text:cd13:base64("Noémien"),base64("Pierluca")\n\n" +
64+
"text:base64(wSfBs25a):base64("Noémien"),base64("Pierluca")\n\n" +
6565
6666
"ndtTx5uxmvnllH1T7NgLORuUWbN"
6767
```
@@ -70,4 +70,4 @@ would then become:
7070

7171
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.
7272

73-
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.
73+
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.

web/frontend/src/pages/ballot/components/VoteEncode.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,26 @@ export function voteEncode(
3838

3939
encodedBallot += '\n';
4040

41-
const encodedBallotSize = Buffer.byteLength(encodedBallot);
41+
let encodedBallotSize = Buffer.byteLength(encodedBallot);
4242

4343
// add padding if necessary until encodedBallot.length == ballotSize
4444
if (encodedBallotSize < ballotSize) {
4545
const padding = new ShortUniqueId({ length: ballotSize - encodedBallotSize });
4646
encodedBallot += padding();
4747
}
4848

49+
encodedBallotSize = Buffer.byteLength(encodedBallot);
50+
4951
const chunkSize = 29;
52+
const maxEncodedBallotSize = chunkSize * chunksPerBallot;
5053
const ballotChunks: string[] = [];
5154

55+
if (encodedBallotSize > maxEncodedBallotSize) {
56+
throw new Error(
57+
`actual encoded ballot size ${encodedBallotSize} is bigger than maximum ballot size ${maxEncodedBallotSize}`
58+
);
59+
}
60+
5261
// divide into chunksPerBallot chunks, where 1 character === 1 byte
5362
for (let i = 0; i < chunksPerBallot; i += 1) {
5463
const start = i * chunkSize;

0 commit comments

Comments
 (0)