IqParser.java

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