IqGenerator.java

  1package eu.siacs.conversations.generator;
  2
  3import android.os.Bundle;
  4import android.util.Base64;
  5import android.util.Base64OutputStream;
  6import android.util.Log;
  7
  8import com.cheogram.android.BobTransfer;
  9
 10import com.google.common.base.Strings;
 11import com.google.common.io.ByteStreams;
 12
 13import org.whispersystems.libsignal.IdentityKey;
 14import org.whispersystems.libsignal.ecc.ECPublicKey;
 15import org.whispersystems.libsignal.state.PreKeyRecord;
 16import org.whispersystems.libsignal.state.SignedPreKeyRecord;
 17
 18import java.io.ByteArrayOutputStream;
 19import java.io.FileInputStream;
 20import java.io.IOException;
 21import eu.siacs.conversations.Config;
 22import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 23import eu.siacs.conversations.services.MessageArchiveService;
 24import eu.siacs.conversations.services.XmppConnectionService;
 25import eu.siacs.conversations.xml.Element;
 26import eu.siacs.conversations.xml.Namespace;
 27import eu.siacs.conversations.xmpp.Jid;
 28import eu.siacs.conversations.xmpp.forms.Data;
 29import im.conversations.android.xmpp.model.stanza.Iq;
 30import java.security.cert.CertificateEncodingException;
 31import java.security.cert.X509Certificate;
 32import java.util.Set;
 33import java.util.TimeZone;
 34import java.util.UUID;
 35
 36import io.ipfs.cid.Cid;
 37
 38import eu.siacs.conversations.Config;
 39import eu.siacs.conversations.R;
 40import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 41import eu.siacs.conversations.entities.Account;
 42import eu.siacs.conversations.entities.Bookmark;
 43import eu.siacs.conversations.entities.Conversation;
 44import eu.siacs.conversations.entities.DownloadableFile;
 45import eu.siacs.conversations.entities.Message;
 46import eu.siacs.conversations.services.MessageArchiveService;
 47import eu.siacs.conversations.services.QuickConversationsService;
 48import eu.siacs.conversations.services.XmppConnectionService;
 49import eu.siacs.conversations.xml.Element;
 50import eu.siacs.conversations.xml.Namespace;
 51import eu.siacs.conversations.xmpp.Jid;
 52import eu.siacs.conversations.xmpp.forms.Data;
 53import eu.siacs.conversations.xmpp.pep.Avatar;
 54import im.conversations.android.xmpp.model.stanza.Iq;
 55import org.whispersystems.libsignal.IdentityKey;
 56import org.whispersystems.libsignal.ecc.ECPublicKey;
 57import org.whispersystems.libsignal.state.PreKeyRecord;
 58import org.whispersystems.libsignal.state.SignedPreKeyRecord;
 59
 60public class IqGenerator extends AbstractGenerator {
 61
 62    public IqGenerator(final XmppConnectionService service) {
 63        super(service);
 64    }
 65
 66    protected Iq publish(final String node, final Element item, final Bundle options) {
 67        final var packet = new Iq(Iq.Type.SET);
 68        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
 69        final Element publish = pubsub.addChild("publish");
 70        publish.setAttribute("node", node);
 71        publish.addChild(item);
 72        if (options != null) {
 73            final Element publishOptions = pubsub.addChild("publish-options");
 74            publishOptions.addChild(Data.create(Namespace.PUBSUB_PUBLISH_OPTIONS, options));
 75        }
 76        return packet;
 77    }
 78
 79    protected Iq publish(final String node, final Element item) {
 80        return publish(node, item, null);
 81    }
 82
 83    private Iq retrieve(String node, Element item) {
 84        final var packet = new Iq(Iq.Type.GET);
 85        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
 86        final Element items = pubsub.addChild("items");
 87        items.setAttribute("node", node);
 88        if (item != null) {
 89            items.addChild(item);
 90        }
 91        return packet;
 92    }
 93
 94    public Iq retrieveVcard4(final Jid jid) {
 95        final var packet = retrieve("urn:xmpp:vcard4", null);
 96        packet.setTo(jid);
 97        return packet;
 98    }
 99
100    public Iq retrieveDeviceIds(final Jid to) {
101        final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
102        if (to != null) {
103            packet.setTo(to);
104        }
105        return packet;
106    }
107
108    public Iq retrieveBundlesForDevice(final Jid to, final int deviceid) {
109        final var packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null);
110        packet.setTo(to);
111        return packet;
112    }
113
114    public Iq retrieveVerificationForDevice(final Jid to, final int deviceid) {
115        final var packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null);
116        packet.setTo(to);
117        return packet;
118    }
119
120    public Iq publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
121        final Element item = new Element("item");
122        item.setAttribute("id", "current");
123        final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
124        for (Integer id : ids) {
125            final Element device = new Element("device");
126            device.setAttribute("id", id);
127            list.addChild(device);
128        }
129        return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions);
130    }
131
132    public Iq publishBundles(
133            final SignedPreKeyRecord signedPreKeyRecord,
134            final IdentityKey identityKey,
135            final Set<PreKeyRecord> preKeyRecords,
136            final int deviceId,
137            Bundle publishOptions) {
138        final Element item = new Element("item");
139        item.setAttribute("id", "current");
140        final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
141        final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
142        signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
143        ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
144        signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.NO_WRAP));
145        final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
146        signedPreKeySignature.setContent(
147                Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.NO_WRAP));
148        final Element identityKeyElement = bundle.addChild("identityKey");
149        identityKeyElement.setContent(
150                Base64.encodeToString(identityKey.serialize(), Base64.NO_WRAP));
151
152        final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
153        for (PreKeyRecord preKeyRecord : preKeyRecords) {
154            final Element prekey = prekeys.addChild("preKeyPublic");
155            prekey.setAttribute("preKeyId", preKeyRecord.getId());
156            prekey.setContent(
157                    Base64.encodeToString(
158                            preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.NO_WRAP));
159        }
160
161        return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions);
162    }
163
164    public Iq publishVerification(
165            byte[] signature, X509Certificate[] certificates, final int deviceId) {
166        final Element item = new Element("item");
167        item.setAttribute("id", "current");
168        final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
169        final Element chain = verification.addChild("chain");
170        for (int i = 0; i < certificates.length; ++i) {
171            try {
172                Element certificate = chain.addChild("certificate");
173                certificate.setContent(
174                        Base64.encodeToString(certificates[i].getEncoded(), Base64.NO_WRAP));
175                certificate.setAttribute("index", i);
176            } catch (CertificateEncodingException e) {
177                Log.d(Config.LOGTAG, "could not encode certificate");
178            }
179        }
180        verification
181                .addChild("signature")
182                .setContent(Base64.encodeToString(signature, Base64.NO_WRAP));
183        return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item);
184    }
185
186    public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
187        final Iq packet = new Iq(Iq.Type.SET);
188        final Element query = packet.addChild("query", mam.version.namespace);
189        query.setAttribute("queryid", mam.getQueryId());
190        final Data data = new Data();
191        data.setFormType(mam.version.namespace);
192        if (mam.muc()) {
193            packet.setTo(mam.getWith());
194        } else if (mam.getWith() != null) {
195            data.put("with", mam.getWith().toString());
196        }
197        final long start = mam.getStart();
198        final long end = mam.getEnd();
199        if (start != 0) {
200            data.put("start", getTimestamp(start));
201        }
202        if (end != 0) {
203            data.put("end", getTimestamp(end));
204        }
205        data.submit();
206        query.addChild(data);
207        Element set = query.addChild("set", "http://jabber.org/protocol/rsm");
208        if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
209            set.addChild("before").setContent(mam.getReference());
210        } else if (mam.getReference() != null) {
211            set.addChild("after").setContent(mam.getReference());
212        }
213        set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE));
214        return packet;
215    }
216
217    public Iq moderateMessage(Account account, Message m, String reason) {
218        final var packet = new Iq(Iq.Type.SET);
219        packet.setTo(m.getConversation().getJid().asBareJid());
220        packet.setFrom(account.getJid());
221        final var moderate =
222            packet.addChild("apply-to", "urn:xmpp:fasten:0")
223                  .setAttribute("id", m.getServerMsgId())
224                  .addChild("moderate", "urn:xmpp:message-moderate:0");
225        moderate.addChild("retract", "urn:xmpp:message-retract:0");
226        moderate.addChild("reason", "urn:xmpp:message-moderate:0").setContent(reason);
227        return packet;
228    }
229
230    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) {
231        return pushTokenToAppServer(appServer, token, deviceId, null);
232    }
233
234    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
235        final Iq packet = new Iq(Iq.Type.SET);
236        packet.setTo(appServer);
237        final Element command = packet.addChild("command", Namespace.COMMANDS);
238        command.setAttribute("node", "register-push-fcm");
239        command.setAttribute("action", "execute");
240        final Data data = new Data();
241        data.put("token", token);
242        data.put("android-id", deviceId);
243        if (muc != null) {
244            data.put("muc", muc.toString());
245        }
246        data.submit();
247        command.addChild(data);
248        return packet;
249    }
250
251    public Iq unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
252        final Iq packet = new Iq(Iq.Type.SET);
253        packet.setTo(appServer);
254        final Element command = packet.addChild("command", Namespace.COMMANDS);
255        command.setAttribute("node", "unregister-push-fcm");
256        command.setAttribute("action", "execute");
257        final Data data = new Data();
258        data.put("channel", channel);
259        data.put("android-id", deviceId);
260        data.submit();
261        command.addChild(data);
262        return packet;
263    }
264
265    public Iq enablePush(final Jid jid, final String node, final String secret) {
266        final Iq packet = new Iq(Iq.Type.SET);
267        Element enable = packet.addChild("enable", Namespace.PUSH);
268        enable.setAttribute("jid", jid);
269        enable.setAttribute("node", node);
270        if (secret != null) {
271            Data data = new Data();
272            data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
273            data.put("secret", secret);
274            data.submit();
275            enable.addChild(data);
276        }
277        return packet;
278    }
279
280    public Iq disablePush(final Jid jid, final String node) {
281        Iq packet = new Iq(Iq.Type.SET);
282        Element disable = packet.addChild("disable", Namespace.PUSH);
283        disable.setAttribute("jid", jid);
284        disable.setAttribute("node", node);
285        return packet;
286    }
287
288    public Iq requestPubsubConfiguration(Jid jid, String node) {
289        return pubsubConfiguration(jid, node, null);
290    }
291
292    public Iq publishPubsubConfiguration(Jid jid, String node, Data data) {
293        return pubsubConfiguration(jid, node, data);
294    }
295
296    private Iq pubsubConfiguration(Jid jid, String node, Data data) {
297        final Iq packet = new Iq(data == null ? Iq.Type.GET : Iq.Type.SET);
298        packet.setTo(jid);
299        Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
300        Element configure = pubsub.addChild("configure").setAttribute("node", node);
301        if (data != null) {
302            configure.addChild(data);
303        }
304        return packet;
305    }
306
307    public Iq bobResponse(Iq request) {
308        try {
309            final var bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid");
310            final var cid = BobTransfer.cid(bobCid);
311            final var f = mXmppConnectionService.getFileForCid(cid);
312            if (f == null || !f.canRead()) {
313                throw new IOException("No such file");
314            } else if (f.getSize() > 129000) {
315                final var response = request.generateResponse(Iq.Type.ERROR);
316                final var error = response.addChild("error");
317                error.setAttribute("type", "cancel");
318                error.addChild("policy-violation", "urn:ietf:params:xml:ns:xmpp-stanzas");
319                return response;
320            } else {
321                final var response = request.generateResponse(Iq.Type.RESULT);
322                final var data = response.addChild("data", "urn:xmpp:bob");
323                data.setAttribute("cid", bobCid);
324                data.setAttribute("type", f.getMimeType());
325                ByteArrayOutputStream b64 = new ByteArrayOutputStream((int) f.getSize() * 2);
326                Base64OutputStream b64wrap = new Base64OutputStream(b64, Base64.NO_WRAP);
327                ByteStreams.copy(new FileInputStream(f), b64wrap);
328                b64wrap.flush();
329                b64wrap.close();
330                data.setContent(b64.toString("utf-8"));
331                return response;
332            }
333        } catch (final IOException | IllegalStateException e) {
334            final var response = request.generateResponse(Iq.Type.ERROR);
335            final var error = response.addChild("error");
336            error.setAttribute("type", "cancel");
337            error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
338            return response;
339        }
340    }
341}