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