Here's a brief rundown of the architecture and setup behind our identity proof of concept that we just launched
Check out the app here: https://safe.fission.name/
The goal of this application to to equip a user with a private key while providing UX similar to a traditional web app.
In the demo application, a user stores a private note in IPFS that is encrypted/decrypted with their private key.
For the first iteration of our identity system, we decided to use the ZeroWallet scheme.
- Install dependencies
yarn
- Setup environment
cp .env.example .env
- change variables
- Run
yarn dev
- Given defaults, your server will be on port
8080
and a React app will be on port3000
ZeroWallet is a simple scheme where a user generates a private key with two passwords, and unlocks their account on login using just a username and password.
It makes use of Shamir's Secret Sharing and zero-knowledge proofs to allow recovery of private keys.
ZeroWallet uses 2 passwords and 3 shards. Any 2 shards can reconstruct the privateKey
.
As a brief overview of the components:
- account password: this is the password that a user generally uses to login to their account. Imagine a normal username/password signin
- recovery password: this is the password that a user uses to recover their private key
- shard1: this shard is just a hash of the account password (
shard1 = sha256(accountPassword)
) - shard2: this is the fancy part of the scheme that makes use of zero-knowledge proofs. You can read more about the nitty gritty details Here. For the purpose here, think of it as
shard2 = serverTransform(sha256(recoveryPassword))
. What actually happens is similar to Diffie-Hellman key exchange. The important part of this is that the server gets no information about the user's share (sha256(recoveryPassword)
), and a malicious hacker gets no information about the server's share by sending spoof requests. - shard3: this shard is derived from the first two using Shamir's Secret Sharing (SSS). SSS can come up with an essentially endless number of shards for a given private key. So
shard3
is actually unique per-device
Signup flow (I'm totally new here)
- user enters,
username
,accountPassword
, andrecoveryPassword
recoveryPassword
is hashed and "randomized" into a value we'll callalpha
, then sent to the server- the server generates an arbitrary 256-bit key
zk_key
. zk_key
is combined withalpha
to producebeta
which is sent back to the userbeta
is "derandomized" to produceshard2
- user generates
shard1 = sha256(accountPassword)
- user acquires
privateKey = SSS.combine(shard1, shard2)
- user generates
shard3 = SSS.newShard(privateKey
) - user stores
shard3
inlocalStorage
and logs into the safe
Login flow (I've already logged onto this device before)
- user enters
username
andaccountPassword
- user generates
shard1 = sha256(accountPassword)
- user gets
shard3
fromlocalStorage
- user acquires
privateKey = SSS.combine(shard1, shard3)
and loogs into the safe
Recovery flow (I want to login on a new device)
- This is the exact same flow as Signup except that the server uses the
zk_key
that's already in the database
If a user is only encrypting data for themselves, they can just encrypt/decrypt it with their privateKey
. However, we wanted to build this in such a way that a "sharing" or "social" component could easily be added.
To do this, you don't want to have to re-encrypt your note for every friend that you want to share it with. This leads to a lot of data replication, and also puts a big load on the user since everytime they edit the note, it must be re-encrypted for every one of their friends.
Instead, we encrypt the note with a symmetric key (cipherKey
). Add the cipher text to IPFS. And then encrypt the cipherKey
with the user's privateKey
. Now, we have a simple setup that can be easily expanded to shared notes. If I want to share a note with you, I don't have to re-encrypt the entire note for your publicKey
. I just have to encrypt and send you the cipherKey
I separated all "key" functionality into it's own module: keystore
. Any code that touches private keys is always susceptible to attack (malicious packages, etc). We've discussed writing this module using WASM which essentially gives us a secure "black box" that other parts of our code can't acess, keeping these keys safe.
For the time being, we store just 4 things in the database: username
, zk_key
, safe_cid
, and safe_key
.
The first two are for ZeroWallet. username
is self-explanatory, zk_key
is the server's "share" of the second shard of the user's privateKey
. By itself, it reveals absolutely nothing about the second shard, but it can be used to transform the user's recovery password in such a manner that they can recover the second shard without revealing any information to the server.
safe_cid
is the current CID of the encrypted safe, and safe_key
is an encrypted cipherKey
for the safe. safe_cid
and safe_key
are currently stored in our database for convenience sake, but would eventually be moved to some form of decentralized storage (OrbitDB spike coming as we speak!)