IqParser.java

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