IqParser.java

  1package eu.siacs.conversations.parser;
  2
  3import android.support.annotation.NonNull;
  4import android.util.Base64;
  5import android.util.Log;
  6import android.util.Pair;
  7
  8import org.whispersystems.libaxolotl.IdentityKey;
  9import org.whispersystems.libaxolotl.InvalidKeyException;
 10import org.whispersystems.libaxolotl.ecc.Curve;
 11import org.whispersystems.libaxolotl.ecc.ECPublicKey;
 12import org.whispersystems.libaxolotl.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.services.XmppConnectionService;
 31import eu.siacs.conversations.utils.Xmlns;
 32import eu.siacs.conversations.xml.Element;
 33import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 34import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 35import eu.siacs.conversations.xmpp.jid.Jid;
 36import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 37
 38public class IqParser extends AbstractParser implements OnIqPacketReceived {
 39
 40	public IqParser(final XmppConnectionService service) {
 41		super(service);
 42	}
 43
 44	private void rosterItems(final Account account, final Element query) {
 45		final String version = query.getAttribute("ver");
 46		if (version != null) {
 47			account.getRoster().setVersion(version);
 48		}
 49		for (final Element item : query.getChildren()) {
 50			if (item.getName().equals("item")) {
 51				final Jid jid = item.getAttributeAsJid("jid");
 52				if (jid == null) {
 53					continue;
 54				}
 55				final String name = item.getAttribute("name");
 56				final String subscription = item.getAttribute("subscription");
 57				final Contact contact = account.getRoster().getContact(jid);
 58				if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
 59					contact.setServerName(name);
 60					contact.parseGroupsFromElement(item);
 61				}
 62				if (subscription != null) {
 63					if (subscription.equals("remove")) {
 64						contact.resetOption(Contact.Options.IN_ROSTER);
 65						contact.resetOption(Contact.Options.DIRTY_DELETE);
 66						contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
 67					} else {
 68						contact.setOption(Contact.Options.IN_ROSTER);
 69						contact.resetOption(Contact.Options.DIRTY_PUSH);
 70						contact.parseSubscriptionFromElement(item);
 71					}
 72				}
 73				mXmppConnectionService.getAvatarService().clear(contact);
 74			}
 75		}
 76		mXmppConnectionService.updateConversationUi();
 77		mXmppConnectionService.updateRosterUi();
 78	}
 79
 80	public String avatarData(final IqPacket packet) {
 81		final Element pubsub = packet.findChild("pubsub",
 82				"http://jabber.org/protocol/pubsub");
 83		if (pubsub == null) {
 84			return null;
 85		}
 86		final Element items = pubsub.findChild("items");
 87		if (items == null) {
 88			return null;
 89		}
 90		return super.avatarData(items);
 91	}
 92
 93	public Element getItem(final IqPacket packet) {
 94		final Element pubsub = packet.findChild("pubsub",
 95				"http://jabber.org/protocol/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 items.findChild("item");
104	}
105
106	@NonNull
107	public Set<Integer> deviceIds(final Element item) {
108		Set<Integer> deviceIds = new HashSet<>();
109		if (item != null) {
110			final Element list = item.findChild("list");
111			if (list != null) {
112				for (Element device : list.getChildren()) {
113					if (!device.getName().equals("device")) {
114						continue;
115					}
116					try {
117						Integer id = Integer.valueOf(device.getAttribute("id"));
118						deviceIds.add(id);
119					} catch (NumberFormatException e) {
120						Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered invalid <device> node in PEP ("+e.getMessage()+"):" + device.toString()+ ", skipping...");
121						continue;
122					}
123				}
124			}
125		}
126		return deviceIds;
127	}
128
129	public Integer signedPreKeyId(final Element bundle) {
130		final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
131		if(signedPreKeyPublic == null) {
132			return null;
133		}
134		return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
135	}
136
137	public ECPublicKey signedPreKeyPublic(final Element bundle) {
138		ECPublicKey publicKey = null;
139		final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
140		if(signedPreKeyPublic == null) {
141			return null;
142		}
143		try {
144			publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
145		} catch (InvalidKeyException | IllegalArgumentException | NullPointerException e) {
146			Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage());
147		}
148		return publicKey;
149	}
150
151	public byte[] signedPreKeySignature(final Element bundle) {
152		final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
153		if(signedPreKeySignature == null) {
154			return null;
155		}
156		try {
157			return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT);
158		} catch (IllegalArgumentException e) {
159			Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : Invalid base64 in signedPreKeySignature");
160			return null;
161		}
162	}
163
164	public IdentityKey identityKey(final Element bundle) {
165		IdentityKey identityKey = null;
166		final Element identityKeyElement = bundle.findChild("identityKey");
167		if(identityKeyElement == null) {
168			return null;
169		}
170		try {
171			identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
172		} catch (InvalidKeyException | IllegalArgumentException | NullPointerException e) {
173			Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
174		}
175		return identityKey;
176	}
177
178	public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
179		Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
180		Element item = getItem(packet);
181		if (item == null) {
182			Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet);
183			return null;
184		}
185		final Element bundleElement = item.findChild("bundle");
186		if(bundleElement == null) {
187			return null;
188		}
189		final Element prekeysElement = bundleElement.findChild("prekeys");
190		if(prekeysElement == null) {
191			Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet);
192			return null;
193		}
194		for(Element preKeyPublicElement : prekeysElement.getChildren()) {
195			if(!preKeyPublicElement.getName().equals("preKeyPublic")){
196				Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
197				continue;
198			}
199			Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
200			try {
201				ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
202				preKeyRecords.put(preKeyId, preKeyPublic);
203			} catch (InvalidKeyException | IllegalArgumentException | NullPointerException e) {
204				Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
205				continue;
206			}
207		}
208		return preKeyRecords;
209	}
210
211	public Pair<X509Certificate[],byte[]> verification(final IqPacket packet) {
212		Element item = getItem(packet);
213		Element verification = item != null ? item.findChild("verification",AxolotlService.PEP_PREFIX) : null;
214		Element chain = verification != null ? verification.findChild("chain") : null;
215		Element signature = verification != null ? verification.findChild("signature") : null;
216		if (chain != null && signature != null) {
217			List<Element> certElements = chain.getChildren();
218			X509Certificate[] certificates = new X509Certificate[certElements.size()];
219			try {
220				CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
221				int i = 0;
222				for(Element cert : certElements) {
223					certificates[i] = (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.getContent(),Base64.DEFAULT)));
224					++i;
225				}
226				return new Pair<>(certificates,Base64.decode(signature.getContent(),Base64.DEFAULT));
227			} catch (CertificateException e) {
228				return null;
229			}
230		} else {
231			return null;
232		}
233	}
234
235	public PreKeyBundle bundle(final IqPacket bundle) {
236		Element bundleItem = getItem(bundle);
237		if(bundleItem == null) {
238			return null;
239		}
240		final Element bundleElement = bundleItem.findChild("bundle");
241		if(bundleElement == null) {
242			return null;
243		}
244		ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
245		Integer signedPreKeyId = signedPreKeyId(bundleElement);
246		byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
247		IdentityKey identityKey = identityKey(bundleElement);
248		if(signedPreKeyPublic == null || identityKey == null) {
249			return null;
250		}
251
252		return new PreKeyBundle(0, 0, 0, null,
253				signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
254	}
255
256	public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
257		List<PreKeyBundle> bundles = new ArrayList<>();
258		Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
259		if ( preKeyPublics != null) {
260			for (Integer preKeyId : preKeyPublics.keySet()) {
261				ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
262				bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
263						0, null, null, null));
264			}
265		}
266
267		return bundles;
268	}
269
270	@Override
271	public void onIqPacketReceived(final Account account, final IqPacket packet) {
272		if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
273			return;
274		} else if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
275			final Element query = packet.findChild("query");
276			// If this is in response to a query for the whole roster:
277			if (packet.getType() == IqPacket.TYPE.RESULT) {
278				account.getRoster().markAllAsNotInRoster();
279			}
280			this.rosterItems(account, query);
281		} else if ((packet.hasChild("block", Xmlns.BLOCKING) || packet.hasChild("blocklist", Xmlns.BLOCKING)) &&
282				packet.fromServer(account)) {
283			// Block list or block push.
284			Log.d(Config.LOGTAG, "Received blocklist update from server");
285			final Element blocklist = packet.findChild("blocklist", Xmlns.BLOCKING);
286			final Element block = packet.findChild("block", Xmlns.BLOCKING);
287			final Collection<Element> items = blocklist != null ? blocklist.getChildren() :
288				(block != null ? block.getChildren() : null);
289			// If this is a response to a blocklist query, clear the block list and replace with the new one.
290			// Otherwise, just update the existing blocklist.
291			if (packet.getType() == IqPacket.TYPE.RESULT) {
292				account.clearBlocklist();
293				account.getXmppConnection().getFeatures().setBlockListRequested(true);
294			}
295			if (items != null) {
296				final Collection<Jid> jids = new ArrayList<>(items.size());
297				// Create a collection of Jids from the packet
298				for (final Element item : items) {
299					if (item.getName().equals("item")) {
300						final Jid jid = item.getAttributeAsJid("jid");
301						if (jid != null) {
302							jids.add(jid);
303						}
304					}
305				}
306				account.getBlocklist().addAll(jids);
307			}
308			// Update the UI
309			mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
310		} else if (packet.hasChild("unblock", Xmlns.BLOCKING) &&
311				packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) {
312			Log.d(Config.LOGTAG, "Received unblock update from server");
313			final Collection<Element> items = packet.findChild("unblock", Xmlns.BLOCKING).getChildren();
314			if (items.size() == 0) {
315				// No children to unblock == unblock all
316				account.getBlocklist().clear();
317			} else {
318				final Collection<Jid> jids = new ArrayList<>(items.size());
319				for (final Element item : items) {
320					if (item.getName().equals("item")) {
321						final Jid jid = item.getAttributeAsJid("jid");
322						if (jid != null) {
323							jids.add(jid);
324						}
325					}
326				}
327				account.getBlocklist().removeAll(jids);
328			}
329			mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
330		} else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
331				|| packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
332			mXmppConnectionService.getJingleConnectionManager()
333				.deliverIbbPacket(account, packet);
334		} else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
335			final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(packet);
336			mXmppConnectionService.sendIqPacket(account, response, null);
337		} else if (packet.hasChild("query","jabber:iq:version")) {
338			final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
339			mXmppConnectionService.sendIqPacket(account,response,null);
340		} else if (packet.hasChild("ping", "urn:xmpp:ping")) {
341			final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
342			mXmppConnectionService.sendIqPacket(account, response, null);
343		} else {
344			if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
345				final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
346				final Element error = response.addChild("error");
347				error.setAttribute("type", "cancel");
348				error.addChild("feature-not-implemented","urn:ietf:params:xml:ns:xmpp-stanzas");
349				account.getXmppConnection().sendIqPacket(response, null);
350			}
351		}
352	}
353
354}