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