The german interior minister conference recently decided that the best way to fight terrorism is passing new laws that allow the government to demand access to communication from messengers like WhatsApp and co. Very important: Messengers like WhatsApp. Will even free software developers see requests to change their messengers to allow government access to communications in the future? If it comes so far, how are we then still possible to protect our communications?
The answer could be: Build your own messenger. I want to demonstrate, how simple it is to create a very basic messenger that allows you to send and receive end-to-end encrypted text messages via XMPP using Smack. We will use Smacks latest new feature – OMEMO support to create a very simple XMPP based command line chat application that uses state of the art encryption. I assume, that you all know, what XMPP is. If not, please read it up on Wikipedia. Smack is a java library that makes it easy to use XMPP in an application. OMEMO is basically the Signal protocol for XMPP.
So lets hop straight into it.
In my example, I import smack as a gradle dependency. That looks like this:
gradle.build
apply plugin: 'java' apply plugin: 'idea' repositories { mavenCentral() maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } ext { smackVersion="4.2.1-SNAPSHOT" } dependencies { compile "org.igniterealtime.smack:smack-java7:$smackVersion" compile "org.igniterealtime.smack:smack-omemo-signal:$smackVersion" compile "org.igniterealtime.smack:smack-resolver-dnsjava:$smackVersion" compile "org.igniterealtime.smack:smack-tcp:$smackVersion" } //Pack dependencies into the jar jar { from(configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }) { exclude "META-INF/*.SF" exclude "META-INF/LICENSE" } manifest { attributes( 'Main-Class': 'Messenger' ) } }
Now we can start the main function of our client. We need to create a connection to a server and log in to go online. Lets assume, that the user passes username and password as arguments to our main function. For sake of simplicity, we’ll not catch any errors like wrong number of parameters etc. Also we want to get notified of incoming chat messages and we want to send messages to others.
Messenger.java
public class Messenger { private AbstractXMPPConnection connection; private static Scanner scanner; public static void main(String[] args) throws Exception { String username = args[0]; String password = args[1]; Messenger messenger = new Messenger(username, password); scanner = new Scanner(System.in); while(true) { String input = scanner.nextLine(); if (input.startsWith("/quit")) { break; } if (input.isEmpty()) { continue; } messenger.handleInput(input); } } public Messenger(String username, String password) throws Exception { connection = new XMPPTCPConnection(username, password); connection = connection.connect(); connection.login(); ChatManager.getInstanceFor(connection).addIncomingListener( (from, message, chat) -> System.out.println(from.asBareJid() + ": " + message) ); System.out.println("Logged in"); } private void handleInput(String input) throws Exception { String[] split = input.split(" "); String command = split[0]; switch (command) { case "/say": if (split.length > 3) { String recipient = split[1]; EntityBareJid recipientJid = JidCreate.entityBareFrom(recipient); StringBuilder message = new StringBuilder(); for (int i=2; i<split.length; i++) message.append(split[i]); ChatManager.getInstanceFor(connection).chatWith(recipientJid).send(message); } break; } } }
If we now compile this code and execute it using credentials of an existing account, we can already log in and start chatting with others using the /say command (eg. /say bob@marley.jm Hi Bob!). But our communications are unencrypted right now (aside from tls transport encryption). Lets change that next. We want to use OMEMO encryption to secure our messages, so we utilize Smacks new OmemoManager which handles OMEMO encryption. For that purpose, we need a new private variable which will hold our OmemoManager. Also we make some changes to the constructor.
Messenger.java
private OmemoManager omemoManager; public Messenger(String username, String password) throws Exception { connection = new XMPPTCPConnection(username, password); connection = connection.connect(); connection.login(); //additions begin here SignalOmemoService.acknowledgeLicense(); SignalOmemoService.setup(); //path where keys get stored OmemoConfiguration.setFileBasedOmemoStoreDefaultPath(new File("path")); omemoManager = OmemoManager.getInstanceFor(connection); //Listener for incoming OMEMO messages omemoManager.addOmemoMessageListener(new OmemoMessageListener() { @Override public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { System.out.println("(O) " + encryptedMessage.getFrom() + ": " + decryptedBody); } @Override public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { //Not needed } }); ChatManager.getInstanceFor(connection).addIncomingListener( (from, message, chat) -> System.out.println(from.asBareJid() + ": " + message) ); omemoManager.initialize(); //additions end here. System.out.println("Logged in"); }
Also we must add two new commands that are needed to control OMEMO. /omemo is similar to /say, but will encrypt the message via OMEMO. /trust is used to trust an identity. Before you can send a message, you have to decide, whether you want to trust or distrust an identity. When you call the trust command, the client will present you with a fingerprint which you have to compare with your chat patner. Only if the fingerprint matches, you should trust it. We add the following two cases to the handleInput’s switch case environment:
Messenger.java
case "/omemo": if (split.length > 2) { String recipient = split[1]; EntityBareJid recipientJid = JidCreate.entityBareFrom(recipient); StringBuilder message = new StringBuilder(); for (int i=2; i<split.length; i++) message.append(split[i]); //encrypt Message encrypted = null; try { encrypted = OmemoManager.getInstanceFor(connection).encrypt(recipientJid, message.toString()); } // In case of undecided devices catch (UndecidedOmemoIdentityException e) { System.out.println("Undecided Identities: "); for (OmemoDevice device : e.getUntrustedDevices()) { System.out.println(device); } } //In case we cannot establish session with some devices catch (CannotEstablishOmemoSessionException e) { encrypted = omemoManager.encryptForExistingSessions(e, message.toString()); } //send if (encrypted != null) { ChatManager.getInstanceFor(connection).chatWith(recipientJid).send(encrypted); } } break; case "/trust": if (split.length == 2) { BareJid contact = JidCreate.bareFrom(split[1]); HashMap<OmemoDevice, OmemoFingerprint> fingerprints = omemoManager.getActiveFingerprints(contact); //Let user decide for (OmemoDevice d : fingerprints.keySet()) { System.out.println("Trust (1), or distrust (2)?"); System.out.println(OmemoKeyUtil.prettyFingerprint(fingerprints.get(d))); int decision = Integer.parseInt(scanner.nextLine()); if (decision == 1) { omemoManager.trustOmemoIdentity(d, fingerprints.get(d)); } else { omemoManager.distrustOmemoIdentity(d, fingerprints.get(d)); } } } break;
Now we can trust contact OMEMO identities using /trust bob@marley.jm and send them encrypted messages using /omemo bob@marley.jm Hi Bob!. When we receive OMEMO messages, they are indicated by a “(O)” in front of the sender.
If we want to go really fancy, we can let our messenger display, whether received messages are encrypted using a trusted key. Unfortunately, there is no convenience method for this available yet, so we have to do a small dirty workaround. We modify the onOmemoMessageReceived method of the OmemoMessageListener like this:
Messenger.java
@Override public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { //Get identityKey of sender IdentityKey senderKey = (IdentityKey) omemoInformation.getSenderIdentityKey().getIdentityKey(); OmemoService<?,IdentityKey,?,?,?,?,?,?,?> service = (OmemoService<?,IdentityKey,?,?,?,?,?,?,?>) OmemoService.getInstance(); //get the fingerprint of the key OmemoFingerprint fingerprint = service.getOmemoStoreBackend().keyUtil().getFingerprint(senderKey); //Lookup trust status boolean trusted = omemoManager.isTrustedOmemoIdentity(omemoInformation.getSenderDevice(), fingerprint); System.out.println("(O) " + (trusted ? "T" : "D") + " " + encryptedMessage.getFrom() + ": " + decryptedBody); }
Now when we receive a message from a trusted identity, there will be a “T” before the message, otherwise there is a “D”.
I hope I could give a brief introduction on how to use Smacks OMEMO support. You now have a basic chat client, that is capable of exchanging multi-end-to-multi-end encrypted messages with other XMPP clients that support OMEMO. All took less than 200 lines of code! Now its up to you to add additional features like support for message carbons, offline messages and co. Spoiler: Its not hard at all 🙂
You can find the source code of this tutorial in the FSFE’s git repository.
When the government is unable or simply not willing to preserve your privacy, you’ll have to do it yourself.
Happy Hacking 🙂
2 responses to “Tutorial: Home-made OMEMO client”
Do I need to import some packages of Omemo Signal like
Import:
org.jivesoftware.smackx.omemo.signal
Hi.
encryptForExistingSessions is not existed in smack 4.4.8!
Please suggest an alternative.