Change to new wire protocol version

Andreas Straub created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java           |  27 
src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java |   5 
src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java       | 143 
src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java       |   8 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                    |   2 
5 files changed, 112 insertions(+), 73 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -583,23 +583,15 @@ public class AxolotlService {
 		if(findSessionsforContact(message.getContact()).isEmpty()) {
 			return null;
 		}
-		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl foreign headers...");
+		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl foreign keyElements...");
 		for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) {
 			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.getRemoteAddress().toString());
-			//if(!session.isTrusted()) {
-			// TODO: handle this properly
-			//              continue;
-			//        }
-			axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
+			axolotlMessage.addKeyElement(session.processSending(axolotlMessage.getInnerKey()));
 		}
-		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl own headers...");
+		Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl own keyElements...");
 		for (XmppAxolotlSession session : findOwnSessions()) {
 			Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.getRemoteAddress().toString());
-			//        if(!session.isTrusted()) {
-			// TODO: handle this properly
-			//          continue;
-			//    }
-			axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey()));
+			axolotlMessage.addKeyElement(session.processSending(axolotlMessage.getInnerKey()));
 		}
 
 		return axolotlMessage;
@@ -651,7 +643,6 @@ public class AxolotlService {
 		XmppAxolotlSession session = sessions.get(senderAddress);
 		if (session == null) {
 			Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Account: "+account.getJid()+" No axolotl session found while parsing received message " + message);
-			// TODO: handle this properly
 			IdentityKey identityKey = axolotlStore.loadSession(senderAddress).getSessionState().getRemoteIdentityKey();
 			if ( identityKey != null ) {
 				session = new XmppAxolotlSession(account, axolotlStore, senderAddress, identityKey.getFingerprint().replaceAll("\\s", ""));
@@ -661,12 +652,12 @@ public class AxolotlService {
 			newSession = true;
 		}
 
-		for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) {
-			if (header.getRecipientDeviceId() == getOwnDeviceId()) {
-				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl header matching own device ID, processing...");
-				byte[] payloadKey = session.processReceiving(header);
+		for (XmppAxolotlMessage.XmppAxolotlKeyElement keyElement : message.getKeyElements()) {
+			if (keyElement.getRecipientDeviceId() == getOwnDeviceId()) {
+				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl keyElement matching own device ID, processing...");
+				byte[] payloadKey = session.processReceiving(keyElement);
 				if (payloadKey != null) {
-					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message...");
+					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl keyElement. Decrypting message...");
 					try{
 						plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint());
 					} catch (CryptoFailedException e) {

src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java 🔗

@@ -2,6 +2,7 @@ package eu.siacs.conversations.crypto.axolotl;
 
 import android.support.annotation.Nullable;
 import android.util.Base64;
+import android.util.Log;
 
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
@@ -20,32 +21,46 @@ import javax.crypto.SecretKey;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.jid.Jid;
 
 public class XmppAxolotlMessage {
+	public static final String TAGNAME = "encrypted";
+	public static final String HEADER = "header";
+	public static final String SOURCEID = "sid";
+	public static final String IVTAG = "iv";
+	public static final String PAYLOAD = "payload";
+
+	private static final String KEYTYPE = "AES";
+	private static final String CIPHERMODE = "AES/GCM/NoPadding";
+	private static final String PROVIDER = "BC";
+
 	private byte[] innerKey;
-	private byte[] ciphertext;
-	private byte[] iv;
-	private final Set<XmppAxolotlMessageHeader> headers;
+	private byte[] ciphertext = null;
+	private byte[] iv = null;
+	private final Set<XmppAxolotlKeyElement> keyElements;
 	private final Jid from;
 	private final int sourceDeviceId;
 
-	public static class XmppAxolotlMessageHeader {
+	public static class XmppAxolotlKeyElement {
+		public static final String TAGNAME = "key";
+		public static final String REMOTEID = "rid";
+
 		private final int recipientDeviceId;
 		private final byte[] content;
 
-		public XmppAxolotlMessageHeader(int deviceId, byte[] content) {
+		public XmppAxolotlKeyElement(int deviceId, byte[] content) {
 			this.recipientDeviceId = deviceId;
 			this.content = content;
 		}
 
-		public XmppAxolotlMessageHeader(Element header) {
-			if("header".equals(header.getName())) {
-				this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid"));
-				this.content = Base64.decode(header.getContent(),Base64.DEFAULT);
+		public XmppAxolotlKeyElement(Element keyElement) {
+			if(TAGNAME.equals(keyElement.getName())) {
+				this.recipientDeviceId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
+				this.content = Base64.decode(keyElement.getContent(),Base64.DEFAULT);
 			} else {
-				throw new IllegalArgumentException("Argument not a <header> Element!");
+				throw new IllegalArgumentException("Argument not a <"+TAGNAME+"> Element!");
 			}
 		}
 
@@ -58,11 +73,10 @@ public class XmppAxolotlMessage {
 		}
 
 		public Element toXml() {
-			Element headerElement = new Element("header");
-			// TODO: generate XML
-			headerElement.setAttribute("rid", getRecipientDeviceId());
-			headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT));
-			return headerElement;
+			Element keyElement = new Element(TAGNAME);
+			keyElement.setAttribute(REMOTEID, getRecipientDeviceId());
+			keyElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT));
+			return keyElement;
 		}
 	}
 
@@ -90,42 +104,69 @@ public class XmppAxolotlMessage {
 		}
 	}
 
-	public XmppAxolotlMessage(Jid from, Element axolotlMessage) {
+	public XmppAxolotlMessage(Jid from, Element axolotlMessage) throws IllegalArgumentException {
 		this.from = from;
-		this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id"));
-		this.headers = new HashSet<>();
-		for(Element child:axolotlMessage.getChildren()) {
-			switch(child.getName()) {
-				case "header":
-					headers.add(new XmppAxolotlMessageHeader(child));
+		Element header = axolotlMessage.findChild(HEADER);
+		this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
+		this.keyElements = new HashSet<>();
+		for(Element keyElement:header.getChildren()) {
+			switch(keyElement.getName()) {
+				case XmppAxolotlKeyElement.TAGNAME:
+					keyElements.add(new XmppAxolotlKeyElement(keyElement));
 					break;
-				case "message":
-					iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT);
-					ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT);
+				case IVTAG:
+					if ( this.iv != null) {
+						throw new IllegalArgumentException("Duplicate iv entry");
+					}
+					iv = Base64.decode(keyElement.getContent(),Base64.DEFAULT);
 					break;
 				default:
+					Log.w(Config.LOGTAG, "Unexpected element in header: "+ keyElement.toString());
 					break;
 			}
 		}
+		Element payloadElement = axolotlMessage.findChild(PAYLOAD);
+		if ( payloadElement != null ) {
+			ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT);
+		}
 	}
 
-	public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) throws CryptoFailedException{
+	public XmppAxolotlMessage(Jid from, int sourceDeviceId) {
 		this.from = from;
 		this.sourceDeviceId = sourceDeviceId;
-		this.headers = new HashSet<>();
+		this.keyElements = new HashSet<>();
+		this.iv = generateIv();
+		this.innerKey = generateKey();
+	}
+
+	public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) throws CryptoFailedException{
+		this(from, sourceDeviceId);
 		this.encrypt(plaintext);
 	}
 
-	private void encrypt(String plaintext) throws CryptoFailedException {
+	private static byte[] generateKey() {
 		try {
-			KeyGenerator generator = KeyGenerator.getInstance("AES");
+			KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE);
 			generator.init(128);
-			SecretKey secretKey = generator.generateKey();
-			SecureRandom random = new SecureRandom();
-			this.iv = new byte[16];
-			random.nextBytes(iv);
+			return generator.generateKey().getEncoded();
+		} catch (NoSuchAlgorithmException e) {
+			Log.e(Config.LOGTAG, e.getMessage());
+			return null;
+		}
+	}
+
+	private static byte[] generateIv() {
+		SecureRandom random = new SecureRandom();
+		byte[] iv = new byte[16];
+		random.nextBytes(iv);
+		return iv;
+	}
+
+	private void encrypt(String plaintext) throws CryptoFailedException {
+		try {
+			SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
 			IvParameterSpec ivSpec = new IvParameterSpec(iv);
-			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
+			Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
 			cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
 			this.innerKey = secretKey.getEncoded();
 			this.ciphertext = cipher.doFinal(plaintext.getBytes());
@@ -148,13 +189,13 @@ public class XmppAxolotlMessage {
 		return ciphertext;
 	}
 
-	public Set<XmppAxolotlMessageHeader> getHeaders() {
-		return headers;
+	public Set<XmppAxolotlKeyElement> getKeyElements() {
+		return keyElements;
 	}
 
-	public void addHeader(@Nullable XmppAxolotlMessageHeader header) {
-		if (header != null) {
-			headers.add(header);
+	public void addKeyElement(@Nullable XmppAxolotlKeyElement keyElement) {
+		if (keyElement != null) {
+			keyElements.add(keyElement);
 		}
 	}
 
@@ -167,16 +208,18 @@ public class XmppAxolotlMessage {
 	}
 
 	public Element toXml() {
-		// TODO: generate outer XML, add in header XML
-		Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX);
-		message.setAttribute("id", sourceDeviceId);
-		for(XmppAxolotlMessageHeader header: headers) {
-			message.addChild(header.toXml());
+		Element encryptionElement= new Element(TAGNAME, AxolotlService.PEP_PREFIX);
+		Element headerElement = encryptionElement.addChild(HEADER);
+		headerElement.setAttribute(SOURCEID, sourceDeviceId);
+		for(XmppAxolotlKeyElement header: keyElements) {
+			headerElement.addChild(header.toXml());
+		}
+		headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
+		if ( ciphertext != null ) {
+			Element payload = encryptionElement.addChild(PAYLOAD);
+			payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
 		}
-		Element payload = message.addChild("message");
-		payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT));
-		payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT));
-		return message;
+		return encryptionElement;
 	}
 
 
@@ -184,8 +227,8 @@ public class XmppAxolotlMessage {
 		XmppAxolotlPlaintextMessage plaintextMessage = null;
 		try {
 
-			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
-			SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
+			Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
+			SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
 			IvParameterSpec ivSpec = new IvParameterSpec(iv);
 
 			cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java 🔗

@@ -69,7 +69,7 @@ public class XmppAxolotlSession {
 	}
 
 	@Nullable
-	public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) {
+	public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlKeyElement incomingHeader) {
 		byte[] plaintext = null;
 		SQLiteAxolotlStore.Trust trust = getTrust();
 		switch (trust) {
@@ -117,12 +117,12 @@ public class XmppAxolotlSession {
 	}
 
 	@Nullable
-	public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(@NonNull byte[] outgoingMessage) {
+	public XmppAxolotlMessage.XmppAxolotlKeyElement processSending(@NonNull byte[] outgoingMessage) {
 		SQLiteAxolotlStore.Trust trust = getTrust();
 		if (trust == SQLiteAxolotlStore.Trust.TRUSTED) {
 			CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
-			XmppAxolotlMessage.XmppAxolotlMessageHeader header =
-					new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(),
+			XmppAxolotlMessage.XmppAxolotlKeyElement header =
+					new XmppAxolotlMessage.XmppAxolotlKeyElement(remoteAddress.getDeviceId(),
 							ciphertextMessage.serialize());
 			return header;
 		} else {

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -272,7 +272,7 @@ public class MessageParser extends AbstractParser implements
 		final String body = packet.getBody();
 		final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
 		final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
-		final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX);
+		final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.TAGNAME, AxolotlService.PEP_PREFIX);
 		int status;
 		final Jid counterpart;
 		final Jid to = packet.getTo();