IqParser.java

  1package eu.siacs.conversations.parser;
  2
  3import android.support.annotation.NonNull;
  4import android.text.TextUtils;
  5import android.util.Base64;
  6import android.util.Log;
  7import android.util.Pair;
  8
  9import org.whispersystems.libsignal.IdentityKey;
 10import org.whispersystems.libsignal.ecc.Curve;
 11import org.whispersystems.libsignal.ecc.ECPublicKey;
 12import org.whispersystems.libsignal.state.PreKeyBundle;
 13
 14import java.io.ByteArrayInputStream;
 15import java.security.cert.CertificateException;
 16import java.security.cert.CertificateFactory;
 17import java.security.cert.X509Certificate;
 18import java.util.ArrayList;
 19import java.util.Collection;
 20import java.util.HashMap;
 21import java.util.HashSet;
 22import java.util.List;
 23import java.util.Map;
 24import java.util.Set;
 25
 26import eu.siacs.conversations.Config;
 27import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 28import eu.siacs.conversations.entities.Account;
 29import eu.siacs.conversations.entities.Contact;
 30import eu.siacs.conversations.entities.Conversation;
 31import eu.siacs.conversations.entities.Room;
 32import eu.siacs.conversations.services.ChannelDiscoveryService;
 33import eu.siacs.conversations.services.XmppConnectionService;
 34import eu.siacs.conversations.xml.Namespace;
 35import eu.siacs.conversations.xml.Element;
 36import eu.siacs.conversations.xmpp.InvalidJid;
 37import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 38import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 39import eu.siacs.conversations.xmpp.forms.Data;
 40import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 41import rocks.xmpp.addr.Jid;
 42
 43public class IqParser extends AbstractParser implements OnIqPacketReceived {
 44
 45    public IqParser(final XmppConnectionService service) {
 46        super(service);
 47    }
 48
 49    private void rosterItems(final Account account, final Element query) {
 50        final String version = query.getAttribute("ver");
 51        if (version != null) {
 52            account.getRoster().setVersion(version);
 53        }
 54        for (final Element item : query.getChildren()) {
 55            if (item.getName().equals("item")) {
 56                final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
 57                if (jid == null) {
 58                    continue;
 59                }
 60                final String name = item.getAttribute("name");
 61                final String subscription = item.getAttribute("subscription");
 62                final Contact contact = account.getRoster().getContact(jid);
 63                boolean bothPre = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
 64                if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
 65                    contact.setServerName(name);
 66                    contact.parseGroupsFromElement(item);
 67                }
 68                if ("remove".equals(subscription)) {
 69                    contact.resetOption(Contact.Options.IN_ROSTER);
 70                    contact.resetOption(Contact.Options.DIRTY_DELETE);
 71                    contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
 72                } else {
 73                    contact.setOption(Contact.Options.IN_ROSTER);
 74                    contact.resetOption(Contact.Options.DIRTY_PUSH);
 75                    contact.parseSubscriptionFromElement(item);
 76                }
 77                boolean both = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
 78                if ((both != bothPre) && both) {
 79                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": gained mutual presence subscription with " + contact.getJid());
 80                    AxolotlService axolotlService = account.getAxolotlService();
 81                    if (axolotlService != null) {
 82                        axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
 83                    }
 84                }
 85                mXmppConnectionService.getAvatarService().clear(contact);
 86            }
 87        }
 88        mXmppConnectionService.updateConversationUi();
 89        mXmppConnectionService.updateRosterUi();
 90        mXmppConnectionService.getShortcutService().refresh();
 91        mXmppConnectionService.syncRoster(account);
 92    }
 93
 94    public String avatarData(final IqPacket packet) {
 95        final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
 96        if (pubsub == null) {
 97            return null;
 98        }
 99        final Element items = pubsub.findChild("items");
100        if (items == null) {
101            return null;
102        }
103        return super.avatarData(items);
104    }
105
106    public Element getItem(final IqPacket packet) {
107        final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
108        if (pubsub == null) {
109            return null;
110        }
111        final Element items = pubsub.findChild("items");
112        if (items == null) {
113            return null;
114        }
115        return items.findChild("item");
116    }
117
118    @NonNull
119    public Set<Integer> deviceIds(final Element item) {
120        Set<Integer> deviceIds = new HashSet<>();
121        if (item != null) {
122            final Element list = item.findChild("list");
123            if (list != null) {
124                for (Element device : list.getChildren()) {
125                    if (!device.getName().equals("device")) {
126                        continue;
127                    }
128                    try {
129                        Integer id = Integer.valueOf(device.getAttribute("id"));
130                        deviceIds.add(id);
131                    } catch (NumberFormatException e) {
132                        Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Encountered invalid <device> node in PEP (" + e.getMessage() + "):" + device.toString() + ", skipping...");
133                        continue;
134                    }
135                }
136            }
137        }
138        return deviceIds;
139    }
140
141    public Integer signedPreKeyId(final Element bundle) {
142        final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
143        if (signedPreKeyPublic == null) {
144            return null;
145        }
146        try {
147            return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
148        } catch (NumberFormatException e) {
149            return null;
150        }
151    }
152
153    public ECPublicKey signedPreKeyPublic(final Element bundle) {
154        ECPublicKey publicKey = null;
155        final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
156        if (signedPreKeyPublic == null) {
157            return null;
158        }
159        try {
160            publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(), Base64.DEFAULT), 0);
161        } catch (Throwable e) {
162            Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid signedPreKeyPublic in PEP: " + e.getMessage());
163        }
164        return publicKey;
165    }
166
167    public byte[] signedPreKeySignature(final Element bundle) {
168        final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
169        if (signedPreKeySignature == null) {
170            return null;
171        }
172        try {
173            return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT);
174        } catch (Throwable e) {
175            Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : Invalid base64 in signedPreKeySignature");
176            return null;
177        }
178    }
179
180    public IdentityKey identityKey(final Element bundle) {
181        IdentityKey identityKey = null;
182        final Element identityKeyElement = bundle.findChild("identityKey");
183        if (identityKeyElement == null) {
184            return null;
185        }
186        try {
187            identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
188        } catch (Throwable e) {
189            Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid identityKey in PEP: " + e.getMessage());
190        }
191        return identityKey;
192    }
193
194    public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
195        Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
196        Element item = getItem(packet);
197        if (item == null) {
198            Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Couldn't find <item> in bundle IQ packet: " + packet);
199            return null;
200        }
201        final Element bundleElement = item.findChild("bundle");
202        if (bundleElement == null) {
203            return null;
204        }
205        final Element prekeysElement = bundleElement.findChild("prekeys");
206        if (prekeysElement == null) {
207            Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Couldn't find <prekeys> in bundle IQ packet: " + packet);
208            return null;
209        }
210        for (Element preKeyPublicElement : prekeysElement.getChildren()) {
211            if (!preKeyPublicElement.getName().equals("preKeyPublic")) {
212                Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
213                continue;
214            }
215            Integer preKeyId = null;
216            try {
217                preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
218                final ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
219                preKeyRecords.put(preKeyId, preKeyPublic);
220            } catch (NumberFormatException e) {
221                Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "could not parse preKeyId from preKey " + preKeyPublicElement.toString());
222            } catch (Throwable e) {
223                Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Invalid preKeyPublic (ID=" + preKeyId + ") in PEP: " + e.getMessage() + ", skipping...");
224            }
225        }
226        return preKeyRecords;
227    }
228
229    public Pair<X509Certificate[], byte[]> verification(final IqPacket packet) {
230        Element item = getItem(packet);
231        Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null;
232        Element chain = verification != null ? verification.findChild("chain") : null;
233        Element signature = verification != null ? verification.findChild("signature") : null;
234        if (chain != null && signature != null) {
235            List<Element> certElements = chain.getChildren();
236            X509Certificate[] certificates = new X509Certificate[certElements.size()];
237            try {
238                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
239                int i = 0;
240                for (Element cert : certElements) {
241                    certificates[i] = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.getContent(), Base64.DEFAULT)));
242                    ++i;
243                }
244                return new Pair<>(certificates, Base64.decode(signature.getContent(), Base64.DEFAULT));
245            } catch (CertificateException e) {
246                return null;
247            }
248        } else {
249            return null;
250        }
251    }
252
253    public PreKeyBundle bundle(final IqPacket bundle) {
254        Element bundleItem = getItem(bundle);
255        if (bundleItem == null) {
256            return null;
257        }
258        final Element bundleElement = bundleItem.findChild("bundle");
259        if (bundleElement == null) {
260            return null;
261        }
262        ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
263        Integer signedPreKeyId = signedPreKeyId(bundleElement);
264        byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
265        IdentityKey identityKey = identityKey(bundleElement);
266        if (signedPreKeyId == null || signedPreKeyPublic == null || identityKey == null) {
267            return null;
268        }
269
270        return new PreKeyBundle(0, 0, 0, null,
271                signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
272    }
273
274    public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
275        List<PreKeyBundle> bundles = new ArrayList<>();
276        Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
277        if (preKeyPublics != null) {
278            for (Integer preKeyId : preKeyPublics.keySet()) {
279                ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
280                bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
281                        0, null, null, null));
282            }
283        }
284
285        return bundles;
286    }
287
288    @Override
289    public void onIqPacketReceived(final Account account, final IqPacket packet) {
290        final boolean isGet = packet.getType() == IqPacket.TYPE.GET;
291        if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
292            return;
293        }
294        if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) {
295            final Element query = packet.findChild("query");
296            // If this is in response to a query for the whole roster:
297            if (packet.getType() == IqPacket.TYPE.RESULT) {
298                account.getRoster().markAllAsNotInRoster();
299            }
300            this.rosterItems(account, query);
301        } else if ((packet.hasChild("block", Namespace.BLOCKING) || packet.hasChild("blocklist", Namespace.BLOCKING)) &&
302                packet.fromServer(account)) {
303            // Block list or block push.
304            Log.d(Config.LOGTAG, "Received blocklist update from server");
305            final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING);
306            final Element block = packet.findChild("block", Namespace.BLOCKING);
307            final Collection<Element> items = blocklist != null ? blocklist.getChildren() :
308                    (block != null ? block.getChildren() : null);
309            // If this is a response to a blocklist query, clear the block list and replace with the new one.
310            // Otherwise, just update the existing blocklist.
311            if (packet.getType() == IqPacket.TYPE.RESULT) {
312                account.clearBlocklist();
313                account.getXmppConnection().getFeatures().setBlockListRequested(true);
314            }
315            if (items != null) {
316                final Collection<Jid> jids = new ArrayList<>(items.size());
317                // Create a collection of Jids from the packet
318                for (final Element item : items) {
319                    if (item.getName().equals("item")) {
320                        final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
321                        if (jid != null) {
322                            jids.add(jid);
323                        }
324                    }
325                }
326                account.getBlocklist().addAll(jids);
327                if (packet.getType() == IqPacket.TYPE.SET) {
328                    boolean removed = false;
329                    for (Jid jid : jids) {
330                        removed |= mXmppConnectionService.removeBlockedConversations(account, jid);
331                    }
332                    if (removed) {
333                        mXmppConnectionService.updateConversationUi();
334                    }
335                }
336            }
337            // Update the UI
338            mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
339            if (packet.getType() == IqPacket.TYPE.SET) {
340                final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
341                mXmppConnectionService.sendIqPacket(account, response, null);
342            }
343        } else if (packet.hasChild("unblock", Namespace.BLOCKING) &&
344                packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) {
345            Log.d(Config.LOGTAG, "Received unblock update from server");
346            final Collection<Element> items = packet.findChild("unblock", Namespace.BLOCKING).getChildren();
347            if (items.size() == 0) {
348                // No children to unblock == unblock all
349                account.getBlocklist().clear();
350            } else {
351                final Collection<Jid> jids = new ArrayList<>(items.size());
352                for (final Element item : items) {
353                    if (item.getName().equals("item")) {
354                        final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"));
355                        if (jid != null) {
356                            jids.add(jid);
357                        }
358                    }
359                }
360                account.getBlocklist().removeAll(jids);
361            }
362            mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
363            final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
364            mXmppConnectionService.sendIqPacket(account, response, null);
365        } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
366                || packet.hasChild("data", "http://jabber.org/protocol/ibb")
367                || packet.hasChild("close", "http://jabber.org/protocol/ibb")) {
368            mXmppConnectionService.getJingleConnectionManager()
369                    .deliverIbbPacket(account, packet);
370        } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
371            final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet);
372            mXmppConnectionService.sendIqPacket(account, response, null);
373        } else if (packet.hasChild("query", "jabber:iq:version") && isGet) {
374            final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
375            mXmppConnectionService.sendIqPacket(account, response, null);
376        } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
377            final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
378            mXmppConnectionService.sendIqPacket(account, response, null);
379        } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) {
380            final IqPacket response;
381            if (mXmppConnectionService.useTorToConnect() || account.isOnion()) {
382                response = packet.generateResponse(IqPacket.TYPE.ERROR);
383                final Element error = response.addChild("error");
384                error.setAttribute("type", "cancel");
385                error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas");
386            } else {
387                response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
388            }
389            mXmppConnectionService.sendIqPacket(account, response, null);
390        } else if (packet.hasChild("pubsub", Namespace.PUBSUB) && packet.getType() == IqPacket.TYPE.SET) {
391            final Jid server = packet.getFrom();
392            final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
393            final Element publish = pubsub == null ? null : pubsub.findChild("publish");
394            final String node = publish == null ? null : publish.getAttribute("node");
395            final Element item = publish == null ? null : publish.findChild("item");
396            final Element notification = item == null ? null : item.findChild("notification", Namespace.PUSH);
397            if (notification != null && node != null && server != null) {
398                final Conversation conversation = mXmppConnectionService.findConversationByUuid(node);
399                if (conversation != null && conversation.getAccount() == account && conversation.getJid().getDomain().equals(server.getDomain())) {
400                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received muc push event for "+conversation.getJid().asBareJid());
401                    mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null);
402                    mXmppConnectionService.mucSelfPingAndRejoin(conversation);
403                } else {
404                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received push event for unknown conference from "+server);
405                    final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
406                    final Element error = response.addChild("error");
407                    error.setAttribute("type", "cancel");
408                    error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
409                    mXmppConnectionService.sendIqPacket(account, response, null);
410                }
411            }
412
413        } else {
414            if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
415                final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
416                final Element error = response.addChild("error");
417                error.setAttribute("type", "cancel");
418                error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas");
419                account.getXmppConnection().sendIqPacket(response, null);
420            }
421        }
422    }
423
424
425    public static List<Jid> items(IqPacket packet) {
426        ArrayList<Jid> items = new ArrayList<>();
427        final Element query = packet.findChild("query", Namespace.DISCO_ITEMS);
428        if (query == null) {
429            return items;
430        }
431        for(Element child : query.getChildren()) {
432            if ("item".equals(child.getName())) {
433                Jid jid = child.getAttributeAsJid("jid");
434                if (jid != null) {
435                    items.add(jid);
436                }
437            }
438        }
439        return items;
440    }
441
442    public static Room parseRoom(IqPacket packet) {
443        final Element query = packet.findChild("query", Namespace.DISCO_INFO);
444        if(query == null) {
445            return null;
446        }
447        final Element x = query.findChild("x");
448        if (x == null) {
449            return null;
450        }
451        final Element identity = query.findChild("identity");
452        Data data = Data.parse(x);
453        String address = packet.getFrom().toEscapedString();
454        String name = identity == null ? null : identity.getAttribute("name");
455        String roomName = data.getValue("muc#roomconfig_roomname");;
456        String description = data.getValue("muc#roominfo_description");
457        String language = data.getValue("muc#roominfo_lang");
458        String occupants = data.getValue("muc#roominfo_occupants");
459        int nusers;
460        try {
461            nusers = occupants == null ? 0 : Integer.parseInt(occupants);
462        } catch (NumberFormatException e) {
463            nusers = 0;
464        }
465
466        return new Room(
467                address,
468                TextUtils.isEmpty(roomName) ? name : roomName,
469                description,
470                language,
471                nusers
472        );
473    }
474
475}