Added PEP and message protocol layers

Andreas Straub created

Can now fetch/retrieve from PEP, as well as encode/decode messages

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 208 
src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java   |   4 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java         |  70 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java    |  21 
src/main/java/eu/siacs/conversations/parser/IqParser.java               | 158 
src/main/java/eu/siacs/conversations/parser/MessageParser.java          |  46 
src/main/java/eu/siacs/conversations/xml/Element.java                   |   5 
src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java    |   5 
8 files changed, 509 insertions(+), 8 deletions(-)

Detailed changes

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

@@ -41,16 +41,26 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.parser.IqParser;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 public class AxolotlService {
 
+    public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
+    public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
+    public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys";
+    public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle";
+
     private final Account account;
     private final XmppConnectionService mXmppConnectionService;
     private final SQLiteAxolotlStore axolotlStore;
     private final SessionMap sessions;
+    private final BundleMap bundleCache;
     private int ownDeviceId;
 
     public static class SQLiteAxolotlStore implements AxolotlStore {
@@ -571,6 +581,8 @@ public class AxolotlService {
 
     }
 
+    private static class BundleMap extends AxolotlAddressMap<PreKeyBundle> {
+
     }
 
     public AxolotlService(Account account, XmppConnectionService connectionService) {
@@ -578,6 +590,7 @@ public class AxolotlService {
         this.account = account;
         this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
         this.sessions = new SessionMap(axolotlStore, account);
+        this.bundleCache = new BundleMap();
         this.ownDeviceId = axolotlStore.getLocalRegistrationId();
     }
 
@@ -618,7 +631,202 @@ public class AxolotlService {
         return ownDeviceId;
     }
 
+    public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) {
+        final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId);
+        if (sessions.get(address) != null) {
+            return;
+        }
+
+        synchronized (bundleCache) {
+            PreKeyBundle bundle = bundleCache.get(address);
+            if (bundle == null) {
+                bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null);
+                bundleCache.put(address, bundle);
+            }
+
+            if(bundle.getPreKey() == null) {
+                Log.d(Config.LOGTAG, "No preKey in cache, fetching...");
+                IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId);
+                mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() {
+                    @Override
+                    public void onIqPacketReceived(Account account, IqPacket packet) {
+                        synchronized (bundleCache) {
+                            Log.d(Config.LOGTAG, "Received preKey IQ packet, processing...");
+                            final IqParser parser = mXmppConnectionService.getIqParser();
+                            final PreKeyBundle bundle = bundleCache.get(address);
+                            final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
+                            if (preKeyBundleList.isEmpty()) {
+                                Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet);
+                                return;
+                            }
+                            Random random = new Random();
+                            final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
+                            if (bundle == null || newBundle == null) {
+                                //should never happen
+                                return;
+                            }
+
+                            final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(),
+                                    bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(),
+                                    bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
+                                    bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
+
+                            bundleCache.put(address, mergedBundle);
+                        }
+                    }
+                });
+            }
+            if(bundle.getIdentityKey() == null) {
+                Log.d(Config.LOGTAG, "No bundle in cache, fetching...");
+                IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId);
+                mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() {
+                    @Override
+                    public void onIqPacketReceived(Account account, IqPacket packet) {
+                        synchronized (bundleCache) {
+                            Log.d(Config.LOGTAG, "Received bundle IQ packet, processing...");
+                            final IqParser parser = mXmppConnectionService.getIqParser();
+                            final PreKeyBundle bundle = bundleCache.get(address);
+                            final PreKeyBundle newBundle = parser.bundle(packet);
+                            if( bundle == null || newBundle == null ) {
+                                Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet);
+                                //should never happen
+                                return;
+                            }
+
+                            final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(),
+                                    bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(),
+                                    newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(),
+                                    newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey());
+
+                            axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey());
+                            bundleCache.put(address, mergedBundle);
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    public void publishOwnDeviceIdIfNeeded() {
+        IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
+        mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+            @Override
+            public void onIqPacketReceived(Account account, IqPacket packet) {
+                Element item = mXmppConnectionService.getIqParser().getItem(packet);
+                List<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+                if(deviceIds == null) {
+                    deviceIds = new ArrayList<>();
+                }
+                if(!deviceIds.contains(getOwnDeviceId())) {
+                    Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing...");
+                    deviceIds.add(getOwnDeviceId());
+                    IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
+                    mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+                        @Override
+                        public void onIqPacketReceived(Account account, IqPacket packet) {
+                            // TODO: implement this!
+                        }
+                    });
+                }
+            }
+        });
+    }
+
+    public void publishBundleIfNeeded() {
+        IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId);
+        mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+            @Override
+            public void onIqPacketReceived(Account account, IqPacket packet) {
+                PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
+                if(bundle == null) {
+                    Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing...");
+                    int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
+                    try {
+                        SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey(
+                                axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1);
+                        axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
+                        IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle(
+                                signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
+                                ownDeviceId);
+                        mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+                            @Override
+                            public void onIqPacketReceived(Account account, IqPacket packet) {
+                                // TODO: implement this!
+                                Log.d(Config.LOGTAG, "Published bundle, got: " + packet);
+                            }
+                        });
+                    } catch (InvalidKeyException e) {
+                        Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
+                    }
+                }
+            }
+        });
+    }
+
+    public void publishPreKeysIfNeeded() {
+        IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId);
+        mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
+            @Override
+            public void onIqPacketReceived(Account account, IqPacket packet) {
+                Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
+                if(keys == null || keys.isEmpty()) {
+                    Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing...");
+                    List<PreKeyRecord> preKeyRecords = KeyHelper.generatePreKeys(
+                            axolotlStore.getCurrentPreKeyId(), 100);
+                    for(PreKeyRecord record : preKeyRecords) {
+                        axolotlStore.storePreKey(record.getId(), record);
+                    }
+                    IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys(
+                            preKeyRecords, ownDeviceId);
+
+                    mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
+                        @Override
+                        public void onIqPacketReceived(Account account, IqPacket packet) {
+                            Log.d(Config.LOGTAG, "Published prekeys, got: " + packet);
+                            // TODO: implement this!
+                        }
+                    });
+                }
+            }
+        });
+    }
+
+
+    public boolean isContactAxolotlCapable(Contact contact) {
+        AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
+        return sessions.hasAny(address) || bundleCache.hasAny(address);
+    }
+
+    public void initiateSynchronousSession(Contact contact) {
+
+    }
+
     private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException {
+        Log.d(Config.LOGTAG, "Creating axolotl sessions if needed...");
+        AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
+        for(Integer deviceId: bundleCache.getAll(address).keySet()) {
+            Log.d(Config.LOGTAG, "Processing device ID: " + deviceId);
+            AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId);
+            if(sessions.get(remoteAddress) == null) {
+                Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId);
+                SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress);
+                try {
+                    builder.process(bundleCache.get(remoteAddress));
+                    XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress);
+                    sessions.put(remoteAddress, session);
+                } catch (InvalidKeyException e) {
+                    Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage());
+                } catch (UntrustedIdentityException e) {
+                    Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage());
+                }
+            } else {
+                Log.d(Config.LOGTAG, "Already have session for " + deviceId);
+            }
+        }
+        if(!this.hasAny(contact)) {
+            Log.e(Config.LOGTAG, "No Axolotl sessions available!");
+            throw new NoSessionsCreatedException(); // FIXME: proper error handling
+        }
     }
 
     public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException {

src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java 🔗

@@ -12,6 +12,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.TimeZone;
 
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.PhoneHelper;
 
@@ -28,7 +29,8 @@ public abstract class AbstractGenerator {
 			"urn:xmpp:avatar:metadata+notify",
 			"urn:xmpp:ping",
 			"jabber:iq:version",
-			"http://jabber.org/protocol/chatstates"};
+			"http://jabber.org/protocol/chatstates",
+			AxolotlService.PEP_DEVICE_LIST+"+notify"};
 	private final String[] MESSAGE_CONFIRMATION_FEATURES = {
 			"urn:xmpp:chat-markers:0",
 			"urn:xmpp:receipts"

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -1,9 +1,17 @@
 package eu.siacs.conversations.generator;
 
 
+import android.util.Base64;
+
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.ecc.ECPublicKey;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+
 import java.util.ArrayList;
 import java.util.List;
 
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.DownloadableFile;
@@ -115,6 +123,68 @@ public class IqGenerator extends AbstractGenerator {
 		return packet;
 	}
 
+	public IqPacket retrieveDeviceIds(final Jid to) {
+		final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
+		if(to != null) {
+			packet.setTo(to);
+		}
+		return packet;
+	}
+
+	public IqPacket retrieveBundleForDevice(final Jid to, final int deviceid) {
+		final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLE+":"+deviceid, null);
+		if(to != null) {
+			packet.setTo(to);
+		}
+		return packet;
+	}
+
+	public IqPacket retrievePreKeysForDevice(final Jid to, final int deviceId) {
+		final IqPacket packet = retrieve(AxolotlService.PEP_PREKEYS+":"+deviceId, null);
+		if(to != null) {
+			packet.setTo(to);
+		}
+		return packet;
+	}
+
+    public IqPacket publishDeviceIds(final List<Integer> ids) {
+        final Element item = new Element("item");
+        final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
+        for(Integer id:ids) {
+            final Element device = new Element("device");
+            device.setAttribute("id", id);
+            list.addChild(device);
+        }
+        return publish(AxolotlService.PEP_DEVICE_LIST, item);
+    }
+
+	public IqPacket publishBundle(final SignedPreKeyRecord signedPreKeyRecord, IdentityKey identityKey, final int deviceId) {
+		final Element item = new Element("item");
+		final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
+		final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
+		signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
+		ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
+		signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT));
+		final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
+		signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT));
+		final Element identityKeyElement = bundle.addChild("identityKey");
+		identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
+
+		return publish(AxolotlService.PEP_BUNDLE+":"+deviceId, item);
+	}
+
+    public IqPacket publishPreKeys(final List<PreKeyRecord> prekeyList, final int deviceId) {
+        final Element item = new Element("item");
+        final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX);
+        for(PreKeyRecord preKeyRecord:prekeyList) {
+            final Element prekey = prekeys.addChild("preKeyPublic");
+			prekey.setAttribute("preKeyId", preKeyRecord.getId());
+            prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT));
+        }
+
+		return publish(AxolotlService.PEP_PREKEYS+":"+deviceId, item);
+    }
+
 	public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
 		final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
 		final Element query = packet.query("urn:xmpp:mam:0");

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.generator;
 
+import android.util.Log;
+
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Locale;
@@ -7,6 +9,11 @@ import java.util.TimeZone;
 
 import net.java.otr4j.OtrException;
 import net.java.otr4j.session.Session;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.NoSessionsCreatedException;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
@@ -59,6 +66,20 @@ public class MessageGenerator extends AbstractGenerator {
 		delay.setAttribute("stamp", mDateFormat.format(date));
 	}
 
+    public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{
+        return generateAxolotlChat(message, false);
+    }
+
+	public MessagePacket generateAxolotlChat(Message message, boolean addDelay) throws NoSessionsCreatedException{
+        MessagePacket packet = preparePacket(message, addDelay);
+        AxolotlService service = message.getConversation().getAccount().getAxolotlService();
+        Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing...");
+        XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(),
+                message.getBody());
+        packet.setAxolotlMessage(axolotlMessage.toXml());
+        return packet;
+    }
+
 	public MessagePacket generateOtrChat(Message message) {
 		return generateOtrChat(message, false);
 	}

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

@@ -1,9 +1,19 @@
 package eu.siacs.conversations.parser;
 
+import android.util.Base64;
 import android.util.Log;
 
+import org.whispersystems.libaxolotl.IdentityKey;
+import org.whispersystems.libaxolotl.InvalidKeyException;
+import org.whispersystems.libaxolotl.ecc.Curve;
+import org.whispersystems.libaxolotl.ecc.ECPublicKey;
+import org.whispersystems.libaxolotl.state.PreKeyBundle;
+
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
@@ -60,7 +70,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
 
 	public String avatarData(final IqPacket packet) {
 		final Element pubsub = packet.findChild("pubsub",
-				"http://jabber.org/protocol/pubsub");
+                "http://jabber.org/protocol/pubsub");
 		if (pubsub == null) {
 			return null;
 		}
@@ -71,6 +81,152 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
 		return super.avatarData(items);
 	}
 
+	public Element getItem(final IqPacket packet) {
+		final Element pubsub = packet.findChild("pubsub",
+				"http://jabber.org/protocol/pubsub");
+		if (pubsub == null) {
+			return null;
+		}
+		final Element items = pubsub.findChild("items");
+		if (items == null) {
+			return null;
+		}
+		return items.findChild("item");
+	}
+
+	public List<Integer> deviceIds(final Element item) {
+		List<Integer> deviceIds = new ArrayList<>();
+		if (item == null) {
+			return null;
+		}
+		final Element list = item.findChild("list");
+		if(list == null) {
+			return null;
+		}
+		for(Element device : list.getChildren()) {
+			if(!device.getName().equals("device")) {
+				continue;
+			}
+			try {
+				Integer id = Integer.valueOf(device.getAttribute("id"));
+				deviceIds.add(id);
+			} catch (NumberFormatException e) {
+				Log.e(Config.LOGTAG, "Encountered nvalid <device> node in PEP:" + device.toString()
+					+ ", skipping...");
+				continue;
+			}
+		}
+		return deviceIds;
+	}
+
+	public Integer signedPreKeyId(final Element bundle) {
+		final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
+		if(signedPreKeyPublic == null) {
+			return null;
+		}
+		return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
+	}
+
+	public ECPublicKey signedPreKeyPublic(final Element bundle) {
+		ECPublicKey publicKey = null;
+		final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
+		if(signedPreKeyPublic == null) {
+			return null;
+		}
+		try {
+			publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
+		} catch (InvalidKeyException e) {
+			Log.e(Config.LOGTAG, "Invalid signedPreKeyPublic in PEP: " + e.getMessage());
+		}
+		return publicKey;
+	}
+
+	public byte[] signedPreKeySignature(final Element bundle) {
+		final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
+		if(signedPreKeySignature == null) {
+			return null;
+		}
+		return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT);
+	}
+
+	public IdentityKey identityKey(final Element bundle) {
+		IdentityKey identityKey = null;
+		final Element identityKeyElement = bundle.findChild("identityKey");
+		if(identityKeyElement == null) {
+			return null;
+		}
+		try {
+			identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
+		} catch (InvalidKeyException e) {
+			Log.e(Config.LOGTAG,"Invalid identityKey in PEP: "+e.getMessage());
+		}
+		return identityKey;
+	}
+
+	public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
+		Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
+        Element prekeysItem = getItem(packet);
+        if (prekeysItem == null) {
+            Log.d(Config.LOGTAG, "Couldn't find <item> in preKeyPublic IQ packet: " + packet);
+            return null;
+        }
+        final Element prekeysElement = prekeysItem.findChild("prekeys");
+        if(prekeysElement == null) {
+            Log.d(Config.LOGTAG, "Couldn't find <prekeys> in preKeyPublic IQ packet: " + packet);
+            return null;
+        }
+		for(Element preKeyPublicElement : prekeysElement.getChildren()) {
+			if(!preKeyPublicElement.getName().equals("preKeyPublic")){
+                Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
+				continue;
+			}
+			Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
+			try {
+				ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
+				preKeyRecords.put(preKeyId, preKeyPublic);
+			} catch (InvalidKeyException e) {
+				Log.e(Config.LOGTAG, "Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
+				continue;
+			}
+		}
+		return preKeyRecords;
+	}
+
+    public PreKeyBundle bundle(final IqPacket bundle) {
+        Element bundleItem = getItem(bundle);
+        if(bundleItem == null) {
+            return null;
+        }
+        final Element bundleElement = bundleItem.findChild("bundle");
+        if(bundleElement == null) {
+            return null;
+        }
+        ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
+        Integer signedPreKeyId = signedPreKeyId(bundleElement);
+        byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
+        IdentityKey identityKey = identityKey(bundleElement);
+        if(signedPreKeyPublic == null || identityKey == null) {
+            return null;
+        }
+
+        return new PreKeyBundle(0, 0, 0, null,
+                signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
+    }
+
+	public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
+		List<PreKeyBundle> bundles = new ArrayList<>();
+		Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
+        if ( preKeyPublics != null) {
+            for (Integer preKeyId : preKeyPublics.keySet()) {
+                ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
+                bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
+                        0, null, null, null));
+            }
+        }
+
+		return bundles;
+	}
+
 	@Override
 	public void onIqPacketReceived(final Account account, final IqPacket packet) {
 		if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {

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

@@ -6,7 +6,11 @@ import android.util.Pair;
 import net.java.otr4j.session.Session;
 import net.java.otr4j.session.SessionStatus;
 
+import java.util.List;
+
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
@@ -94,6 +98,18 @@ public class MessageParser extends AbstractParser implements
 		}
 	}
 
+    private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) {
+        Message finishedMessage = null;
+        AxolotlService service = conversation.getAccount().getAxolotlService();
+        XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage);
+        XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage);
+        if(plaintextMessage != null) {
+            finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED);
+        }
+
+        return finishedMessage;
+    }
+
 	private class Invite {
 		Jid jid;
 		String password;
@@ -170,6 +186,18 @@ public class MessageParser extends AbstractParser implements
 				mXmppConnectionService.updateConversationUi();
 				mXmppConnectionService.updateAccountUi();
 			}
+		} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
+            Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing...");
+            Element item = items.findChild("item");
+			List<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
+            AxolotlService axolotlService = account.getAxolotlService();
+            if(account.getJid().toBareJid().equals(from)) {
+            } else {
+                Contact contact = account.getRoster().getContact(from);
+                for (Integer deviceId : deviceIds) {
+                    axolotlService.fetchBundleIfNeeded(contact, deviceId);
+                }
+            }
 		}
 	}
 
@@ -232,8 +260,9 @@ public class MessageParser extends AbstractParser implements
 			timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
 		}
 		final String body = packet.getBody();
-		final String encrypted = packet.findChildContent("x", "jabber:x:encrypted");
 		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);
 		int status;
 		final Jid counterpart;
 		final Jid to = packet.getTo();
@@ -261,11 +290,11 @@ public class MessageParser extends AbstractParser implements
 			return;
 		}
 
-		if (extractChatState(mXmppConnectionService.find(account,from), packet)) {
+		if (extractChatState(mXmppConnectionService.find(account, from), packet)) {
 			mXmppConnectionService.updateConversationUi();
 		}
 
-		if ((body != null || encrypted != null) && !isMucStatusMessage) {
+		if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
 			Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat);
 			if (isTypeGroupChat) {
 				if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
@@ -294,9 +323,14 @@ public class MessageParser extends AbstractParser implements
 				} else {
 					message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
 				}
-			} else if (encrypted != null) {
-				message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status);
-			} else {
+			} else if (pgpEncrypted != null) {
+                message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
+            } else if (axolotlEncrypted != null) {
+                message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation);
+                if (message == null) {
+                    return;
+                }
+            } else {
 				message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
 			}
 			message.setCounterpart(counterpart);

src/main/java/eu/siacs/conversations/xml/Element.java 🔗

@@ -21,6 +21,11 @@ public class Element {
 		this.name = name;
 	}
 
+	public Element(String name, String xmlns) {
+		this.name = name;
+		this.setAttribute("xmlns", xmlns);
+	}
+
 	public Element addChild(Element child) {
 		this.content = null;
 		children.add(child);

src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java 🔗

@@ -29,6 +29,11 @@ public class MessagePacket extends AbstractStanza {
 		this.children.add(0, body);
 	}
 
+	public void setAxolotlMessage(Element axolotlMessage) {
+		this.children.remove(findChild("body"));
+		this.children.add(0, axolotlMessage);
+	}
+
 	public void setType(int type) {
 		switch (type) {
 		case TYPE_CHAT: