How to store encrypted secrets using IDX
Learn to encrypt, store, and recover user-managed secrets with IDX.
IDX library has been deprecated, for future interactions with the Identity Index protocol we've built DID-DataStore. For more details on how to store encrypted secrets on the Identity Index protocol, please take a look at our updated documentation here.
This tutorial will demonstrate how to allow users of your application or platform to encrypt secrets with a DID and store them in their IDX. Apps and libraries can use this pattern to easily store and recover seeds, secret keys, personal information, application data, and credentials that they may need for their own systems, simply based on the ability to authenticate a DID.
Because IDX allows any data to be associated to a DID in a decentralized manner, the secrets and information stored in IDX are secure, 100% owned by the user, and can easily be discovered and accessed by any application or platform allowed by the user. (Interop!) Under the hood, IDX uses Ceramic as a decentralized data storage protocol.
In this tutorial we will use the following technologies:
- DIDs: a W3C standard for decentralized identifiers
- JWE: an IETF standard for encrypted content
- Ceramic HTTP client: a JS client for interacting with a remote Ceramic node over HTTP
- IDX: a protocol for user-centric data management based on DIDs
- Ed25519 Key DID provider: a library for using the Key DID method
- uint8arrays: a utility library for converting between binary and text data
Environment setup
To get started using IDX, we'll first need to install the Ceramic and IDX CLIs using npm
. You'll also need to have node
installed.
npm install -g @ceramicnetwork/cli @ceramicstudio/idx-cli
Then we'll start the Ceramic daemon to run a local Ceramic node:
ceramic daemon
We will also need to make sure the Ceramic node supports IDX documents, using the following command:
idx bootstrap
Now let's create a DID and give it the label local
so we can more easily reference it throughout the rest of this tutorial. We will use this DID for authentication when interacting with the CLI as a developer (not an end-user):
idx did:create --label=local
Data model
To store our encrypted secret using Ceramic and IDX, we will need two things:
- A JSON schema for the encrypted payload (JWE)
- An IDX definition to uniquely identify our encrypted secret in the user's IDX index
JWE schema
We will use the following JSON schema to validate that the encrypted payloads match the JWE specification when storing them on Ceramic:
{
$schema: 'http://json-schema.org/draft-07/schema#',
title: 'JWE',
type: 'object',
properties: {
protected: { type: 'string' },
iv: { type: 'string' },
ciphertext: { type: 'string' },
tag: { type: 'string' },
aad: { type: 'string' },
recipients: {
type: 'array',
items: {
type: 'object',
properties: {
header: {
type: 'object',
properties: {
alg: { type: 'string' },
iv: { type: 'string' },
tag: { type: 'string' },
epk: { type: 'object' },
kid: { type: 'string' },
},
required: ['alg', 'iv', 'tag'],
},
encrypted_key: { type: 'string' },
},
required: ['header', 'encrypted_key'],
},
},
},
required: ['protected', 'iv', 'ciphertext', 'tag'],
}
Using the DID previously created, we can publish the schema to the local Ceramic node. Use the idx schema:publish
command as shown below:
idx schema:publish local '{"$schema":"http://json-schema.org/draft-07/schema#","title":"JWE","type":"object","properties":{"protected":{"type":"string"},"iv":{"type":"string"},"ciphertext":{"type":"string"},"tag":{"type":"string"},"aad":{"type":"string"},"recipients":{"type":"array","items":{"type":"object","properties":{"header":{"type":"object","properties":{"alg":{"type":"string"},"iv":{"type":"string"},"tag":{"type":"string"},"epk":{"type":"object"},"kid":{"type":"string"}},"required":["alg","iv","tag"]},"encrypted_key":{"type":"string"}},"required":["header","encrypted_key"]}}},"required":["protected","iv","ciphertext","tag"]}'
Successfully running this command will display the schema URL, which will be used in the next step. In my case the URL is ceramic://k3y52l7qbv1fryd65pd3dyu9dim46vx39z4bgzrrligbi5sfd0e3mwump2p6pdn9c
but when you run the command with your own DID, it will be different.
Encrypted secret definition
Now that we have a generic JWE schema, we need to create a unique definition for the secret data we want to store using the idx definition:create
command. Multiple definitions can use the same schema, but each definition functions as a unique key in the user's IDX index.
idx definition:create local --schema=<schema URL from previous command> --name="my secret data" --description="encrypted payload"
Running this command with display the unique StreamID for the definition, which we will need in the following step. In my case this ID is kjzl6cwe1jw14adxazb6clhiiniffhkedz69mrkvi4i1miad62iwazktfiy8rgm
but it will be different for you.
Using our definition
Let's create a simple script that uses our definition to check if the encrypted secret is present in a user's IDX index, or create and store a new one if needed.
Dependencies
We'll need to install the following dependencies from npm
: Ceramic HTTP client, IDX, Ed25519 Key DID provider, Key DID resolver, DIDs, and uint8arrays.
npm install @ceramicnetwork/http-client @ceramicstudio/idx key-did-provider-ed25519 key-did-resolver dids uint8arrays
Now let's create a simple script in a secret.js
file, importing these dependencies along with randomBytes
from the crypto
module:
Ceramic authentication
In order to allow a user to encrypt their secret and store it in their IDX index, we will need our Ceramic client to be authenticated. Here we will use the Ed25519Provider
requiring a 32 bytes seed that we will either get from the environment, or by generating a random one:
In order to access secret data already associated to a DID, we will need to use the same seed by setting the SEED=<base16-encoded string>
environment variable, otherwise a new DID will be created.
Now let's use this getSeed()
function to authenticate our Ceramic client:
Encrypted secret interactions
We will now create two functions to interact with our encrypted secret: one to encrypt and store, and another one to load and decrypt, using the definition we created from the CLI. Note, make sure to replace kjzl6cwe1jw14adxazb6clhiiniffhkedz69mrkvi4i1miad62iwazktfiy8rgm
in the code below with the definition ID you created:
We provide our own DID idx.did.id
when calling idx.did.createJWE()
so that only the authenticated DID can decrypt the created JWE payload. It's not the case in this example, but if you would like to allow other DIDs to decrypt the payload you would include them here.
As an alternative tocreateJWE
anddecryptJWE
, we could use thecreateDagJWE
anddecryptDagJWE
methods to encrypt JSON data instead of binary as we are doing in this example.
Interaction logic
Finally, let's create a function to implement a simple flow of checking if the encrypted secret exists and display it, or create and store a new secret:
Running our script
Let's run our script a first time, this will generate both a random seed for the DID and a random secret:
node ./secret.js
This will log the created seed, DID and created secret.
Now let's run the script again but providing the created seed as logged above:
SEED=<base16-encoded string> node ./secret.js
The script should log the same DID and decrypted secret.
Full script
Here is the full secret.js
script for convenience, don't forget to change the KEY
value with your own definition ID before running it:
Use case: Encrypted seeds
There are many exciting use cases for storing and retrieving encrypted secrets using IDX. Let's walk through a very important one – storing encrypted seeds.
Many decentralized systems rely on public key cryptography for performing authenticated transactions, such as signing and encrypting information. In these systems, a private key (seed) is used to sign a message which can then be validated against the corresponding public key (identifier) belonging to the user. Public key cryptography allows the decentralized system to verify that an interaction has originated from a given user without requiring the use of servers. Most of these types of systems require users to manage a dedicated seed for their system, which means that as users use more systems, they have and ever-growing list of keys to manage.
This model poses many challenges to users and application developers. Here are some common questions that arise when using decentralized technologies dependent on seeds for authenticated transactions:
- How should I securely store this seed to make sure only the intended user can access it?
- How can an app or library check if an account (seed) has already been created for a given user?
- How can my app make use of multiple decentralized technologies without requiring users to manage a different seed for each one?
As we have demonstrated throughout this tutorial, storing encrypted seeds directly with users on IDX using their DID can solve all of these challenges. And as the number of decentralized systems that users will need to interact with is only increasing, the need to simplify secret management in a platform-agnostic way is becoming ever more valuable.
What's next?
The examples in this tutorial were intentionally kept simple but more complex schemas could be created, for example, to store public data along with encrypted secrets to support more specific use cases.
Learn more about IDX by visiting the IDX website and documentation.
If you need any help or just want to say hi, join the IDX Discord channel!