This is the second episode of a two-part tutorial. While the first article (ZeroMQ & Node.js Tutorial – Cracking JWT Tokens) was solely focused on theory, this one is about the actual coding.
You’ll get to know ZeroMQ, how JWT tokens work and how our application can crack some of them! Be aware, that the application will be intentionally simple. I only want to demonstrate how we can leverage some specific patterns.
At the end of the article, I’ll invite you to participate in a challenge and to use your newly acquired knowledge for cracking a JWT token. The first 3 developers who crack the code will get a gift!
Let’s get it started!
Preparing the environment and the project folder
To follow this tutorial, you will need to have the ZeroMQ libraries and Node.jsNode.js is an asynchronous event-driven JavaScript runtime and is the most effective when building scalable network applications. Node.js is free of locks, so there's no chance to dead-lock any process. version >=4.0
installed in your system. We will also need to initialize a new project with the following commands:
npm init # then follow the guided setup
npm install --save big-integer@^1.6.16 dateformat@^1.0.12 indexed-string-variation@^1.0.2 jsonwebtoken@^7.1.9 winston@^2.2.0 yargs@5.0.0 zmq@^2.15.3
This will make sure that you have all the dependencies ready in the project folder and you can only focus on the code.
You can also checkout the code in the projects’ official GitHub repository and keep it aside as a working reference.
Writing the client application (Dealer + Subscriber) with ZeroMQ and Node.js
We should finally have a clear understanding of the whole architecture and the patterns we are going to use. Now we can finally focus on writing code!
Let’s start with the code representing the client, which holds the real JWT-cracking business logic.
As a best practice, we are going to use a modular approach, and we will split our client code into four different parts:
- The
processBatch
module, containing the core logic to process a batch. - The
createDealer
module containing the logic to handle the messages using the ZeroMQ dealer pattern. - The
createSubscriber
module containing the logic to handle the exit message using the subscriber pattern. - The
client
executable script that combines all the modules together and offers a nice command-line interface.
The processBatch module
The first module that we are going to build will focus only on analyzing a given batch and checking if the right password is contained in it.
This is probably the most complex part of our whole application, so let’s make some useful preambles:
- We are going to use the big-integer library to avoid approximation problems with large integers. In fact, in JavaScript all numbers are internally represented as floating point numbers and thus they are subject to floating point approximation. For example the expression
10000000000000000 === 10000000000000001
(notice the last digit) will evaluate totrue
. If you are interested in this aspect of the language, you can read more here](http://greweb.me/2013/01/be-careful-with-js-numbers/). All the maths in our project will be managed by the big-integer library. If you have never used it before, it might look a bit weird at first, but I promise it won’t be hard to understand. - We are also going to use the jsonwebtoken library to verify the signature of a given token against a specific password.
Let’s finally see the code of the processBatch
module:
// src/client/processBatch.js
'use strict';
const bigInt = require('big-integer');
const jwt = require('jsonwebtoken');
const processBatch = (token, variations, batch, cb) => {
const chunkSize = bigInt(String(1000));
const batchStart = bigInt(batch[0]);
const batchEnd = bigInt(batch[1]);
const processChunk = (from, to) => {
let pwd;
for (let i = from; i.lesser(to); i = i.add(bigInt.one)) {
pwd = variations(i);
try {
jwt.verify(token, pwd, {ignoreExpiration: true, ignoreNotBefore: true});
// finished, password found
return cb(pwd, i.toString());
} catch (e) {}
}
// prepare next chunk
from = to;
to = bigInt.min(batchEnd, from.add(chunkSize));
if (from === to) {
// finished, password not found
return cb();
}
// process next chunk
setImmediate(() => processChunk(from, to));
};
const firstChunkStart = batchStart;
const firstChunkEnd = bigInt.min(batchEnd, batchStart.add(chunkSize));
setImmediate(() => processChunk(firstChunkStart, firstChunkEnd));
};
module.exports = processBatch;
(Note: This is a slightly simplified version of the module, you can check out the original one in the official repository which also features a nice animated bar to report the batch processing progress on the console.)
This module exports the processBatch
function, so first things first, let’s analyze the arguments of this function:
token
: The current JWT token.variations
: An instance of indexed-string-variations already initialized with the current alphabet.batch
: An array containing two strings representing the segment of the solution space where we search for the password (e.g.['22', '150']
).cb
: A callback function that will be invoked on completion. If the password is found in the current batch, the callback will be invoked with the password and the current index as arguments. Otherwise, it will be called without arguments.
This function is asynchronous, and it is the one that will be executed most of the time in the client.
The main goal is to iterate over all the numbers in the range, and generate the corresponding string on the current alphabet (using the variations
function) for every number.
After that, the string is checked against jwt.verify
to see if it’s the password we were looking for. If that’s the case, we immediately stop the execution and invoke the callback, otherwise the function will throw an error, and we will keep iterating until the current batch is fully analyzed. If we reach the end of the batch without success, we invoke the callback with no arguments to notify the failure.
What’s peculiar here is that we don’t really execute a single big loop to cover all the batch elements, but instead we define an internal function called processChunk
that has the goal of executing asynchronously the iteration in smaller chunks containing at most 1000 elements.
We do this because we want to avoid to block the event loop for too long, so, with this approach, the event loop has a chance to react to some other events after every chunk, like a received exit signal.
(You can read much more on this topic in the last part of Node.js Design Patterns Second Edition).
CreateDealer module
The createDealer
module holds the logic that is needed to react to the messages received by the server through the batchSocket
, which is the one created with the router/dealer pattern.
Let’s jump straight into the code:
// src/client/createDealer.js
'use strict';
const processBatch = require('./processBatch');
const generator = require('indexed-string-variation').generator;
const createDealer = (batchSocket, exit, logger) => {
let id;
let variations;
let token;
const dealer = rawMessage => {
const msg = JSON.parse(rawMessage.toString());
const start = msg => {
id = msg.id;
variations = generator(msg.alphabet);
token = msg.token;
logger.info(`client attached, got id "${id}"`);
};
const batch = msg => {
logger.info(`received batch: ${msg.batch[0]}-${msg.batch[1]}`);
processBatch(token, variations, msg.batch, (pwd, index) => {
if (typeof pwd === 'undefined') {
// request next batch
logger.info(`password not found, requesting new batch`);
batchSocket.send(JSON.stringify({type: 'next'}));
} else {
// propagate success
logger.info(`found password "${pwd}" (index: ${index}), exiting now`);
batchSocket.send(JSON.stringify({type: 'success', password: pwd, index}));
exit(0);
}
});
};
switch (msg.type) {
case 'start':
start(msg);
batch(msg);
break;
case 'batch':
batch(msg);
break;
default:
logger.error('invalid message received from server', rawMessage.toString());
}
};
return dealer;
};
module.exports = createDealer;
This module exports a factory function used to initialize our dealer component. The factory accepts three arguments:
batchSocket
: the ZeroMQ socket used to implement the dealer part of the router/dealer pattern.exit
: a function to end the process (it will generally beprocess.exit
).logger
: a logger object (theconsole
object or a winston logger instance) that we will see in detail later.
The arguments exit
and logger
are requested from the outside (and not initialized within the module itself) to make the module easily “composable” and to simplify testing (we are here using the Dependency Injection pattern).
The factory returns our dealer function which in turn accepts a single argument, the rawMessage
received through the batchSocket channel.
This function has two different behaviors depending on the type of the received message. We assume the first message is always a start message that is used to propagate the client id, the token and the alphabet. These three parameters are used to initialize the dealer. The first batch is also sent with them, so after the initialization, the dealer can immediately start to process it.
The second message type is the batch, which is used by the server to deliver a new batch to analyze to the clients.
The main logic to process a batch is abstracted in the batch
function. In this function, we simply delegate the processing job to our processBatch
module. If the processing is successful, the dealer creates a success message for the router – transmitting the discovered password and the corresponding index over the given alphabet. If the batch doesn’t contain the password, the dealer sends a next message to the router to request a new batch.
CreateSubscriber module
In the same way, we need an abstraction that allows us to manage the pub/sub messages on the client. For this purpose we can have the createSubscriber
module:
// src/client/createSubscriber.js
'use strict';
const createSubscriber = (subSocket, batchSocket, exit, logger) => {
const subscriber = (topic, rawMessage) => {
if (topic.toString() === 'exit') {
logger.info(`received exit signal, ${rawMessage.toString()}`);
batchSocket.close();
subSocket.close();
exit(0);
}
};
return subscriber;
};
module.exports = createSubscriber;
This module is quite simple. It exports a factory function that can be used to create a subscriber (a function able to react to messages on the pub/sub channel). This factory function accepts the following arguments:
subSocket
: the ZeroMQ socket used for the publish/subscribe messages.batchSocket
: the ZeroMQ socket used for the router/dealer message exchange (as we saw in thecreateDealer
module).exit
andlogger
: as in thecreateDealer
module, these two arguments are used to inject the logic to terminate the application and to record logs.
The factory function, once invoked, returns a subscriber
function which contains the logic to execute every time a message is received through the pub/sub socket. In the pub/sub model, every message is identified by a specific topic
. This allows us to react only to the messages referring to the exit topic and basically shut down the application. To perform a clean exit, the function will take care of closing the two sockets before exiting.
Command line client script
Finally, we have all the pieces we need to assemble our client application. We just need to write the glue between them and expose the resulting application through a nice command line interface.
To simplify the tedious task of parsing the command line arguments, we will use the yargs library:
// src/client.js
#!/usr/bin/env node
'use strict';
const zmq = require('zmq');
const yargs = require('yargs');
const logger = require('./logger');
const createDealer = require('./client/createDealer');
const createSubscriber = require('./client/createSubscriber');
const argv = yargs
.usage('Usage: $0 [options]')
.example('$0 --host=localhost --port=9900 -pubPort=9901')
.string('host')
.default('host', 'localhost')
.alias('h', 'host')
.describe('host', 'The hostname of the server')
.number('port')
.default('port', 9900)
.alias('p', 'port')
.describe('port', 'The port used to connect to the batch server')
.number('pubPort')
.default('pubPort', 9901)
.alias('P', 'pubPort')
.describe('pubPort', 'The port used to subscribe to broadcast signals (e.g. exit)')
.help()
.version()
.argv
;
const host = argv.host;
const port = argv.port;
const pubPort = argv.pubPort;
const batchSocket = zmq.socket('dealer');
const subSocket = zmq.socket('sub');
const dealer = createDealer(batchSocket, process.exit, logger);
const subscriber = createSubscriber(subSocket, batchSocket, process.exit, logger);
batchSocket.on('message', dealer);
subSocket.on('message', subscriber);
batchSocket.connect(`tcp://${host}:${port}`);
subSocket.connect(`tcp://${host}:${pubPort}`);
subSocket.subscribe('exit');
batchSocket.send(JSON.stringify({type: 'join'}));
In the first part of the script we use yargs
to describe the command line interface, including a description of the command with a sample usage and all the accepted arguments:
host
: is used to specify the host of the server to connect to.port
: the port used by the server for the router/dealer exchange.pubPort
: the port used by the server for the pub/sub exchange.
This part is very simple and concise. Yargs will take care of performing all the validations of the input and populates the optional arguments with default values in case they are not provided by the user. If some argument doesn’t meet the expectations, Yargs will take care of displaying a nice error message. It will also automatically create the output for --help
and --version
.
In the second part of the script, we use the arguments provided to connect to the server, creating the batchSocket
(used for the router/dealer exchange) and the subSocket
(used for the pub/sub exchange).
We use the createDealer
and createSubscriber
factories to generate our dealer and subscriber functions and then we associate them with the message event of the corresponding sockets.
Finally, we subscribe to the exit topic on the subSocket
and send a join
message to the server using the batchSocket
.
Now our client is fully initialized and ready to respond to the messages coming from the two sockets.
The server
Now that our client application is ready we can focus on building the server. We already described what will be the logic that the server application will adopt to distribute the workload among the clients, so we can jump straight into the code.
CreateRouter
For the server, we will build a module that contains most of the business logic – the createRouter
module:
// src/server/createRouter.js
'use strict';
const bigInt = require('big-integer');
const createRouter = (batchSocket, signalSocket, token, alphabet, batchSize, start, logger, exit) => {
let cursor = bigInt(String(start));
const clients = new Map();
const assignNextBatch = client => {
const from = cursor;
const to = cursor.add(batchSize).minus(bigInt.one);
const batch = [from.toString(), to.toString()];
cursor = cursor.add(batchSize);
client.currentBatch = batch;
client.currentBatchStartedAt = new Date();
return batch;
};
const addClient = channel => {
const id = channel.toString('hex');
const client = {id, channel, joinedAt: new Date()};
assignNextBatch(client);
clients.set(id, client);
return client;
};
const router = (channel, rawMessage) => {
const msg = JSON.parse(rawMessage.toString());
switch (msg.type) {
case 'join': {
const client = addClient(channel);
const response = {
type: 'start',
id: client.id,
batch: client.currentBatch,
alphabet,
token
};
batchSocket.send([channel, JSON.stringify(response)]);
logger.info(`${client.id} joined (batch: ${client.currentBatch[0]}-${client.currentBatch[1]})`);
break;
}
case 'next': {
const batch = assignNextBatch(clients.get(channel.toString('hex')));
logger.info(`client ${channel.toString('hex')} requested new batch, sending ${batch[0]}-${batch[1]}`);
batchSocket.send([channel, JSON.stringify({type: 'batch', batch})]);
break;
}
case 'success': {
const pwd = msg.password;
logger.info(`client ${channel.toString('hex')} found password "${pwd}"`);
// publish exit signal and closes the app
signalSocket.send(['exit', JSON.stringify({password: pwd, client: channel.toString('hex')})], 0, () => {
batchSocket.close();
signalSocket.close();
exit(0);
});
break;
}
default:
logger.error('invalid message received from channel', channel.toString('hex'), rawMessage.toString());
}
};
router.getClients = () => clients;
return router;
};
module.exports = createRouter;
The first thing to notice is that we built a module that exports a factory function again. This function will be used to initialize an instance of the logic used to handle the router part of the router/dealer pattern in our application.
The factory function accepts a bunch of parameters. Let’s describe them one by one:
batchSocket
: is the ZeroMQ socket used to send the batch requests to the clients.signalSocket
: is the ZeroMQ socket to publish the exit signal to all the clients.token
: the string containing the current token.alphabet
: the alphabet used to build the strings in the solution space.batchSize
: the number of strings in every batch.start
: the index from which to start the first batch (generally ‘0’).logger
: an instance of the loggerexit
: a function to be called to shut down the application (usuallyprocess.exit
).
Inside the factory function, we declare the variables that define the state of the server application: cursor
and clients
. The first one is the pointer to the next batch, while the second is a map structure used to register all the connected clients and the batches assigned to them. Every entry in the map is an object containing the following attributes:
id
: the id given by ZeroMQ to the client connection.channel
: a reference to the communication channel between client and server in the router/dealer exchange.joinedAt
: the date when the client established a connection to the server.currentBatch
: the current batch being processed by the client (an array containing the two delimiters of the segment of the solution space to analyze).currentBatchStartedAt
: the date when the current batch was assigned to the client.
Then we define two internal utility functions used to change the internal state of the router instance: assignNextBatch
and addClient
.
The way these functions work is pretty straightforward: the first one assigns the next available batch to an existing client and moves the cursors forward, while the second takes input a new ZeroMQ connection channel as an input and creates the corresponding entry in the map of connected clients.
After these two helper functions, we define the core logic of our router with the router
function. This function is the one that is returned by the factory function and defines the logic used to react to an incoming message on the router/dealer exchange.
As it was happening for the client, we can have different type of messages, and we need to react properly to every one of them:
- join: received when a client connects to the server for the first time. In this case, we register the client and send it the settings of the current run and assign it the first batch to process. All this information is provided with a start message, which is sent on the router/dealer channel (using the ZeroMQ
batchSocket
). - next: received when a client finishes to process a batch without success and needs a new batch. In this case we simply assign the next available batch to the client and send the information back to it using a batch message through the
batchSocket
. - success: received when a client finds the password. In this case, the found password is logged and propagated to all the other clients with an exit signal through the
signalSocket
(the pub/sub exchange). When the exit signal broadcast is completed, the application can finally shut down. It also takes care to close the ZeroMQ sockets, for a clean exit.
That’s mostly it for the implementation of the router logic.
However, it’s important to underline that this implementation is assuming that our clients always deliver either a success message or a request for another batch. In a real world application, we must take into consideration that a client might fail or disconnect at any time and manages to redistribute its batch to some other client.
The server command line
We have already written most of our server logic in the createRouter
module, so now we only need to wrap this logic with a nice command line interface:
// src/server.js
#!/usr/bin/env node
'use strict';
const zmq = require('zmq');
const isv = require('indexed-string-variation');
const yargs = require('yargs');
const jwt = require('jsonwebtoken');
const bigInt = require('big-integer');
const createRouter = require('./server/createRouter');
const logger = require('./logger');
const argv = yargs
.usage('Usage: $0 <token> [options]')
.example('$0 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ')
.demand(1)
.number('port')
.default('port', 9900)
.alias('p', 'port')
.describe('port', 'The port used to accept incoming connections')
.number('pubPort')
.default('pubPort', 9901)
.alias('P', 'pubPort')
.describe('pubPort', 'The port used to publish signals to all the workers')
.string('alphabet')
.default('alphabet', isv.defaultAlphabet)
.alias('a', 'alphabet')
.describe('alphabet', 'The alphabet used to generate the passwords')
.number('batchSize')
.alias('b', 'batchSize')
.default('batchSize', 1000000)
.describe('batchSize', 'The number of attempts assigned to every client in a batch')
.number('start')
.alias('s', 'start')
.describe('start', 'The index from where to start the search')
.default('start', 0)
.help()
.version()
.check(args => {
const token = jwt.decode(args._[0], {complete: true});
if (!token) {
throw new Error('Invalid JWT token: cannot decode token');
}
if (!(token.header.alg === 'HS256' && token.header.typ === 'JWT')) {
throw new Error('Invalid JWT token: only HS256 JWT tokens supported');
}
return true;
})
.argv
;
const token = argv._[0];
const port = argv.port;
const pubPort = argv.pubPort;
const alphabet = argv.alphabet;
const batchSize = bigInt(String(argv.batchSize));
const start = argv.start;
const batchSocket = zmq.socket('router');
const signalSocket = zmq.socket('pub');
const router = createRouter(
batchSocket,
signalSocket,
token,
alphabet,
batchSize,
start,
logger,
process.exit
);
batchSocket.on('message', router);
batchSocket.bindSync(`tcp://*:${port}`);
signalSocket.bindSync(`tcp://*:${pubPort}`);
logger.info(`Server listening on port ${port}, signal publish on port ${pubPort}`);
We make the arguments’ parsing very easy by using yargs again. The command must be invoked specifying a token as the only argument and must support several options:
port
: used to specify in which port the batchSocket will be listening.pubPort
: used to specify which port will be used to publish theexit
signal.alphabet
: a string containing all the characters in the alphabet we want to use to build all the possible strings used for the brute force.batchSize
: the size of every batch forwarded to the clients.start
: an index from the solution space from where to start the search (generally 0). Can be useful if you already analyzed part of the solution space.
In this case, we also add a check
function to be sure that the JWT token we receive as an argument is well formatted and uses the HS256 algorithm for the signature.
In the rest of the code we initialize two ZeroMQ sockets: batchSocket
and signalSocket
– and we take them along with the token and the options received from the command line to initialize our router through the createRouter
function that we wrote before.
Then we register the router listener to react to all the messages received on the batchSocket.
Finally, we bind our sockets to their respective ports to start to listen for incoming connections from the clients.
This completes our server application, and we are almost ready to give our little project a go. Hooray!
Logging utility
The last piece of code that we need is our little logger
instance. We saw it being used in many of the modules we wrote before – so now let’s code this missing piece.
As we briefly anticipated earlier, we are going to use winston for the logging functionality of this app.
We need a timestamp close to every log line to have an idea about how much time our application is taking to search for a solution – so we can write the following module to export a configured instance of winston that can simply import in every module and be ready to use:
// src/logger.js
'use strict';
const dateFormat = require('dateformat');
const winston = require('winston');
module.exports = new (winston.Logger)({
transports: [
new (winston.transports.Console)({
timestamp: () => dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss'),
colorize: true
})
]
});
Notice, that we are just adding the timestamp with a specific format of our choice and then enabling the colorized output on the console.
Winston can be configured to support multiple transport layers like log files, network and syslog, so, if you want, you can get really fancy here and make it much more complex.
Running the application
We are finally ready to give our app a spin, let’s brute force some JWT tokens!
Our token of choice is the following:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
This token is the default one from jwt.io and its password is secret
.
To run the server, we need to launch the following command:
node src/server.js eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
This command starts the server and initializes it with the default alphabet (abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789
). Considering that the password is long enough to keep our clients busy for a while and also that we already know the token password, we can cheat a little bit and specify a much smaller alphabet to speed up the search of the solution. If you feel like wanting to take a shortcut add the option -a cerst
to the server start command!
Now you can run any number of clients in separate terminals with:
node src/client.js
After the first client is connected, you will start to see the activity going on in both the server and the client terminals. It might take a while to discover the password – depending on the number of clients you run, the power of your local machine and the alphabet you choose to use.
In the following picture you can see an example of running both the server (left column) and four clients (right column) applications on the same machine:
In a real world case, you might want to run the server on a dedicated machine and then use as many machines as possible as clients. You could also run many clients per machine, depending on the number of cores in every machine.
Wrapping up
We are at the end of this experiment! I really hope you had fun and that you learned something new about Node.js, ZeroMQ and JWT tokens.
If you want to keep experimenting with this example and improve the application, here there are some ideas that you might want to work on:
- Limit execution to a maximum string length and offer estimation on the elapsed time
- Ability to restore the server with its internal state after a failure or a manual shutdown
- Ability to monitor clients and reassign their ongoing batches in case of failure
- Multi level architecture
- Server web interface
Also, if you want to learn more about other Node.js design patternsIf you encounter a problem that you think someone else solved already, there's a good chance that you can find a design pattern for it. Design patterns are "blueprints" prepared in a way to solve one (or more) problems in a way that's easy to implement and reuse. It also helps your team to understand your code better if they... (including more advanced topics like scalability, architectural, messaging and integration patterns) you can check my book Node.js Design Patterns – Second Edition:
A little challenge
Can you crack the following JWT token?
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoaW50IjoiY2FuIHlvdSBjcmFjayBtZT8ifQ.a_8rViHX5q2oSZ3yB7H0lWniEYpLZrcgG8rJvkRTcoE
If you can crack it there’s a prize for you. Append the password you discovered to http://bit.ly/ (e.g., if the password is njdsp2e
the resulting URL will be http://bit.ly/njdsp2e) to download the instructions to retrieve your prize! You won’t regret this challenge, I promise.
Have fun! Also, if you have questions or additional insights regarding this topic, please share them in the comments.
Acknowledgements
This article was peer reviewed with great care by Arthur Thevenet, Valerio De Carolis, Mario Casciaro, Padraig O’Brien, Joe Minichino and Andrea Mangano. Thank you guys for the amazing support!
This article is written by Luciano Mammino. The author’s bio:
“I’m a Node.js aficionado and co-author of Node.js Design Patterns (nodejsdesignpatterns.com), a book that discusses the challenges of designing and developing software using Node.js”