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