IqParser.java

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