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