Security goals for Indienet are:
Implement end-to-end encrypted private messages between federated personal web sites (with the same level of protection as PGP in email).
Enable people to access their federated personal web site, and their entire list of end-to-end encrypted private messages, from any browser/device using a master password. If they haven’t authenticated on a certain device before, they will have to enter their master password the first time only.
Research and use the latest cryptography knowledge and best practices whenever possible. (We will be consulting with cryptographers on our choices as we go.)
Please keep all spikes in separate projects in the /spikes/authentication subgroup and push to the source repo regularly as you’re working on them.
Please create issues for the various tasks you need to perform for each spike in the source repo and update and close the issues as you go.
Please document findings and questions on the notes sections of the spikes on this page as you work.
FeathersJS adds a layer on top of express.js that makes it more easy for developers to quickly build a prototype or a full blown API. It is still modular enough to extend with regular express middleware if necessary, has a great CLI and offers a lot of functionality out of the box.
However it adds an extra level of abstraction to your code and pushes you in a certain direction while creating your app. This will make it a little more difficult to get into the code at first, but once used to it, it will increase productivity and enforce a nice and well structured code-base.
Therefore we think it is a good idea to use the framework to its full potential in realizing our spikes and future Indienet projects.
The goal of this week is to take what we’ve learned from Week 1 and build upon that to spike out authentication with the tech stack that we’re considering (FeathersJS + Nuxt).
Based on what we learned last week:
Use best practices, do not roll any custom crypto:
crypto_pwhashto derive the key we encrypt the private key with (uses Argon2)
crypto_sign_detachedto sign (uses Ed25519)
crypto_box_easyto encrypt (uses X25519)
Repeat all tests from Spike 5, plus the following server-side rendered route tests:
Explore the design; does it work? Do we encounter any pain points or obvious flaws?
Explore the WebCrypto API-based OpenCrypto library. Is it fit for purpose?
Please keep the Web interface as simple as possible. Plain HTML.
Please also keep the server (Node) as simple as possible. Plain Express.
Create a public key and an unextractable private key
This will result in:
unextractable extractable unencrypted private key (
The public key (
Update: note that we are now creating an extractable private key at this point (otherwise, as pointed out by Wim during the spike, we couldn’t encrypt it with the secret key in the next steps). Also note that this changes the order of the steps below:
Create an ephemeral symmetric key from a master password that the site owner chooses (
passwordKey) and use it to .
unencryptedPrivateKey with the
passwordKey to get the
unencryptedPrivateKey as an unextractable key using the WebCrypto API and store it in IndexedDB (see level-browserify for a simpler LevelDB interface for IndexedDB).
Note: see this example of storing unextractable keys in IndexedDB (in that example they create an unextractable key, we will be importing an the extractable key we created as an unextractable key during the import call).
This means that:
The owner of the site doesn’t have to keep entering their password (they will have to – once – the first time they use a different browser or a different device to access their personal federated web site)
encryptedPrivateKey to the server and store them there. You can store them on the file system.
publicKey accessible from a route on the server (just use plain Express on the server):
GET /public-key → returns
encryptedPrivateKey accessible from a route on the server:
GET /private-key → returns
publicKey is accessible to any other instance at a well-known location (it will also be advertised within the actor object in ActivityPub, but don’t worry about that right now)
encryptedPrivateKey can be downloaded to any client that the owner uses in the future. When the owner provides the password they used in Step #2, we can recreate the symmetric key and decrypt
encryptedPrivateKey to obtain
privateKey (which we persist, as in Step 1, on the new client).
Note: at this point, the server has been set up/configured for the first time. It has been claimed by the owner. From here on, to authenticate the owner, we will use publickey authentication (see Spike 3).
The flow has been updated based on feedback from the team to remove a logic error in the original (the private key is now created as an extractable key as, otherwise, it would not be possible to encrypt it and send it to the server). We do, however, still import the unencrypted private key as an unextractable key and store it in IndexedDB as such.
Wim pointed out that we should note that IndexedDB can be deleted by the browser due to space constraints. We should plan for this eventuality and handle it gracefully. The impact to the site owner will be that they will have to enter their password again so we can reauthenticate them.
Use libsodium to implement Spike 1 (please keep all spikes separate and checked into source control; don’t overwrite Spike 1).
Which implementations should we spike (and why?)
What are the differences in workflow between Spike 1 and Spike 2? Which is easier to work with?
What are the security implications of using a libsodium port (which gives us better ciphers) instead of the WebCrypto API (which gives us unextractable keys)? (This is a question we should post to some friendly neighbourhood cryptographers in the community.)
We have chosen to go with the Natrium Browser because this library implements promises instead of callbacks. It also combines the libsodium.js library with Natrium so we have the best of both. ==> After trying to implement this, we came to the conclusion that both Natrium Browser and Natrium are very difficult to implement and rely on different build tools. That’s why we choose to implement the libsodium.js library
The problem with using libsodium is that you have to make choices about different settings for the encryption yourself. In contrast to the OpenCrypto library, the libsodium one is “big” (42kb ~ 512kb + 57,5kb).
To authenticate with the server for the REST and WebSocket APIs, we will be using JWT with a publickey authentication scheme, implemented within Passport.
For publickey authentication, we will:
nonce from the server
nonce with our
unencryptedPrivateKey to get a
signedNonce to the server
On the server, verify the signature using our
If the signature is verified, the server will return the JWT, which we will persist on the client and use in subsequent requests. (We should import the JWT using the WebCrypto API as an unextractable key.)
For this spike, please explore two versions, in order:
Version 1: Vanilla Express and Passport:
Version 2: FeathersJS
At the moment we created a keypair with the OpenCrypto library. The library generates keys based on the RSA-OAEP algorithm. But this algorithm can only be used for encryption and decryption. So in order to sign and verify a nonce, we have to create a new keypair with the RSA-PKCS1-v1_5 algorithm. So a site will have two keypairs, one for encryption/decryption and one for sign/verify.
We will have to make a pull request to the OpenCrypto library, so we can choose which algorithm he will use to generate a keypair.
More info about which algorithm to use for what: * https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API/Supported_algorithms * https://blog.engelke.com/tag/webcrypto/ * https://stackoverflow.com/questions/33929699/using-webcrypto-to-generate-a-key-pair-useful-for-both-encryption-and-signing * https://crypto.stackexchange.com/questions/12090/using-the-same-rsa-keypair-to-sign-and-encrypt
Mock a separate, second node (
node2) that has a:
The goal is for this second node is to send us a private message, encrypted with our public key, that we will decrypt and read using our private key (as created in Spikes 1 & 2; please keep the spikes separate).
Continue to mock
Create a secret symmetric session key (
secretSessionKeyNode2 to get
secretSessionKeyNode2 with our
publicKey to get
secretSessionKeyNode2 with our public key since the message is being sent to us from
node2 and we should be the only ones able to decrypt it, using our
We then assume that node2 communicates, via ActivityPub,
encryptedSecretSessionKeyNode2 to us. Please don’t mock separate nodes or networking between them for this spike.
Then, on our node:
privateKey to decrypt
encryptedSecretSessionKeyNode2 and get
secretSessionKeyNode2 to decrypt
encryptedMessageNode2 and get
For more resources, references, etc., see: /engine/technology-stack/authentication