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}