InterPlanetary Applications: Disco Chat

InterPlanetary Applications: Disco Chat

Screenshot of Disco Chat
Disco Chat is a peer-to-peer chat application built using Tauri, JavaScript, and HTML.

# Table of Contents

In this post we'll go over what Disco Chat is, and the journey of how and why Disco Chat was built. I'll also highlight some of the challenges that were encountered along the way. Having some familiarity with web development will be handy for following along with the technical sections. There's also a Rust component of the application too, but knowledge of Rust isn't necessary to follow along.

# What is Disco Chat?

Disco Chat is a fun, easy-to-use peer-to-peer chat application sporting custom profiles, end-to-end encryption, offline chat sync, and more! It's meant to help show developers how to build applications like it. If you'd like to join in the fun (even if you're not very technical), head over to the Disco Chat Releases (opens new window) page to grab a binary.

At it's core Disco Chat uses IPFS & libp2p, powered by a Kubo (opens new window) node. It's written in mostly JavaScript with the interface in HTML/CSS to hopefully make it easy-to-use for most developers today. There's also a Rust component from Tauri (opens new window) which allows us to have a thin browser to work with on the desktop (and mobile in the future!), giving us the full power of Kubo, JavaScript, HTML, and CSS in a single application.

Disco Chat is based upon native-ipfs-building-blox (opens new window), which is a great kit for getting started on building desktop IPFS applications using HTML/Javascript (Rust too if you like). If you're looking at taking some code snippets from Disco Chat for a totally different idea, I highly recommend using native-ipfs-building-blox as a base.

# Disco Chat's Journey

Disco Chat had its beginnings as a truely minimal in-browser chat application. You could set a nickname and write messages into a public channel, the backend could support multiple "rooms" but the frontend could not. This application exists today as the write-up Create a Simple Chat Application (opens new window) and a video titled IPFS: Browser Connectivity Walkthrough (opens new window). Its goal, similar to Disco Chat, was to provide developers with the scaffolding they'd need to build an in-browser chat application, or whatever else they could dream up.

From there Disco Chat was born! After building the framework I thought it'd be fun to make my own chat application and see what struggles are involved with it. At this point Disco Chat still worked in a browser, complete with multiple-rooms, markdown, image sharing, video sharing, emojis, multi-line text, and more! It was exciting opening up a web page that instantly connects you to a serverless chatroom with other peers.

Disco Chat, and the original chat example relied on CircuitRelayV1 (opens new window) and libp2p-webrtc-star (opens new window). Unfortunately these days it's much harder to achieve relaying in a browser with the deprecation of CirvuitRelayV1 in Kubo. On top of this, the browser environments practically strangle most p2p techniques making things like hole-punching very hard. That was already enough to convince me to persue other avenues, but now libp2p-webrtc-star is also deprecated in favour of an upcoming libp2p WebRTC browser-to-browser specification (opens new window). So continuing on the browser wasn't really feasible for the time being.

One day I'd love to see the browser version return, but for now we can continue work in a pseudo-browser environment so porting it back to browser should be relatively straight-forward.

# Disco Chat Today

I wanted to keep Disco Chat in the browser as it's easy to create graphical applications that work cross-platform, not to mention the current popularity of Javascript. First I looked at Electron, but I'm haunted by many complaints developers and users have about the behemoth (not to mention it's quite large). After some searching I found Tauri, a very thin/lightweight browser view tied together with Rust.

From there I created native-ipfs-building-blox (opens new window) to assist in creating desktop applications with a webview powered by a Kubo node in the background. The API on the Javascript side is exactly the same, but by using kubp-rpc-client (opens new window) it's Kubo handling all the heavy lifting like running the IPFS node instead of js-ipfs. This gives us a lot of powers like advanced hole punching techniques using CircuitRelayV2 and being able to listen on a socket in general.

After this I just dropped the browser-based Disco Chat into the ui directory (opens new window) with some tweaks. The browser version of Disco Chat used js-ipfs in the browser which was no longer needed in the new Disco Chat which uses Kubo and Kubo RPC API. After this, I cleaned up that ui directory, added a boatload of comments, and implemented IPNS-based profiles and end-to-end encryption.

# Disco Chat's Features

Disco Chat has many features, all of which operate peer-to-peer, some notable ones:

  • Chat rooms (not encrypted)
  • Encrypted direct messages
  • Emojis
  • Markdown
  • Image sharing
  • Video sharing

And these are cool, but nothing that stands out as special in the web3 space (except maybe serverless & chainless chat rooms). To make Disco Chat stand out with some useful features other devs could learn from I did some brainstorming and reading with the community and my fellow teammates. I concluded many devs get hung up on at least these two things in a distributed and chainless/consensusless environment:

  1. How to have mutable data beyond just a website / redirect
  2. How to encrypt or hide data

So to assist with those problems, I created IPNS-based profiles for Disco Chat - to show off mutable data, and a simple end-to-end encryption feature - to show the basics of how to hide or encrypt data over IPFS (or any public room). This was made possible as each IPFS node has a unique identifier known as a PeerID which is generated via a keypair. I utilise the PeerID as the IPNS name for profile lookups, and the keypair to generate a secret used for data encryption.

# How Disco Chat Uses IPNS for Mutable Profiles

Previously, in the chat example a user's nickname was sent over with each message send. As it evolved into Disco Chat, a CID linking to a profile picture was also sent over with each message. Now, the profile information isn't sent over at all! Instead a CID of the profile info itself is published via IPNS (opens new window) using the user's PeerID (also known as their self key!). This means if a peer user wants to look another peer up, they already know the peer's PeerID thanks to libp2p, so they only need to do an IPNS lookup on it to retrieve the profile info as needed / desired.

# IPNS Profiles Technical Breakdown

The meat of the IPNS Profiles code lives in ui/peers/ipns.js (opens new window), here's a slightly modified example, which might be easier to snatch up as a snippet:

// fetchPeerInfo will try to resolve a PeerID over IPNS to get their profile information, returning it as an object
async function fetchPeerInfo(ipfs, id, timeout) {
  if (timeout == undefined) {
    timeout = 5000;
  }
  let peer = undefined;
  for await (const name of ipfs.name.resolve(id, {timeout: timeout})) {
    peer = name;
  }
  if (peer == undefined) {
    return;
  }
  const content = [];
  try {
    for await (const chunk of ipfs.cat(peer, {timeout: timeout, length: 1024})) {
      content.push(chunk);
    }
  } catch {
    return;
  }
  if (content.length == 0) {
    return;
  }
  try {
    peer = JSON.parse(new TextDecoder().decode(content[0]));
  } catch {
    return;
  }
  return peer;
}

// publishProfile publishes our profile information to IPNS
async function publishProfile(ipfs, currentNick, currentImg) {
  let cid = await ipfs.add(JSON.stringify({nick: currentNick, img: currentImg}));
  await ipfs.name.publish(cid.cid);
}

We have two functions here: publishProfile and fetchPeerInfo.

publishProfile is what publishes your profile info to IPNS. It takes three arguments:

  • ipfs - an IPFS core API (opens new window) compatible object
  • currentNick - a string representing the peer's nickname
  • currentImg - a string CID representing the peer's avatar

fetchPeerInfo will fetch another peer's profile information, parse the JSON, and return an object (returning undefined upon error). It takes three arguments:

  • ipfs - an IPFS core API (opens new window) compatible object
  • id - a string representing the peer's PeerID
  • timeout (optional) - an integer representing the time to wait while searching for the profile in milliseconds

I hope the usage is straight-forward, here's an example:

// Connect to a local Kubo node
var ipfs = await IpfsHttpClient.create();
// Retrieve our PeerID as a string, and output it to console
var me = await ipfs.id().then(id => id.id.toString());
console.log(me);

// Publish our profile with the nickname "Example_Nickname"
// and picture "Qmcm32sVsMYhURY3gqH7vSQ76492t5Rfxb3vsWCb35gVme"
await publishProfile(ipfs, "Example_Nickname", "Qmcm32sVsMYhURY3gqH7vSQ76492t5Rfxb3vsWCb35gVme");

// Retrieve our own profile info
let peerInfo = await fetchPeerInfo(ipfs, me);
// Output our profile info as JSON to console
console.log("Got peer info: " + JSON.stringify(peerInfo));

# How Disco Chat Implements End-to-End Encryption

I often see questions along the lines of "How do I hide the data in a CID?" or sometimes "How do I encrypt the data before I add it to IPFS?". For Disco Chat I implemented one simple scheme anyone can use. It will encrypt any data you want for a specific peer. This ensures you and the peer you're communicating with are the only two people who can decrypt the data.

# End-to-End Encryption Technical Breakdown

I highly recommend studying the security implications of using encryption in the wild or seeking consulting on the subject before using a specific scheme.

Disco Chat's encryption example lives in ui/crypto.js (opens new window). You'll need these libraries to get started:

To decrypt a message all a reciever needs is the nonce used to generate the encrypted message, and the encrypted message itself. Here's a code snippet:

// decryptMsg is called by the receiver to decrypt the message. It takes the
// encrypted message (msg), the sender's public key (from), and the unique
// nonce used to encrypt the message (n). If this message is from ourselves
// (me), it will use the `to` key to decrypt it.
//
// _priv_key is the private key to our PeerID.
// 
// This function can only decrypt messages meant for us, or messages sent by
// us.
async function decryptMsg(msg, n, from, to, me, _priv_key) {
  let other_pub = null;
  if (from != me) {
    other_pub = bs58.decode(from).subarray(6);
  } else {
    other_pub = bs58.decode(to).subarray(6);
  }
  let secret = await nobleEd25519.getSharedSecret(_priv_key, other_pub);
  let encryptedBytes = aesjs.utils.hex.toBytes(msg);
  let aesCtr = new aesjs.ModeOfOperation.ctr(secret, new aesjs.Counter(parseInt(n)));
  return aesjs.utils.utf8.fromBytes(aesCtr.decrypt(encryptedBytes));
}

// encryptMsg is called by the sender to encrypt a message. It takes the
// message (msg) and the receiver's public key (to). It returns a list
// containing the unique nonce and the encrypted message.
//
// _priv_key is the private key to our PeerID.
async function encryptMsg(msg, to, _priv_key) {
  let other_pub = bs58.decode(to).subarray(6);
  let secret = await nobleEd25519.getSharedSecret(_priv_key, other_pub);
  let uniqueN = window.crypto.getRandomValues(new Uint16Array(1))[0];
  let aesCtr = new aesjs.ModeOfOperation.ctr(secret, new aesjs.Counter(uniqueN));
  let encryptedBytes = aesCtr.encrypt(aesjs.utils.utf8.toBytes(msg));
  return [encryptedBytes, uniqueN];
}

decryptMsg decrypts the encrypted message fed to it. It takes six arguments:

  • msg - a string of the encrypted message to decrypt
  • n - an integer representing the nonce used to encrypt the message
  • from - a string CID representing the sender's PeerID
  • to - a string CID representing the reciever's PeerID
  • me - a string CID representing our own PeerID
  • _priv_key - the private key of our own PeerID

It returns a string of the decrypted message.

encryptMsg encrypts the message fed to it. It takes three arguments:

  • msg - a string to encrypt
  • to - a string CID representing the reciever's PeerID
  • _priv_key - the private key of our own PeerID

It returns a list containing [encrypted data, nonce]. Both the encrypted data and the nonce will be needed the decrypt the message.

It's important to note that these functions generate the secret every single time. This is a moderately heavy operation, which technically only needs to be done once per peer. So if you're looking for performance, you should be caching secrets. This is left as an exercise for the reader 🙂.

The biggest challenge to using these functions will likely be in retrieving your node's private key. For Kubo, this is stored by default in your config file (~/.ipfs/config) and needs to be decoded. In Disco Chat, this is done on the Rust side in src/main.rs (opens new window). The native-ipfs-building-blox (opens new window) scaffolding has the Rust side of this built-in, and just needs to be exposed on the JavaScript side:

// Connect to a local Kubo node
var ipfs = await IpfsHttpClient.create();
// Retrieve our PeerID as a string, and output it to console
var me = await ipfs.id().then(id => id.id.toString());
console.log(me);
// A different PeerID for example purposes
var notMe = "12D3KooWQLS5bagAnf43kSdf5Nd9yiMW2sBksL43q94nduXzpdpV";

// retrieve our private key
let priv = await __TAURI__.invoke('get_priv_key');
// convert our key into something nobleEd25519 likes
var _priv_key = Uint8Array.from(atob(priv), c => c.charCodeAt(0));

// run encryptMsg to get the encryptedMessage and nonce
let [encryptedMessage, nonce] = await encryptMsg("hello world", notMe, _priv_key);
// we set "from" to "me" because the message is from us
let decryptedMessage = await decryptMsg(aesjs.utils.hex.fromBytes(encryptedMessage), nonce, me, notMe, me, _priv_key);

console.log(decryptedMessage); // "hello world"

# Wrapping Up

Today Disco Chat is one of the best examples to help get developers started building peer-to-peer applications with distributed profiles and encrypted data. It's also a fully-featured chat application that anyone can use. Below I have a couple buttons to grab your own copy of Disco Chat, as well as some links to learn more about libp2p and IPFS. Let's all build and use the future of the web together, today.


Download DiscoChat Explore the code on GitHub