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