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