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