IqParser.java

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