IqParser.java

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