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