IqParser.java

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