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