An implementation of the OPAQUE protocol
Opaque is a new method of augmented password-authenticated key exchange (aPAKE). aPAKE allows a server and client to agree on a key without the server ever receiving the plaintext password. Opaque improves on previous methods by never revealing the server's salt to the client, and thus is more resistent to certain classes of precomputation attack. The server never receiving a plaintext password is a very valuable property, as numerous services have accidentally logged millions of user passwords (including a recent, high-profile Facebook blunder). A more subtle feature of Opaque is that the protocol allows for a client-run key derivation function (KDF), which is traditionally run by the server. KDF makes authenticating a password more computationally expensive, and thus any attack involving guessing the password becomes much more computationally expensive. It is uncommon for servers to opt for the most secure KDFs, as that is often too costly from a resource standpoint. If that cost is distributed across clients, however, it becomes feasible to use the most secure KDFs, like Argon2, to secure client passwords. This is covered in section 3.4 of the draft.
While Opaque defines the methodology of authentication, it does not specify the specific cryptographic primitives to use. This implementation aims to provide a reasonable set of primitives to use, and configuration is currently a non-goal.
In the academic paper, the password is protected using exponential blinding, while the internet draft suggests multiplicative blinding. For this implementation, we use exponential blinding because it results in a simpler API, which doesn't require the v=g^k
term in the draft proposal. This may end up being slightly more computationally expensive, but this cost pales in comparison with even the simplest key derivation function.
Elliptic curves are very highly regarded cryptographic primitives. secp256r1
is a cousin to secp256k1
but with ostensibly random parameters (hence r
instead of k
). It is impossible to prove that the parameters were, in fact, randomly and not nefariously chosen but there seems to be enough evidence that secp256r1
is robust, at least for our purposes. The Koblitz curve, secp256k1
, was also a contender but the parameters were chosen in the interest of efficiency, which doesn't really matter for our use case since we also want to use a fairly robust KDF. Also, the surfeit of financial interest in Bitcoin, which utilizes secp256k1
has given rise to specialized hardware for executing cryptographic operations on this curve. As a result, a user (who doesn't have specialized hardware) is at a disadvantage against an attacker (who may have specialized hardware).
UPDATE: Cloudflare's League of Entropy now provides an interesting source of randomness, and may be a good candidate for generating verifiably random elliptic curves.
SHA-2 is showing signs of age, and it was developed by the US Government and not by the community at large. While SHA-3 shares its name and is also endorsed by NIST, it was the product of a lengthy, open standardization process and thus engenders more confidence that it does not have a back door. SHA-3 is very simple, and can be implemented in a couple hundred lines of C, and the underlying permutation (Keccak) is quite powerful and has other cool applications.
Scrypt and Argon2 are the most popular high-security KDFs. Argon2 is based on BLAKE2 which is based on Daniel J. Bernstien's ChaCha cipher. BLAKE2 participated in the SHA-3 competition, and though it lost to Keccak, it is still regarded as secure and Daniel J. Bernstein is very well regarded in the cryptography community.
Argon2 comes in a two variants, Argon2i
and Argon2d
. Argon2id
is the hybrid mode which provides good protection against most adversaries.
As was mentioned in the previous secition, Daniel J. Bernstien is well regarded in the cryptography community and his primitive of choice is Curve25519
. There are many services using Curve25519
and many implementations of the underlying cryptography. The default library, NaCl
, also provides a simple, idiot-resistant API, mitigating several potential implementation issues.
All dependencies are vendored for convenience, though care is taken not to edit the dependency source unless absolutely necessary. The goal is to have this library build across many platforms without requiring that libraries be installed and available on those platforms.
We use libecc
for operations on secp256r1
. libecc
also implements SHA-3, which is convenient and saves us an extra dependency. Most importantly, libecc
aims to be simple and uses a minimal amount of code to provide the functionality Opaque requires. Also, libecc
seems to be under development by the French government, which gives me hope that it is (or will become) fairly robust.
Argon2 won the Password Hashing Competition. We currently use their reference implementation as it is compact and simple. The other alternative, libsodium
is well regarded but seemed harder to vendor as a dependency as it is often recommended to have the static library installed on your system to use.
Again, though standard NaCl
is almost certainly more performant, tweetNaCl and its emphasis on compactness and simplicity was more aligned with our goals. Additionally, the performance of encryption and sigining are likely to be dwarfed by the computational cost of running a KDF, so we are not as worried about performance here.
Currently, my use case for this library is for use with a Swift application and thus I'm using Swift Package Manager to build the C code. I also like that SwiftPM is more declarative than make
and doesn't require specifying what shell commands to run.
To run the code, simply have Swift installed and run swift test
.
NOTE: WebAssembly requires a version of clang
that supports --target=wasm32
. Notably, the clang
that ships with Xcode does not support this. This was last tested with llvm
version 9.0.0
.
NOTE: The command below requires jq
to extract the list of source files from the package.
clang \
--target=wasm32 \
-Oz \
-flto \
-nostdlib \
-Wl,--no-entry \
-Wl,--export=__heap_base \
-Wl,--export={malloc,strlen} \
-Wl,--export=opq_{encrypt_password,generate_keys,generate_verification} \
-Wl,--export=opq_size_of_{result,encrypted_password,password_key,encrypted_salted_password,encrypted_private_key,public_key,verification_nonce,verification} \
-Wl,--export=opq_result_message \
-Wl,--export=opq_result_type{,_{success,failure,fatal_error}} \
-Wl,--export=Base64{en,de}code{,_len} \
-Wl,--allow-undefined \
-Wl,--lto-O3 \
-DWITH_LIBECC_CONFIG_OVERRIDE \
-DWITH_CURVE_SECP256R1 \
-DWITH_HASH_SHA3_256 \
-DWITH_SIG_ECDSA \
-DWORDSIZE=32 \
-DARGON2_NO_THREADS \
-I Sources/CLibOpaque/Dependencies/libecc/src \
-I Sources/CLibOpaque/Dependencies/argon2/include \
-I Sources/CLibOpaque/Dependencies/tweetnacl \
-I Sources/CLibOpaque/include \
-I Sources/WebAssembly/Shim/Dependencies/base64 \
-I Sources/WebAssembly/Shim/include \
$(swift package describe --type json | jq -r '.targets[] | select(.c99name == "CLibOpaque").sources[]' | sed 's@^@Sources/CLibOpaque/@') \
$(ls Sources/WebAssembly/Shim/*.c | grep -v emscripten) \
Sources/WebAssembly/Shim/Dependencies/base64/base64.c \
-o opaque.wasm
The WebAssemblyTestCompanion
target can be used in conjunction with opaque.html
and the compiled opaque.wasm
to demonstrate compatibility between the C and WebAssembly products.