api-guide.md 12 KB

ov-api / online voting client api

About

This api provides a high level interface to the client-side crypto and the server endpoints of the online voting protocol, with two main interfaces:

  • OvApi is a simple and straightforward api for the full protocol. It basically exposes three simple methods (authentication, send vote and cast vote) and transparently takes care of all the details and intermediate requests. Implementing a full voting client with this api is pretty trivial, so this is the preferred method.

  • OvWorker is a helper interface for precomputing values in background during the session, while the user is interacting with the voting portal. This may significantly speed up the final vote encryption process.

This document is a general guide for setting up and using these interfaces.

Setup

The library is distributed as a single bundle file containing all necessary dependencies, including support for web workers for asynchronous processing.

  <script src="crypto/ov-api.js"></script>

Note: './crypto/' is the default location for the script. You may use a different location but the API needs to be aware of this (this same script will be dynamically loaded into web workers) so you will have to pass the new location as a configuration option.

Basic usage

Bootstrap

As soon as the host page is loaded, the api will automatically start collecting entropy. However, you need a way to know when it is safe to start invoking methods in the api. For this, you call the ovApi.init() method like so:

window.ovApi = new OvApi({
    lib: 'my/custom/path/ov-api.js',
});
ovApi.init().then(function() {
     console.log('Online voting ready!');
});

Note that you can invoke the api without doing this, but this is not recommended. If you start making requests before enough entropy has been collected, the api will simply work with lower quality randomness, which you probably don't want.

Asynchronous process

Most API methods are asynchronous and return promises. When called they will return immediately a promise object. You can chain 'then', 'fail' and 'progress' event handlers to this promise object which will be invoked as appropiate once the promise is resolved. All these handlers are optional.

ovApi.sendVote(voteoptions)

    .then(function(result) {
        // the method has completed ok and 'result' will
        // contain the return value
    })
    .fail(function(error) {
        // the method has failed and 'error' will
        // contain information about the error
    })
    .progress(function(percent) {
        // the method is still processing and 'percent' will
        // contain the estimated percentage completed (0-100 integer)
        // this is informative, and might get called multiple times
        // (or not at all)
    });

Note that these promises are implemented with Kris Kowal's "q" library which supports several alternative ways to handle them. More info: https://github.com/kriskowal/q/wiki/API-Reference

Concurrency

Note that the API being asynchronous doesn't mean it is automatically concurrent. You should have only one task running independently from the main thread per instance of the API (i.e. per web worker).

It is safe, however, to create several instances of the API and have them running concurrently if you should ever need to do so.

Using OvApi

Implementing a vote cycle with this API is quite simple.

Initializing

First create and initialize an instance and wait for the init method to resolve:

window.ovApi = new OvApi({
     tenantId: 'someTenantId',
     electionEventId: $stateParams.eeid
});
ovApi.init().then(function() {
    
     // ovApi is safe to be used now!
});

Note that you can pass configuration options to the constructor.

Authenticating and getting a ballot

Next, once you have asked the voter for his/her startVotingKey, just call the requestBallot method like so:

ovApi.requestBallot(startVotingKey)
.then(function(ballotResponse) {

 // if you do want a parsed ballot, you can now call: 
 var ballot = ovApi.parseBallot(ballotResponse);

})

This is all it takes to get an authenticated session for the voter and obtain the ballot.

The ballot

The ballot is basically an object tree with the full collection of questions, votations and elections available to the voter, suitable for navigation and rendering it to HTML. The structure will vary depending on the type of elections, but every selectable option will contain a 'prime' property which is a number representing the actual vote option. This number is the only relevant information for the voting protocol, so you can do anything you want to the ballot object (add properties to track selections, etc ...).

Note that the ballot will be in the language configured as default. If you want to present it in a different language, just call translateBallot.

ovApi.translateBallot(ballot, 'fr_FR');

The ballot object has two additional properties, status and correctnessIds, that you will need to be aware of:

ballot.status / Checking vote status

This property indicates if the voter has already voted on this ballot and wether the vote was fully completed or not.

switch (ballot.status) {

     case 'SENT_BUT_NOT_CAST':

     // The voter already sent a vote for this ballot, but left the
     // portal before actually 'casting' it. You should ask him to do
     // that now. See OvApi.requestChoiceCodes

     case 'CAST':

     // The voter did cast a vote for this ballot. You can now remind
     // him that he already did so and show him his 'vote cast
     // code'. See OvApi.requestVoteCastCode

     case 'NOT_SENT':

     // The voter never voted on this ballot. You should present it to
     // him so he can make his choices.

ballot.correctnessIds / Vote correctness map

This is a map of vote correctness identifiers for each representation (prime) in the ballot. You will need to supply it as a parameter for the sendVote (see below).

Sending the vote

Once the voter has made all his/her choices, assembling and sending a vote is just a matter of calling sendVote, passing the vote options as an array of strings representing (big) integers.

    
ovApi.sendVote(primesArray, 
    encrypterValues, 
    null, // precomputed partial choice codes, can be null
    null, // precomputed proof values, can be null
    ballot.correctnessIds,
    null, // writein (only present if writeins are enabled, can be null or omitted if later arguments also omitted)
    null, // verificationCode (only present if an additional verification step is enabled, can be null or omitted if later arguments also omitted)
    null) // disableOptimizations (intended for testing purposes, can be null or omitted)
    .then(function(choiceCodes) {

   // Once this point is reached, you should have the choice codes to show to the voter. The voter should be encouraged to make sure that they match.

The write-in options, if present, are informed as an array of strings.

Casting the vote

If everything is ok, the voter will supply the ballot casting key to cast the vote. You just call:

ovApi.castVote(ballotCastingKey)
  .then(function(voteCastCode) {

  // Once this point is reached, you have the vote cast code to show to the voter. Again,
  // they should make sure it matches.

That's all, we're done!

The voting process is now complete. The process of recovering from an interrupted session ( requestChoiceCodes method) and returning voters ( requestVoteCastCodes method) is not covered in this guide, but it follows the same pattern as above.

Using OvWorker for precomputations

Initializing

First create and initialize an instance and wait for the init method to resolve, just like you do with OvApi :

window.ovApi = new OvApi(config);
window.ovWorker = new OvWorker(config);
        
ovApi.init().then(function() {
    ovWorker.init().then(function() {
        console.log('Online voting ready!');
    });
});

Precomputations

The vote encryption step is quite compute intensive and can take a while, specially on low end devices. To minimize this several computations can be performed in advance, in a separate thread (i.e., a web worker) while the user is interacting with the voter portal. We call these 'precomputations'.

To use precomputations you first invoke the appropiate method, store the result when available and finally supply them to your vote encryption method of choice.

There are 3 types of precomputations you can use:

  • for encryption.
  • for partial choice code generation
  • for zero knowledge proofs

Precomputations for encryption

You need the election's encryption parameters for these. The encryption parameters are provided along with the ballot, so this means you must have already called (and resolved) OvApi.requestBallot .

Once you did, you can recover the encryption parameters with ovApi.getSerializedEncryptionParams() and feed them to OvWorker.precomputeEncrypterValues() , like so:

ovApi.getSerializedEncryptionParams()
    .then(function(serializedEncParams) {

        // start precomputing encrypter values
        ovWorker.precomputeEncrypterValues(serializedEncParams)
            .then(function(encrypterValues) {
                // got our encrypter values, store for later
                mySessionService.setEncrypterValues(encrypterValues);

To actually use these encrypterValues you need to supply them to the encryption method OvApi.sendVote() , like so:

ovApi.sendVote(
    primes,
    mySessionService.getEncrypterValues(),
    ...

Note that both methods accept this parameter to be null or omitted, in which case they simply will just not use any precomputed values.

Precomputations for zero knowledge proofs

Once you have the encryption parameters and the precomputed encrypter values you can also perform in advance several computations for the zero knowledge proofs.

// start precomputing proofs
ovWorker.precomputeProofValues(
    at.voterInformation.electionEventId,
    at.voterInformation.votingCardId,
    serializedEncParams,
    encrypterValues)
.then(function(proofValues) {
    // got our proof values, store for later
    mySessionService.setProofValues(proofValues);
    ...

To use these precomputed proof values just supply them to the encryption method ( OvApi.sendVote() ), like so:

ovApi.sendVote(
    primes,
    mySessionService.getEncrypterValues(),
    ...
    mySessionService.getProofValues(proofValues)
    ...

Precomputations for partial choice code generation

For these optimizations you need also a secret key contained in the voter's keystore, which is provided by OvApi.requestBallot

// get verification secret 
ovApi.getSerializedVerificationCardSecret()
    .then(function(serializedVerificationCardSecret) {
        // got our verification secret, store for later
        mySessionService.setVerificationKey(serializedVerificationCardSecret);

The partial choice codes are precomputed individually. This is because there can be a potentially large number of them and computing them all might be considerable overkill or even choke the browser. So the strategy is up to you and depends largely on the size of your election (in terms of possible different vote options).

A sensible approach is to precompute just the options the user actually selects since they are likely to be present in the final vote. So, whenever the user makes a choice you can do something like this:

// user just selected 'option' ...
ovWorker.precomputePartialChoiceCode(
    mySessionService.getEncParams(),
    option.prime,
    mySessionService.getVerificationKey());

Note that this method is 'fire and forget'. It returns nothing, but will silently compute the partial choice code in background and keep it in a pool. You don't really have to care.

When the time comes to actually encrypt the vote you can call ovWorker.getPrecomputedPartialChoiceCodes() to obtain the pool of precomputed partial choice codes up to that point:

ovWorker.getPrecomputedPartialChoiceCodes()
    .then(function(precomputedPCC) {
        ovApi.sendVote(primes, sessionService.getEncrypterValues(), precomputedPCC)

You then pass these values to OvApi.sendVote() . It will use any partial choice codes present in the pool, and will compute the rest, if any, on the fly. Again, this parameter can be null.