IqParser.java

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