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