1package eu.siacs.conversations.parser;
2
3import android.util.Log;
4import android.util.Pair;
5import androidx.annotation.NonNull;
6import com.google.common.base.CharMatcher;
7import com.google.common.io.BaseEncoding;
8import eu.siacs.conversations.Config;
9import eu.siacs.conversations.crypto.axolotl.AxolotlService;
10import eu.siacs.conversations.services.XmppConnectionService;
11import eu.siacs.conversations.xml.Element;
12import eu.siacs.conversations.xml.Namespace;
13import eu.siacs.conversations.xmpp.XmppConnection;
14import eu.siacs.conversations.xmpp.manager.BlockingManager;
15import eu.siacs.conversations.xmpp.manager.DiscoManager;
16import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
17import eu.siacs.conversations.xmpp.manager.PingManager;
18import eu.siacs.conversations.xmpp.manager.RosterManager;
19import eu.siacs.conversations.xmpp.manager.UnifiedPushManager;
20import im.conversations.android.xmpp.model.blocking.Block;
21import im.conversations.android.xmpp.model.blocking.Unblock;
22import im.conversations.android.xmpp.model.disco.info.InfoQuery;
23import im.conversations.android.xmpp.model.error.Condition;
24import im.conversations.android.xmpp.model.error.Error;
25import im.conversations.android.xmpp.model.ibb.InBandByteStream;
26import im.conversations.android.xmpp.model.ping.Ping;
27import im.conversations.android.xmpp.model.roster.Query;
28import im.conversations.android.xmpp.model.stanza.Iq;
29import im.conversations.android.xmpp.model.time.Time;
30import im.conversations.android.xmpp.model.up.Push;
31import im.conversations.android.xmpp.model.version.Version;
32import java.io.ByteArrayInputStream;
33import java.security.cert.CertificateException;
34import java.security.cert.CertificateFactory;
35import java.security.cert.X509Certificate;
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.HashSet;
39import java.util.List;
40import java.util.Map;
41import java.util.Set;
42import java.util.function.Consumer;
43
44import eu.siacs.conversations.Config;
45import eu.siacs.conversations.crypto.axolotl.AxolotlService;
46import eu.siacs.conversations.entities.Account;
47import eu.siacs.conversations.entities.Contact;
48import eu.siacs.conversations.entities.Conversation;
49import eu.siacs.conversations.entities.Room;
50import eu.siacs.conversations.services.XmppConnectionService;
51import eu.siacs.conversations.xml.Element;
52import eu.siacs.conversations.xml.Namespace;
53import eu.siacs.conversations.xmpp.Jid;
54import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
55import eu.siacs.conversations.xmpp.forms.Data;
56import im.conversations.android.xmpp.model.stanza.Iq;
57
58import org.whispersystems.libsignal.IdentityKey;
59import org.whispersystems.libsignal.InvalidKeyException;
60import org.whispersystems.libsignal.ecc.Curve;
61import org.whispersystems.libsignal.ecc.ECPublicKey;
62import org.whispersystems.libsignal.state.PreKeyBundle;
63
64public class IqParser extends AbstractParser implements Consumer<Iq> {
65
66 public IqParser(final XmppConnectionService service, final XmppConnection connection) {
67 super(service, connection);
68 }
69
70 public static String avatarData(final Iq packet) {
71 final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
72 if (pubsub == null) {
73 return null;
74 }
75 final Element items = pubsub.findChild("items");
76 if (items == null) {
77 return null;
78 }
79 return AbstractParser.avatarData(items);
80 }
81
82 public static Element getItem(final Iq packet) {
83 final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
84 if (pubsub == null) {
85 return null;
86 }
87 final Element items = pubsub.findChild("items");
88 if (items == null) {
89 return null;
90 }
91 return items.findChild("item");
92 }
93
94 @NonNull
95 public static Set<Integer> deviceIds(final Element item) {
96 Set<Integer> deviceIds = new HashSet<>();
97 if (item != null) {
98 final Element list = item.findChild("list");
99 if (list != null) {
100 for (Element device : list.getChildren()) {
101 if (!device.getName().equals("device")) {
102 continue;
103 }
104 try {
105 Integer id = Integer.valueOf(device.getAttribute("id"));
106 deviceIds.add(id);
107 } catch (NumberFormatException e) {
108 Log.e(
109 Config.LOGTAG,
110 AxolotlService.LOGPREFIX
111 + " : "
112 + "Encountered invalid <device> node in PEP ("
113 + e.getMessage()
114 + "):"
115 + device
116 + ", skipping...");
117 }
118 }
119 }
120 }
121 return deviceIds;
122 }
123
124 private static Integer signedPreKeyId(final Element bundle) {
125 final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
126 if (signedPreKeyPublic == null) {
127 return null;
128 }
129 try {
130 return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
131 } catch (NumberFormatException e) {
132 return null;
133 }
134 }
135
136 private static ECPublicKey signedPreKeyPublic(final Element bundle) {
137 ECPublicKey publicKey = null;
138 final String signedPreKeyPublic = bundle.findChildContent("signedPreKeyPublic");
139 if (signedPreKeyPublic == null) {
140 return null;
141 }
142 try {
143 publicKey = Curve.decodePoint(base64decode(signedPreKeyPublic), 0);
144 } catch (final IllegalArgumentException | InvalidKeyException e) {
145 Log.e(
146 Config.LOGTAG,
147 AxolotlService.LOGPREFIX
148 + " : "
149 + "Invalid signedPreKeyPublic in PEP: "
150 + e.getMessage());
151 }
152 return publicKey;
153 }
154
155 private static byte[] signedPreKeySignature(final Element bundle) {
156 final String signedPreKeySignature = bundle.findChildContent("signedPreKeySignature");
157 if (signedPreKeySignature == null) {
158 return null;
159 }
160 try {
161 return base64decode(signedPreKeySignature);
162 } catch (final IllegalArgumentException e) {
163 Log.e(
164 Config.LOGTAG,
165 AxolotlService.LOGPREFIX + " : Invalid base64 in signedPreKeySignature");
166 return null;
167 }
168 }
169
170 private static IdentityKey identityKey(final Element bundle) {
171 final String identityKey = bundle.findChildContent("identityKey");
172 if (identityKey == null) {
173 return null;
174 }
175 try {
176 return new IdentityKey(base64decode(identityKey), 0);
177 } catch (final IllegalArgumentException | InvalidKeyException e) {
178 Log.e(
179 Config.LOGTAG,
180 AxolotlService.LOGPREFIX
181 + " : "
182 + "Invalid identityKey in PEP: "
183 + e.getMessage());
184 return null;
185 }
186 }
187
188 public static Map<Integer, ECPublicKey> preKeyPublics(final Iq packet) {
189 Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
190 Element item = getItem(packet);
191 if (item == null) {
192 Log.d(
193 Config.LOGTAG,
194 AxolotlService.LOGPREFIX
195 + " : "
196 + "Couldn't find <item> in bundle IQ packet: "
197 + packet);
198 return null;
199 }
200 final Element bundleElement = item.findChild("bundle");
201 if (bundleElement == null) {
202 return null;
203 }
204 final Element prekeysElement = bundleElement.findChild("prekeys");
205 if (prekeysElement == null) {
206 Log.d(
207 Config.LOGTAG,
208 AxolotlService.LOGPREFIX
209 + " : "
210 + "Couldn't find <prekeys> in bundle IQ packet: "
211 + packet);
212 return null;
213 }
214 for (Element preKeyPublicElement : prekeysElement.getChildren()) {
215 if (!preKeyPublicElement.getName().equals("preKeyPublic")) {
216 Log.d(
217 Config.LOGTAG,
218 AxolotlService.LOGPREFIX
219 + " : "
220 + "Encountered unexpected tag in prekeys list: "
221 + preKeyPublicElement);
222 continue;
223 }
224 final String preKey = preKeyPublicElement.getContent();
225 if (preKey == null) {
226 continue;
227 }
228 Integer preKeyId = null;
229 try {
230 preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
231 final ECPublicKey preKeyPublic = Curve.decodePoint(base64decode(preKey), 0);
232 preKeyRecords.put(preKeyId, preKeyPublic);
233 } catch (NumberFormatException e) {
234 Log.e(
235 Config.LOGTAG,
236 AxolotlService.LOGPREFIX
237 + " : "
238 + "could not parse preKeyId from preKey "
239 + preKeyPublicElement);
240 } catch (Throwable e) {
241 Log.e(
242 Config.LOGTAG,
243 AxolotlService.LOGPREFIX
244 + " : "
245 + "Invalid preKeyPublic (ID="
246 + preKeyId
247 + ") in PEP: "
248 + e.getMessage()
249 + ", skipping...");
250 }
251 }
252 return preKeyRecords;
253 }
254
255 private static byte[] base64decode(String input) {
256 return BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(input));
257 }
258
259 public static Pair<X509Certificate[], byte[]> verification(final Iq packet) {
260 Element item = getItem(packet);
261 Element verification =
262 item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null;
263 Element chain = verification != null ? verification.findChild("chain") : null;
264 String signature = verification != null ? verification.findChildContent("signature") : null;
265 if (chain != null && signature != null) {
266 List<Element> certElements = chain.getChildren();
267 X509Certificate[] certificates = new X509Certificate[certElements.size()];
268 try {
269 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
270 int i = 0;
271 for (final Element certElement : certElements) {
272 final String cert = certElement.getContent();
273 if (cert == null) {
274 continue;
275 }
276 certificates[i] =
277 (X509Certificate)
278 certificateFactory.generateCertificate(
279 new ByteArrayInputStream(
280 BaseEncoding.base64().decode(cert)));
281 ++i;
282 }
283 return new Pair<>(certificates, BaseEncoding.base64().decode(signature));
284 } catch (CertificateException e) {
285 return null;
286 }
287 } else {
288 return null;
289 }
290 }
291
292 public static PreKeyBundle bundle(final Iq bundle) {
293 final Element bundleItem = getItem(bundle);
294 if (bundleItem == null) {
295 return null;
296 }
297 final Element bundleElement = bundleItem.findChild("bundle");
298 if (bundleElement == null) {
299 return null;
300 }
301 final ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
302 final Integer signedPreKeyId = signedPreKeyId(bundleElement);
303 final byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
304 final IdentityKey identityKey = identityKey(bundleElement);
305 if (signedPreKeyId == null
306 || signedPreKeyPublic == null
307 || identityKey == null
308 || signedPreKeySignature == null
309 || signedPreKeySignature.length == 0) {
310 return null;
311 }
312 return new PreKeyBundle(
313 0,
314 0,
315 0,
316 null,
317 signedPreKeyId,
318 signedPreKeyPublic,
319 signedPreKeySignature,
320 identityKey);
321 }
322
323 public static List<PreKeyBundle> preKeys(final Iq preKeys) {
324 List<PreKeyBundle> bundles = new ArrayList<>();
325 Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
326 if (preKeyPublics != null) {
327 for (Integer preKeyId : preKeyPublics.keySet()) {
328 ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
329 bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, 0, null, null, null));
330 }
331 }
332
333 return bundles;
334 }
335
336 @Override
337 public void accept(final Iq packet) {
338 final var type = packet.getType();
339 switch (type) {
340 case SET -> acceptPush(packet);
341 case GET -> acceptRequest(packet);
342 default ->
343 throw new AssertionError(
344 "IQ results and errors should are handled in callbacks");
345 }
346 }
347
348 private void acceptPush(final Iq packet) {
349 if (packet.hasExtension(Query.class)) {
350 this.getManager(RosterManager.class).push(packet);
351 } else if (packet.hasExtension(Block.class)) {
352 this.getManager(BlockingManager.class).pushBlock(packet);
353 } else if (packet.hasExtension(Unblock.class)) {
354 this.getManager(BlockingManager.class).pushUnblock(packet);
355 } else if (packet.hasExtension(InBandByteStream.class)) {
356 mXmppConnectionService
357 .getJingleConnectionManager()
358 .deliverIbbPacket(getAccount(), packet);
359 } else if (packet.hasExtension(Push.class)) {
360 this.getManager(UnifiedPushManager.class).push(packet);
361 } else {
362 this.connection.sendErrorFor(
363 packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented());
364 }
365 }
366
367 private void acceptRequest(final Iq packet) {
368 if (packet.hasExtension(InfoQuery.class)) {
369 this.getManager(DiscoManager.class).handleInfoQuery(packet);
370 } else if (packet.hasExtension(Version.class)) {
371 this.getManager(DiscoManager.class).handleVersionRequest(packet);
372 } else if (packet.hasExtension(Time.class)) {
373 this.getManager(EntityTimeManager.class).request(packet);
374 } else if (packet.hasExtension(Ping.class)) {
375 this.getManager(PingManager.class).pong(packet);
376 } else if (packet.hasChild("data", "urn:xmpp:bob")) {
377 mXmppConnectionService.sendIqPacket(getAccount(), mXmppConnectionService.getIqGenerator().bobResponse(packet), null);
378 } else {
379 this.connection.sendErrorFor(
380 packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented());
381 }
382 }
383}