IqParser.java

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