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