Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use size of Base64-encoded ID to calculate ballot length for decryption #168

Merged
merged 4 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading