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