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