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