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 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		return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT);
153	}
154
155	public IdentityKey identityKey(final Element bundle) {
156		IdentityKey identityKey = null;
157		final Element identityKeyElement = bundle.findChild("identityKey");
158		if(identityKeyElement == null) {
159			return null;
160		}
161		try {
162			identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
163		} catch (InvalidKeyException e) {
164			Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
165		}
166		return identityKey;
167	}
168
169	public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
170		Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
171		Element item = getItem(packet);
172		if (item == null) {
173			Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet);
174			return null;
175		}
176		final Element bundleElement = item.findChild("bundle");
177		if(bundleElement == null) {
178			return null;
179		}
180		final Element prekeysElement = bundleElement.findChild("prekeys");
181		if(prekeysElement == null) {
182			Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet);
183			return null;
184		}
185		for(Element preKeyPublicElement : prekeysElement.getChildren()) {
186			if(!preKeyPublicElement.getName().equals("preKeyPublic")){
187				Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
188				continue;
189			}
190			Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
191			try {
192				ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
193				preKeyRecords.put(preKeyId, preKeyPublic);
194			} catch (InvalidKeyException e) {
195				Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
196				continue;
197			}
198		}
199		return preKeyRecords;
200	}
201
202	public PreKeyBundle bundle(final IqPacket bundle) {
203		Element bundleItem = getItem(bundle);
204		if(bundleItem == null) {
205			return null;
206		}
207		final Element bundleElement = bundleItem.findChild("bundle");
208		if(bundleElement == null) {
209			return null;
210		}
211		ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
212		Integer signedPreKeyId = signedPreKeyId(bundleElement);
213		byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
214		IdentityKey identityKey = identityKey(bundleElement);
215		if(signedPreKeyPublic == null || identityKey == null) {
216			return null;
217		}
218
219		return new PreKeyBundle(0, 0, 0, null,
220				signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
221	}
222
223	public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
224		List<PreKeyBundle> bundles = new ArrayList<>();
225		Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
226		if ( preKeyPublics != null) {
227			for (Integer preKeyId : preKeyPublics.keySet()) {
228				ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
229				bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
230						0, null, null, null));
231			}
232		}
233
234		return bundles;
235	}
236
237	@Override
238	public void onIqPacketReceived(final Account account, final IqPacket packet) {
239		if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
240			final Element query = packet.findChild("query");
241			// If this is in response to a query for the whole roster:
242			if (packet.getType() == IqPacket.TYPE.RESULT) {
243				account.getRoster().markAllAsNotInRoster();
244			}
245			this.rosterItems(account, query);
246		} else if ((packet.hasChild("block", Xmlns.BLOCKING) || packet.hasChild("blocklist", Xmlns.BLOCKING)) &&
247				packet.fromServer(account)) {
248			// Block list or block push.
249			Log.d(Config.LOGTAG, "Received blocklist update from server");
250			final Element blocklist = packet.findChild("blocklist", Xmlns.BLOCKING);
251			final Element block = packet.findChild("block", Xmlns.BLOCKING);
252			final Collection<Element> items = blocklist != null ? blocklist.getChildren() :
253				(block != null ? block.getChildren() : null);
254			// If this is a response to a blocklist query, clear the block list and replace with the new one.
255			// Otherwise, just update the existing blocklist.
256			if (packet.getType() == IqPacket.TYPE.RESULT) {
257				account.clearBlocklist();
258				account.getXmppConnection().getFeatures().setBlockListRequested(true);
259			}
260			if (items != null) {
261				final Collection<Jid> jids = new ArrayList<>(items.size());
262				// Create a collection of Jids from the packet
263				for (final Element item : items) {
264					if (item.getName().equals("item")) {
265						final Jid jid = item.getAttributeAsJid("jid");
266						if (jid != null) {
267							jids.add(jid);
268						}
269					}
270				}
271				account.getBlocklist().addAll(jids);
272			}
273			// Update the UI
274			mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
275		} else if (packet.hasChild("unblock", Xmlns.BLOCKING) &&
276				packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) {
277			Log.d(Config.LOGTAG, "Received unblock update from server");
278			final Collection<Element> items = packet.findChild("unblock", Xmlns.BLOCKING).getChildren();
279			if (items.size() == 0) {
280				// No children to unblock == unblock all
281				account.getBlocklist().clear();
282			} else {
283				final Collection<Jid> jids = new ArrayList<>(items.size());
284				for (final Element item : items) {
285					if (item.getName().equals("item")) {
286						final Jid jid = item.getAttributeAsJid("jid");
287						if (jid != null) {
288							jids.add(jid);
289						}
290					}
291				}
292				account.getBlocklist().removeAll(jids);
293			}
294			mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
295		} else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
296				|| packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
297			mXmppConnectionService.getJingleConnectionManager()
298				.deliverIbbPacket(account, packet);
299		} else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
300			final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(packet);
301			mXmppConnectionService.sendIqPacket(account, response, null);
302		} else if (packet.hasChild("query","jabber:iq:version")) {
303			final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
304			mXmppConnectionService.sendIqPacket(account,response,null);
305		} else if (packet.hasChild("ping", "urn:xmpp:ping")) {
306			final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
307			mXmppConnectionService.sendIqPacket(account, response, null);
308		} else {
309			if ((packet.getType() == IqPacket.TYPE.GET)
310					|| (packet.getType() == IqPacket.TYPE.SET)) {
311				final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
312				final Element error = response.addChild("error");
313				error.setAttribute("type", "cancel");
314				error.addChild("feature-not-implemented",
315						"urn:ietf:params:xml:ns:xmpp-stanzas");
316				account.getXmppConnection().sendIqPacket(response, null);
317					}
318		}
319	}
320
321}