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}