Benchmarking Paillier Encryption

and an open source Rust library

with Mario Cornejo and Mathieu Poumeyrol at Snips.

Today we’re open sourcing our experimental implementation of the Paillier homomorphic encryption scheme, written in Rust by our small team at Snips working on various privacy enhancing technologies.

Testing its performance we are not only interested in the concrete numbers we can achieve, but also in the price we pay for using a modern language (spoiler: Rust performed as well as C in all tests).

In the name of reproducibility we also share all our benchmarking code, including instruction for how to launch it on a Google GCE instance.

Security disclaimer: this library is experimental and have not received the same security treatment as for instance OpenSSL or Sodium. Like all libraries tested here, no special attention has been paid towards hardened it against side-channel attacks, and doing so might change the numbers reported.

Private aggregation

The context of this work is secure computation, more specifically private aggregation. The basic idea is that we want to aggregate data from the mobile devices of a large set of users in such a way that their privacy is respected, by for instance guaranteeing that nothing is revealed about any individual.

We’ve already written a bit about that, and will go into more details in a future blog post, but for now just mention a few applications. One widespread use-case is analytics, which we may now do with strong guarantees of privacy. Another is machine learning, where a global model can be trained on user data without learning anything about the individuals.

And how does the Paillier scheme come into the picture? For the same reasons that we talked about secret sharing schemes earlier: its homomorphic properties allow us to compute on encrypted data! Specifically, given a set of encrypted values we can compute their encrypted sum without decrypting, and hence without learning anything about the individual values. But for this to be feasible we need encryption and decryption to be efficient on mobile devices, especially since some applications may require many values to be encrypted.

Why Rust?

Before looking at the benchmarks it is also worth spending a moment on why we chose Rust as the implementation language for most of our work on privacy enhancing technologies (PETs).

As a company focusing on both Android and iOS we have a growing list of languages to choose from, including Java, C#, Swift, Go, Rust, and C. However, supporting cross-device development as a small team made us opt for a single implementation language, in turn narrowing the list down to Swift, Go, Rust, and C. Wanting a modern language with safety features and a good build system further reduced it to Swift, Go, and Rust.

A bit arbitrarily we went with Rust, influenced to some extent by its reputation as being suited for embedded devices. But the question of course is then how much we pay in terms of performance for this choice.

Homomorphic encryption

To understand the benchmarks it is instructive to understand the computations the libraries have to perform. Without going into the details of the Paillier scheme, it can be seen as defining a mapping encrypt from pairs (m, r) to values c, as well as its inverse decrypt; here m is the message to encrypt, r is randomness used to hide the message, and c is the ciphertext.

Focusing on encrypt, which depends on a public key consisting of a generator g and a modulus n, a pair (m, r) is mapped to c as g^m * r^n mod n². Some implementations compute exactly that, using two modular exponentiations and one modular multiplication. Others take advantage of an optimisation that can be done by fixing g as n + 1: in this particular setting, the g^m component may be computed as n*m+1, meaning one modular exponentiation and two modular multiplications instead.

In a similar vein, the computation needed for decrypt comes in two algorithmic flavours: one natural and one optimised using the Chinese Remaindering Theorem.

For those preferring a more mathematical treatment, encrypt is an efficient isomorphism between Zn x Zn* and Zm* with m = n², which may be inverted efficiently given extra information contained in the decryption key. Considering the additive group Zn in the direct product we see the homomorphic properties enjoyed by the scheme, namely addition and scalar multiplication.

Arbitrary precision arithmetic

For the Paillier scheme to be secure, n in the above must be around 2048 bits long, meaning most operations will be on numbers of roughly twice that size due to the n² modulus. While some languages support such large integers natively (e.g. Java and Go), most use third-party libraries.

Unsurprisingly, we will see below that the choice of arbitrary precision library plays a dominant part in the overall performance, yet choosing one is more complicated than that. For instance, while GMP comes out as the overall top performer, its use also implies a more involved build process on devices, not to mention the fact that it comes under a GPL license that may make it impractical for inclusion in mobile applications.

Paillier libraries

In addition to our own Rust library running on top of either GMP or RAMP, we also picked a handful of open source libraries matching the languages mentioned earlier:

C : libpaillier from ACSC using GMP

: libpaillier from ACSC using GMP Go : go-go-gadget-paillier using the native BigInt

: go-go-gadget-paillier using the native BigInt Java : javallier from NICTA using the native BigInteger

: javallier from NICTA using the native BigInteger Python: phe from NICTA using GMP

And, as a potential contester for quickly prototyping cryptographic implementations, we wrote a sketch implementation in Julia, using the native BigInt type (wrapping GMP).

However, some of these libraries implement the algorithmic optimisations and some do not, and as the following graphs shows this can have a big impact on performance numbers.