How to encrypt data for multiple users in Gaia

A few people have asked me for an overview of how to encrypt data for multiple users. This post will attempt to explain how this can be done using just Gaia.

WARNINGS

The code snippits below are meant for illustrative purposes only. They have not been audited or tested, and they omit necessary error-handling code paths. You will almost certainly need to modify them to use them in your apps. If you find a bug, please reply to this topic and I’ll put the fix in.

Step 1: Share App Public Keys

If Alice (alice.id) wants to share an encrypted file with Bob (bob.id), she first needs Bob’s app-specific public key. But how does she get it? The easiest way is for Bob to store it in a well-known location, which Alice can fetch as an unencrypted file.

Bob’s computer would do something like the following:

import {
   hexStringToECPair,
   putFile
} from 'blockstack'

const appPrivateKey = /* 64-byte or 66-byte hex string */
const appPublicKey = hexStringToECPair(appPrivateKey).publicKey.toString('hex')
const publicKeyPath = "public_key.json"
const publicKeyData = { appPublicKey }
putFile(publicKeyPath, JSON.stringify(publicKeyData), { sign: true })
   .then(keyUrl => { /* do something else */ })

Now, Alice can fetch Bob’s public key with getFile.

Step 2: Fetch Public Keys

Before Alice can share an encrypted file with Bob, she’ll need to get his public key. She can do so with getFile as follows:

import {
   getFile
} from 'blockstack'

const publicKeyPath = "key.json"
const user = "bob.id"
/* NOTE: error-handling is omitted for brevity -- you should implement catch() as well*/
getFile(publicKeyPath, { username: user, verify: true })
   .then(keyData => JSON.parse(keyData))
   .then(keyJSON => keyJSON.appPublicKey)
   .then(publicKey => { /* Now Alice has Bob's public key! */})

Note that Alice must know Bob’s Blockstack ID in order to fetch his public key. If this isn’t easy to do in your app, then I highly recommend that you check out Radiks, which will let you query data by groups of users.

Step 3: Encrypt And Store the File with a Symmetric Key

To encrypt and store the file, Alice will explicitly generate a symmetric key and use that to encrypt the plaintext. Once she does so, she can store the resulting ciphertext publicly.

To encrypt the file, Alice would do the following:

import crypto from 'crypto'
import { putFile } from 'blockstack'

const fileData = /* this is your file data as a UTF-8 string */
const keyData = {
   iv: crypto.randomBytes(16),
   key: crypto.randomBytes(32)
}
const cipher = crypto.createCipheriv('aes-256-cbc', keyData.key, keyData.iv)

/* You can choose different input and output encodings if you want.  See the Node crypto documentation. */
const cipherText = cipher.update(fileData, "utf-8", "hex") + cipher.final("hex")

/* store the ciphertext */
const cipherTextPath = "ciphertext.hex"
putFile(cipherTextPath, cipherText, { sign: true })
   .then(cipherTextUrl => { /* see Step 4 */ })

Step 4: Encrypt and Store the Symmetric Key for Bob

The final step is for Alice to encrypt the symmetric key data keyData for Bob to find. This can be done as follows:

import { putFile, encryptECIES } from 'blockstack'

const keyData = /* keyData from Step 3 */
const publicKey = /* Bob's public key from Step 2 */
const keyDataCipherTextObject = encryptECIES(publicKey, JSON.stringify(keyData))

/* Bob must know about this path */
const keyCipherTextPath = "bob.id-key-data.json"
putFile(keyCipherTextPath, JSON.stringify(keyDataCipherTextObject), { sign: true })
  .then(keyDataUrl => { /* do something with key data url */})

Now, as long as Bob knows (1) the name of his encrypted keyData file, and (2) the name of the encrypted file Alice wants to share, he can fetch and decrypt the data.

Step 5: Read and Decrypt the Shared File

Now that Alice has shared the encrypted file and encrypted symmetric key, Bob can fetch the data as follows:

import { getFile, decryptECIES } from 'blockstack'
import crypto from 'crypto'

const keyCipherTextPath = /* the value from Step 4 -- i.e. "bob.id-key-data.json" */
const fileCipherTextPath = /* the value from Step 3 -- i.e. "ciphertext.hex" */
const user = "alice.id"
const appPrivateKey = /* Bob's app private key */

let keyData = null  /* will be instantiated below */

getFile(keyCipherTextPath, { username: user, verify: true })
  .then(keyCipherTextData => JSON.parse(keyCipherTextData))
  .then(keyCipherText => decryptECIES(appPrivateKey, keyCipherText))
  .then(keyDataJSON => JSON.parse(keyDataJSON))
  .then(keyDataObj => {
    keyData = keyDataObj   /* remember this for below */
    return getFile(fileCipherTextPath, { username: user, verify: true })
  })
  .then(cipherText => {
    const cipher = crypto.createDecipheriv("aes-256-cbc", keyData.key, keyData.iv)
    return cipher.update(cipherText, "hex", "utf-8") + cipher.final("utf8")
  })
  .then(plainText => { /* Bob now has the plaintext! */ })

Remarks

Suppose Alice wanted to share the ciphertext with a third user Charlie (charlie.id). To do so, she would only need to repeat step 4 for Charlie’s public key. She would not need to re-encrypt the file(s), just the symmetric key data.

What if Alice wanted to un-share the file from Charlie? To do so, she would need to at least (1) remove Charlie’s encrypted key file, (2) generate a new key file for everyone else, and (3) use the new symmetric key for all subsequent files. If Alice wanted, she could also re-encrypt all files Charlie could have had access to with the new key, but this would take time. It’s up to the application to decide the best way to handle this.

Regardless, these algorithms assume that Alice and Bob know each other’s Blockstack IDs, and have a way of knowing where the file ciphertext and key ciphertext can be found. This will not be true for all apps.

6 Likes

Great tutorial! This would be nice as a library. I imagine a lot of developers out there don’t know enough about cryptography to implement this or simply don’t want to spend the time learning in order to build their app.

Thank you @jude for this write-up, it is much appreciated.

This is great Jude, I’ll add it to the Gaia documentation.

Thank you very much, this is very helpful!

You state that this is meant for illustrative purposes only and that this needs to be adapted. One necessary adaption I would like to point out, because it can cause security issues is that the same IV shouldn’t be used multiple times.

It’s better practice to only encrypt and share the key of the keyData and generate a new IV each time you encrypt new fileData. As the IV doesn’t need to be secret it could be simply attached to the file which contains the ciphertext.

If you use the same IV, same message and same key you end up getting the same cipher text, that’s one of the possible scenarios for an attacker.

1 Like