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