Building an interplanetary ĐApp from scratch

apps Jun 13, 2018

And sending secret messages over the distributed web

Part of our plan to bring the text 1 million peers to the IPFS network involves making it fun and easy to participate in the distributed web. We’re taking this call to action seriously, and have already started building real products that solve real problems in a secure and decentralized way (without requiring folks to learn a bunch of new crypto-/web-technologies). But sometimes, developers and hobbyists want to get their hands dirty. That’s what this post is for.

We’re going to build a simple ĐApp from scratch, using modern web-dev technologies, magic (cryptography),and the Interplanetary File System (IPFS). And we’re going to host it permanently and securely on the decentralized web. The final product? A ĐApp that allows users to securely send encrypted, ephemeral messages across the web without the need for any centralized servers. A decentralized dead drop if you will. So let’s dig in.

First, I’m going to assume that a) you’re familiar with node development (and that you have node installed), b) you’re aware that IPFS is a thing (check out this video if you aren’t), c) you’ve heard of cryptography, and d) you think that spies and secret messages are cool.


With those assumptions in mind, let’s get started. You’re going to want to create a new node project form the command line. I called mine encryptoid, and the only non-defaults I used were to set entry point to dist/index.html and test to standard (which is really just a nice linter).

mkdir encryptoid
cd encryptoid
npm init

Your package.json file should look something like this:

  "name": "encryptoid",
  "version": "1.0.0",
  "description": "Browser dapp for encrypting and sending ephemeral secret messages over ipfs",
  "main": "dist/index.html",
  "scripts": {
    "test": "standard"
  "author": "Carson Farmer <[email protected]>",
  "license": "MIT"

Now we’ll add some devDependencies. Because we want to run our ĐApp in the browser, but write modern, node-style modules using ECMAScript 2015(ES6) syntax, we’ll need to use browserify and friends to transpile our Javascript.

yarn add --dev browserify babelify babel-core babel-polyfill babel-preset-env

We’re also going to use a few other modules to minify, lint, watch, and simplify our code, and serve up our ĐApp while we’re building it:

yarn add --dev envify npm-run-all shx standard uglifyify watchify ecstatic

Next, we need to setup our folder structure. I like to keeps things pretty modularized, so we’re going to set things up something like this:

| |____index.html
| |____images
| | |____logo.png
| |____main.js
| |____style.css
| |____...

With dist being the output location for our transpiled modules and support files (html, css, images, etc). Now, with that folder structure in mind, let’s modify package.json to setup our build scripts:

"scripts": {
    "start": "ecstatic dist",
    "clean": "shx rm -rf dist",
    "build": "run-s build:*",
    "build:copy": "run-p build:copy:*",
    "build:copy:html": "shx mkdir -p dist && shx cp src/index.html dist/index.html",
    "build:copy:css": "shx mkdir -p dist && shx cp src/style.css dist/style.css",
    "build:js": "browserify src/main.js -o dist/bundle.js -g uglifyify",
    "watch": "npm-run-all build:* --parallel watch:*",
    "watch:js": "watchify -t envify src/main.js -o dist/bundle.js -v",
    "watch:serve": "ecstatic --cache=0 dist",
    "test": "standard"
  "browserify": {
    "transform": [
      ["babelify", {"presets": ["env"]}],

There’s a lot here, but the build components are the most important pieces. Our main build:js script will run our src/main.js Javascript file through browserify and uglifyify using the settings specified in the “browserify” entry (which includes running it through babelify and envify), and the copy-based scripts will move our various support files to our dist folder. The only ‘custom’ bit in here is that we’re using the shx module to run cross-platform shell commands within our scripts.

Getting started

Ok, we have our structure in place, now let’s get to the task at hand: getting our hands dirty writing code. We’ll start with the basics, let’s create an index.html file in our src directory to support our ĐApp, along with some minimal styling (style.css)and an ‘empty’ Javascript file (main.js) with something like console.log('hello world').

If we run yarn build, then yarn start, and navigate to we should get a simple web-page with “Welcome to Encryptoid!” splashed across the screen. Nothing ‘ĐAppy’yet. So now let’s add some functionality.

First, we’ll add some run-time dependencies. For instantiating an IPFS peer node, we’ll use window.ipfs-fallback, and for performing the cryptographic calculations, we’ll use js-libp2p-crypto. The crypto module is pretty self-explanatory, but the ipfs-fallback functionality requires some explanation.

The fine folks at IPFS have released the IPFS Companion web extension that simplifies access to IPFS resources by running a Javascript IPFS peer in your browser. It can expose an embedded IPFS node as window.ipfs on every webpage! This makes it possible for any IPFS-based ĐApp to detect if window.ipfs exists and opt-in to use it instead of creating a one-off js-ipfs peer. It is not required for running ĐApps, but it does make them run better, because you don’t have to spin up a whole IPFS peer each time you load a new ĐApp. But, we obviously can’t expect our users to install a browser extension before being able to use our ĐApp, which is where window.ipfs-fallback comes in. This module will detect the presence of window.ipfs, and use it if available, or automatically fall back to downloading the latest version of IPFS from the CDN if it’s not. Nice and clean:

yarn add window.ipfs-fallback js-libp2p-crypto

Now that we have these modules, let’s add some Javascript functionality to our src/main.js file:

We’re using async/await patterns to make the code nice and readable, so hopefully the above code is pretty self-explanatory. In short, we are calling and waiting for our getIpfsfunction to return an IPFS peer node, and then querying said peer for its idinfo, and logging this to the console. We could also add a little ‘ready’ indicator to the page so that our users would know they are ready to access the distributed web.

At this point, we already have a working IPFS-based ĐApp. We could call it a day and move on — which is what we did when we created the IPFS ĐApp Template for others to build on — or we could start to get a bit more creative… so let’s do that. To allow users to write secret messages, we’ll need two text inputs, one for the message itself and one for the super secret password. We’ll also want some way of showing the encrypted message address, and maybe some styling to fit our spy theme?

Under our welcome header, let’s add two text inputs in a form, and a button to do the work. We’ll also stick in an output div to display the output for us:

Now, you’ll want to restart your yarn watch process, and we’ll start editing src/main.js again:

The biggest change here is that we’ve added an event listener on our Encrypt button. In this event listener we are doing several things:

  • On line 19 we are using the PBKDF2 (Password-BasedKey Derivation Function 2) key derivation function to produce a derived key, which we then use as the cryptographic key in subsequent operations. While it is generally a good idea to use random salt when hashing passwords, we aren’t ever storing passwords here, so we just use a fixed salt. We stick to 5000 iterations here, with a key size of 24 bytes.
  • Line 20 we create a fixed length(16) buffer initialization vector (IV). Since we’re only ever using our derived key once, this should be ok. But ideally, we’d use a random IV.
  • The real ‘magic’ happens in lines 22 through 27. We start by creating an AES encryption object in CTR mode (sincewe have a 32-length key, we’re using AES 256) using our key and IV.
  • Next, on line 24 we do the actual encryption. We pass a Buffer of our plaintext message into the cipher’s encrypt method, who’s ciphertext gets passed into our callback (unfortunately, libp2p-crypto doesn’t seem to support promises, hence the callbacks here).
  • Finally, on line 26 we add the encrypted message to IPFS and await the returned CID hash. If you’re unfamiliar with CID hashes and the mechanics of IPFS, I recommend brushing up here.
  • To provide a way to access the encrypted message, we simply output the CID hash. We could then send this hash to our intended recipient, and have them decrypt its contents. You can view the encrypted message’s ciphertext by accessing it over the public gateway:….

To do the actual decryption, the process is almost identical. I’ll leave the actual implementation details as an exercise for the reader(libp2p-cryptoprovides some nice examples), but the main difference would be that the aes.createcallback might look something more like this:

You can take a look at how we implemented this in our GitHub repo, which we’ll be releasing as a full-blown demo app on IPFS soon.

And that’s it for now! You know have a fully working IPFS-based ĐApp that actually does something useful (encrypts secret messages and ads them to the distributed web). Since we’re running this ĐApp in a browser, and not pinning the files on a server anywhere, they’ll eventually get garbage collected. Probably after about 24–48 hours (unless people keep accessing them). At which point, your message will be gone forever (or at least until you create it again). And the best part of all of this? The encrypted messages are accessed via the IPFS peer2peer network. No centralized servers to snoop on your communications here. That’s the power of decentralization.

No centralized servers to snoop on your communications here. That’s the power of decentralization. — tweet it

And speaking of encryption, at Textile, we’re developing a whole new way to enable secure (think encryption) photo backup and sharing on the distributed web (think IPFS). Come check us out, and jump on the Textile Photos waitlist to request early access to the future.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.