Photo by Glenn Carstens-Peters on Unsplash
How to build application on top of the mixnet: the example of Pastenym
Disclaimer: in this article, we assume the reader to have some technical background, as it is written for developers. We chose to not detail every term on purpose.
The Nym mixnet offers very interesting privacy guarantees, as we detailed in some of our previous articles. In this article, we will explain how to build your application to use the mixnet provided by Nym Technologies, to leverage the privacy guarantees it can bring. To illustrate the process, we will take our anonymous pastebin service Pastenym (v2.0.2), as an example. Do not hesitate to check the code on the repository of the application!
At the time of writing this article, our instance of Pastenym (https://pastenym.ch) is down due to some issue with some components from Nym. In the meantime, we advise you to use another instance, such as https://pastenym.mynixnode.com
If you missed what Pastenym was, make sure to read this article from our blog! If you are not familiar with the mixnet of Nym, we would advise this introduction to mixnets from their blog.
Naturally, if your application is made of different components (client-server application) or involves different actors (peer-to-peer application), these will need to communicate. We'll first review the traditional way of exchanging data between applications and then explain how to do it using the mixnet.
The traditional way of communicating
The traditional way of exchanging data between components or applications is through a REST, GraphQL or custom API, or using a custom protocol, through (web)sockets. The exchanged messages are sent using HTTP requests from a component through the internet to another component, identified by an IP address and a port.
If we wanted to have Pastenym components communicate traditionally, we would have had our React frontend call REST API endpoints on our backend in Python, for which we would probably have used an API-oriented framework such as Flask or FastAPI.
How to send & receive messages through the mixnet?
When your application communicates with the mixnet, it is not the application itself that is talking directly to the mixnet. To exchange messages on the mixnet, your component needs a nym client. The nym client is in charge of the encryption and decryption of messages, communicating with the other mixnet's components, managing the Single Use Reply Blocks (SURBs) and plenty of other things that you probably do not want to manage yourself in your application. You can learn more about nym clients in the official documentation.
Nym clients identify each other using clientID
s. A clientID
is a string, composed of three parts, delimited by two points '.'
. You can find more information on the addressing system here.
Nym clients can send each other messages, through the mixnet. They can exchange raw or text data, depending on your need and configuration. In Pastenym, we exchanged JSON payloads, containing everything we need to store and retrieve pastes!
We will now detail to use the two nym clients that we use in Pastenym: the websocket client & the Web Assembly client.
Websocket nym client
This nym client is convenient because it runs on every platform and can be used independently of the language you chose for your application. However, it requires running a standalone executable with which your application will communicate through a websocket. For these reasons, this nym client is the default choice to run on the backend side. You can find here the documentation of Nym on this client.
Here is a step-by-step guide of what to do to use the websocket nym client:
Download the websocket nym client from the releases from Nym's Github repository or build it yourself (instructions here).
Make sure that the
nym-client
is executable. You may need to adapt the following command if you want another user to be able to execute itchmod u+x nym-client
Initialize the websocket nym client as follows:
./nym-client init --id <name_of_your_choosing>
where you replaced
<name_of_your_choosing>
by some name of your choosing. You will need to remember this name when you want to run the nym client. The name is associated with theclientID
: if you use a different name, theclientID
will be different. If you want to provide a service available through the Nym network (as we do with Pastenym backend), you may not want theclientID
of your application to change.Run the websocket nym client as follows:
./nym-client run --id <name_of_your_choosing>
You cannot run the nym-client for the
<name_of_your_choosing>
without having initialized it beforehand.If this command is executed successfully, you should see a line in the logs, displaying the
clientID
of this nym client, similar to:> The address of this client is: GCdwdkpnn94YjsDqkQjXQi7mMkYxrf87LDCkdi8Wsh4f.E4edYXjPN4DZi77q4n6JEUavVwdFhFFj2J2yEGpW15Tq@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN
You may also have noticed the following line in the logs:
> Running websocket on "127.0.0.1:1977"
Indicating the address of the websocket your application should connect to, to exchange with the mixnet. Note that this websocket is only available on localhost, so your application needs to be on the same host as the nym client. If you want to run your app on a different machine (or in a docker container), you can use our nym client Docker image here.
In your application, you can now connect to the websocket at the address printed in the logs, using a websocket library available for the language of your app, as we did for Pastenym, in Python. Hereafter is an extract, you can find the whole code here:
import websocket import json import rel class Serve: def __init__(self): websocket.enableTrace(False) self.ws = websocket.WebSocketApp(f"ws://127.0.0.1:1977", on_message=lambda ws, msg: self.on_message(ws, msg), on_error=lambda ws, msg: self.on_error(ws, msg), on_close=lambda ws: self.on_close(ws), on_open=lambda ws: self.on_open(ws), on_pong=lambda ws, msg: self.on_pong(ws, msg) ) self.ws.run_forever(dispatcher=rel, ping_interval=30, ping_timeout=10) rel.signal(2, rel.abort) # Keyboard Interrupt rel.dispatch() self.ws.close() def on_message(self, ws, message): # Implement your app logic try: received_message = json.loads(message) except JSONDecodeError as e: print(f"Failed to decode JSON message: {e}") return print(f"Got: {received_message}") # on_error, on_close, on_open, on_pong functions def send_reply_to(self, recipient, message): if recipient is None or self.ws is None: print("Cannot send message: recipient or ws is None") return self.ws.send(json.dumps({ "type": "reply", "message": message, "senderTag": recipient, })
If you use Golang, we developed a module to facilitate the mixnet integration in your application, you can check it out on our repository: github.com/notrustverify/nymsocketmanager.
Web assembly client
Please note that we explain here how to use the web assembly client in a JS/TS web application, however, a new Typescript SDK has been released in early October 2023, which we will integrate into Pastenym in the near future. We might release a guide for this as well or update this article, keep an eye on our blog ;)
The web assembly client is a bit trickier to use and you might need to check our code or Nym's examples a bit more in-depth. In our case, we used it in Pastenym frontend, in React and the whole application is packaged with Webpack. You can find the documentation for the web assembly client here.
You first need to install the Nym SDK, which eases the use of the Wasm client, using the following command, from your project directory:
npm install @nymproject/sdk
# you can of course use yarn or other package managers
In our React frontend, we grouped the connection to the Nym client in a file (this one), where we do the following:
import { createNymMixnetClient } from '@nymproject/sdk'
export async function connectMixnet() {
const nym = await createNymMixnetClient()
const nymApiUrl = 'https://validator.nymtech.net/api'
const preferredGatewayIdentityKey = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM'
// WSS is mandatory for HTTPS website
const gatewayListener = 'wss://gateway1.nymtech.net:443'
// start the client and connect to a gateway
await nym.client.start({
clientId: '<another_name_of_your_choosing>',
nymApiUrl,
preferredGatewayIdentityKey: preferredGatewayIdentityKey,
gatewayListener: gatewayListener,
})
return nym
}
We can see that we use Nym's library to create a nym client and we then start with specific parameters, among which, a clientID
, which you are invited to change, as for the websocket nym client.
The nym
object returned by the Nym library, after being started, allows to subscribe functions with nym.events
.subscribeToConnected
, nym.events
.subscribeToTextMessageReceivedEvent
, or nym.events
.subscribeToRawMessageReceivedEvent
to receive messages, as well as to send messages to the mixnet using nym.client.Send
or nym.client.rawSend
.
The above piece of code allows us to call the connectMixnet()
function in our components. For instance, in the UserText.js component (the landing page of the application), we have the following piece of code that will, once the component has been mounted, load the Nym client and instruct callback functions to be called upon reception of messages from the mixnet (here). In our case, it is the displayReceived
function of the UserInput
component that will handle the messages received from the mixnet.
import * as React from 'react'
import { connectMixnet } from './context/createConnection'
class UserInput extends React.Component {
// Plenty of other stuff here
async componentDidMount() {
this.nym = await connectMixnet()
// When connected, we receive a message with our clientID
this.nym.events.subscribeToConnected((e) => {
if (e.args.address) {
// We store our clientID in the component's state
this.setState({ self_address: e.args.address })
}
})
this.nym.events.subscribeToRawMessageReceivedEvent((e) => {
this.displayReceived(this.decoder.decode(e.args.payload))
})
}
async sendMessageTo(payload) {
if (!this.nym) {
console.error('Could not send message because worker does not exist')
return
}
await this.nym.client.rawSend({
payload: this.encoder.encode(payload),
recipient: recipient
})
}
}
This is how we can use the Nym SDK to have a frontend communicating with the mixnet.
Conclusion
And that's it! You now know how to send messages to and receive messages from the mixnet from your application! You will maybe only use one of these two nym clients, maybe both, or maybe another one, it will depend on your application. We did not mentioned about SURBs, the last nym client, or the latest Typescript SDK to keep it simple, however you can always check the official documentation, Nym's examples or our code to learn more about these!
Warning from the privacy wizard
It is not because an application uses the mixnet that the privacy guarantees of the mixnet are still applying to your application or that your application will be perfectly private! In other words, using the mixnet is a necessary but not sufficient condition to respect the privacy of the users.
If your application requires users to provide a unique identifier (pseudo, email, phone number,...), to connect the an account for instance, then the sender of a message will still be identifiable, despite the use of the mixnet. The "sender pseudonimity" property will be lost, as the sender will be identifiable.
Similarly, if your application requires the user to visit a specific URL or endpoint to use a specific feature of your application (/login
, /sendMessage
, /profile?id=1234, ...
), each use of the feature will result in an HTTP request, that will not go through the mixnet and will leak information about the user and their activity to the ISP and other internet intermediaries.
For these reasons, your application should be designed with privacy as the default and each implementation choice should be carefully thought to prevent harming the privacy of the users.