Skip to content

Commit

Permalink
Merge pull request #168 from c4dt/167
Browse files Browse the repository at this point in the history
fix: use size of Base64-encoded ID to calculate ballot length for decryption
  • Loading branch information
PascalinDe committed Jun 25, 2024
2 parents 0ce24ab + e0003ec commit 5b03fe8
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 25 deletions.
37 changes: 27 additions & 10 deletions contracts/evoting/types/ballots.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,30 +333,47 @@ 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)
// at most 3 bytes (128) + ',' per choice
size += len(rank.GetID())
// 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

// 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.ID)
// 1 bytes (0/1) + ',' per choice
size += len(selection.GetID())
// 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

// 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.ID)
size += len(text.GetID())
// 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

// at most 4 bytes per character + ',' per answer
// 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++
}
Expand Down
10 changes: 5 additions & 5 deletions contracts/evoting/types/ballots_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,37 +314,37 @@ 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,
Choices: make([]Choice, 5),
}},

Ranks: []Rank{{
ID: encodedQuestionID(3),
ID: decodedQuestionID(3),
Title: Title{En: "", Fr: "", De: "", URL: ""},
MaxN: 4,
MinN: 0,
Choices: make([]Choice, 4),
}},

Texts: []Text{{
ID: encodedQuestionID(4),
ID: decodedQuestionID(4),
Title: Title{En: "", Fr: "", De: "", URL: ""},
MaxN: 2,
MinN: 0,
MaxLength: 10,
Regex: "",
Choices: make([]Choice, 2),
}, {
ID: encodedQuestionID(5),
ID: decodedQuestionID(5),
Title: Title{En: "", Fr: "", De: "", URL: ""},
MaxN: 1,
MinN: 0,
Expand Down
18 changes: 9 additions & 9 deletions docs/ballot_encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>[","<answer>]*
ANSWER = <select_answer>|<text_answer>|<rank_answer>
SELECT_ANSWER = "0"|"1"
Expand All @@ -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
Expand All @@ -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"
```
Expand All @@ -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.
11 changes: 10 additions & 1 deletion web/frontend/src/pages/ballot/components/VoteEncode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,26 @@ 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) {
const padding = new ShortUniqueId({ length: ballotSize - encodedBallotSize });
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;
Expand Down

0 comments on commit 5b03fe8

Please sign in to comment.