IqGenerator.java

  1package eu.siacs.conversations.generator;
  2
  3import android.os.Bundle;
  4import android.util.Base64;
  5import android.util.Log;
  6import eu.siacs.conversations.Config;
  7import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  8import eu.siacs.conversations.entities.Account;
  9import eu.siacs.conversations.entities.Conversation;
 10import eu.siacs.conversations.entities.DownloadableFile;
 11import eu.siacs.conversations.services.MessageArchiveService;
 12import eu.siacs.conversations.services.XmppConnectionService;
 13import eu.siacs.conversations.xml.Element;
 14import eu.siacs.conversations.xml.Namespace;
 15import eu.siacs.conversations.xmpp.Jid;
 16import eu.siacs.conversations.xmpp.forms.Data;
 17import eu.siacs.conversations.xmpp.pep.Avatar;
 18import im.conversations.android.xmpp.model.stanza.Iq;
 19import im.conversations.android.xmpp.model.upload.Request;
 20import java.nio.ByteBuffer;
 21import java.security.cert.CertificateEncodingException;
 22import java.security.cert.X509Certificate;
 23import java.util.ArrayList;
 24import java.util.List;
 25import java.util.Set;
 26import java.util.UUID;
 27import org.whispersystems.libsignal.IdentityKey;
 28import org.whispersystems.libsignal.ecc.ECPublicKey;
 29import org.whispersystems.libsignal.state.PreKeyRecord;
 30import org.whispersystems.libsignal.state.SignedPreKeyRecord;
 31
 32public class IqGenerator extends AbstractGenerator {
 33
 34    public IqGenerator(final XmppConnectionService service) {
 35        super(service);
 36    }
 37
 38    public static Iq purgeOfflineMessages() {
 39        final Iq packet = new Iq(Iq.Type.SET);
 40        packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
 41        return packet;
 42    }
 43
 44    protected Iq publish(final String node, final Element item, final Bundle options) {
 45        final var packet = new Iq(Iq.Type.SET);
 46        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
 47        final Element publish = pubsub.addChild("publish");
 48        publish.setAttribute("node", node);
 49        publish.addChild(item);
 50        if (options != null) {
 51            final Element publishOptions = pubsub.addChild("publish-options");
 52            publishOptions.addChild(Data.create(Namespace.PUBSUB_PUBLISH_OPTIONS, options));
 53        }
 54        return packet;
 55    }
 56
 57    protected Iq publish(final String node, final Element item) {
 58        return publish(node, item, null);
 59    }
 60
 61    private Iq retrieve(String node, Element item) {
 62        final var packet = new Iq(Iq.Type.GET);
 63        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
 64        final Element items = pubsub.addChild("items");
 65        items.setAttribute("node", node);
 66        if (item != null) {
 67            items.addChild(item);
 68        }
 69        return packet;
 70    }
 71
 72    public Iq deleteNode(final String node) {
 73        final var packet = new Iq(Iq.Type.SET);
 74        final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER);
 75        pubsub.addChild("delete").setAttribute("node", node);
 76        return packet;
 77    }
 78
 79    public Iq retrievePepAvatar(final Avatar avatar) {
 80        final Element item = new Element("item");
 81        item.setAttribute("id", avatar.sha1sum);
 82        final var packet = retrieve(Namespace.AVATAR_DATA, item);
 83        packet.setTo(avatar.owner);
 84        return packet;
 85    }
 86
 87    public Iq retrieveVcardAvatar(final Avatar avatar) {
 88        final Iq packet = new Iq(Iq.Type.GET);
 89        packet.setTo(avatar.owner);
 90        packet.addChild("vCard", "vcard-temp");
 91        return packet;
 92    }
 93
 94    public Iq retrieveVcardAvatar(final Jid to) {
 95        final Iq packet = new Iq(Iq.Type.GET);
 96        packet.setTo(to);
 97        packet.addChild("vCard", "vcard-temp");
 98        return packet;
 99    }
100
101    public Iq retrieveAvatarMetaData(final Jid to) {
102        final Iq packet = retrieve("urn:xmpp:avatar:metadata", null);
103        if (to != null) {
104            packet.setTo(to);
105        }
106        return packet;
107    }
108
109    public Iq retrieveDeviceIds(final Jid to) {
110        final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
111        if (to != null) {
112            packet.setTo(to);
113        }
114        return packet;
115    }
116
117    public Iq retrieveBundlesForDevice(final Jid to, final int deviceid) {
118        final var packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null);
119        packet.setTo(to);
120        return packet;
121    }
122
123    public Iq retrieveVerificationForDevice(final Jid to, final int deviceid) {
124        final var packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null);
125        packet.setTo(to);
126        return packet;
127    }
128
129    public Iq publishDeviceIds(final Set<Integer> ids, final Bundle publishOptions) {
130        final Element item = new Element("item");
131        item.setAttribute("id", "current");
132        final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
133        for (Integer id : ids) {
134            final Element device = new Element("device");
135            device.setAttribute("id", id);
136            list.addChild(device);
137        }
138        return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions);
139    }
140
141    public Iq publishBundles(
142            final SignedPreKeyRecord signedPreKeyRecord,
143            final IdentityKey identityKey,
144            final Set<PreKeyRecord> preKeyRecords,
145            final int deviceId,
146            Bundle publishOptions) {
147        final Element item = new Element("item");
148        item.setAttribute("id", "current");
149        final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
150        final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
151        signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
152        ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
153        signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.NO_WRAP));
154        final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
155        signedPreKeySignature.setContent(
156                Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.NO_WRAP));
157        final Element identityKeyElement = bundle.addChild("identityKey");
158        identityKeyElement.setContent(
159                Base64.encodeToString(identityKey.serialize(), Base64.NO_WRAP));
160
161        final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
162        for (PreKeyRecord preKeyRecord : preKeyRecords) {
163            final Element prekey = prekeys.addChild("preKeyPublic");
164            prekey.setAttribute("preKeyId", preKeyRecord.getId());
165            prekey.setContent(
166                    Base64.encodeToString(
167                            preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.NO_WRAP));
168        }
169
170        return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions);
171    }
172
173    public Iq publishVerification(
174            byte[] signature, X509Certificate[] certificates, final int deviceId) {
175        final Element item = new Element("item");
176        item.setAttribute("id", "current");
177        final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX);
178        final Element chain = verification.addChild("chain");
179        for (int i = 0; i < certificates.length; ++i) {
180            try {
181                Element certificate = chain.addChild("certificate");
182                certificate.setContent(
183                        Base64.encodeToString(certificates[i].getEncoded(), Base64.NO_WRAP));
184                certificate.setAttribute("index", i);
185            } catch (CertificateEncodingException e) {
186                Log.d(Config.LOGTAG, "could not encode certificate");
187            }
188        }
189        verification
190                .addChild("signature")
191                .setContent(Base64.encodeToString(signature, Base64.NO_WRAP));
192        return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item);
193    }
194
195    public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
196        final Iq packet = new Iq(Iq.Type.SET);
197        final Element query = packet.query(mam.version.namespace);
198        query.setAttribute("queryid", mam.getQueryId());
199        final Data data = new Data();
200        data.setFormType(mam.version.namespace);
201        if (mam.muc()) {
202            packet.setTo(mam.getWith());
203        } else if (mam.getWith() != null) {
204            data.put("with", mam.getWith().toString());
205        }
206        final long start = mam.getStart();
207        final long end = mam.getEnd();
208        if (start != 0) {
209            data.put("start", getTimestamp(start));
210        }
211        if (end != 0) {
212            data.put("end", getTimestamp(end));
213        }
214        data.submit();
215        query.addChild(data);
216        Element set = query.addChild("set", "http://jabber.org/protocol/rsm");
217        if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
218            set.addChild("before").setContent(mam.getReference());
219        } else if (mam.getReference() != null) {
220            set.addChild("after").setContent(mam.getReference());
221        }
222        set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE));
223        return packet;
224    }
225
226    public Iq generateSetPassword(final Account account, final String newPassword) {
227        final Iq packet = new Iq(Iq.Type.SET);
228        packet.setTo(account.getDomain());
229        final Element query = packet.addChild("query", Namespace.REGISTER);
230        final Jid jid = account.getJid();
231        query.addChild("username").setContent(jid.getLocal());
232        query.addChild("password").setContent(newPassword);
233        return packet;
234    }
235
236    public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) {
237        List<Jid> jids = new ArrayList<>();
238        jids.add(jid);
239        return changeAffiliation(conference, jids, affiliation);
240    }
241
242    public Iq changeAffiliation(Conversation conference, List<Jid> jids, String affiliation) {
243        final Iq packet = new Iq(Iq.Type.SET);
244        packet.setTo(conference.getJid().asBareJid());
245        packet.setFrom(conference.getAccount().getJid());
246        Element query = packet.query("http://jabber.org/protocol/muc#admin");
247        for (Jid jid : jids) {
248            Element item = query.addChild("item");
249            item.setAttribute("jid", jid);
250            item.setAttribute("affiliation", affiliation);
251        }
252        return packet;
253    }
254
255    public Iq changeRole(Conversation conference, String nick, String role) {
256        final Iq packet = new Iq(Iq.Type.SET);
257        packet.setTo(conference.getJid().asBareJid());
258        packet.setFrom(conference.getAccount().getJid());
259        Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item");
260        item.setAttribute("nick", nick);
261        item.setAttribute("role", role);
262        return packet;
263    }
264
265    public Iq requestHttpUploadSlot(
266            final Jid host, final DownloadableFile file, final String mime) {
267        final Iq packet = new Iq(Iq.Type.GET);
268        packet.setTo(host);
269        final var request = packet.addExtension(new Request());
270        request.setFilename(convertFilename(file.getName()));
271        request.setSize(file.getExpectedSize());
272        return packet;
273    }
274
275    private static String convertFilename(String name) {
276        int pos = name.indexOf('.');
277        if (pos != -1) {
278            try {
279                UUID uuid = UUID.fromString(name.substring(0, pos));
280                ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
281                bb.putLong(uuid.getMostSignificantBits());
282                bb.putLong(uuid.getLeastSignificantBits());
283                return Base64.encodeToString(
284                                bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP)
285                        + name.substring(pos);
286            } catch (Exception e) {
287                return name;
288            }
289        } else {
290            return name;
291        }
292    }
293
294    public static Iq generateCreateAccountWithCaptcha(
295            final Account account, final String id, final Data data) {
296        final Iq register = new Iq(Iq.Type.SET);
297        register.setFrom(account.getJid().asBareJid());
298        register.setTo(account.getDomain());
299        register.setId(id);
300        Element query = register.query(Namespace.REGISTER);
301        if (data != null) {
302            query.addChild(data);
303        }
304        return register;
305    }
306
307    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) {
308        return pushTokenToAppServer(appServer, token, deviceId, null);
309    }
310
311    public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
312        final Iq packet = new Iq(Iq.Type.SET);
313        packet.setTo(appServer);
314        final Element command = packet.addChild("command", Namespace.COMMANDS);
315        command.setAttribute("node", "register-push-fcm");
316        command.setAttribute("action", "execute");
317        final Data data = new Data();
318        data.put("token", token);
319        data.put("android-id", deviceId);
320        if (muc != null) {
321            data.put("muc", muc.toString());
322        }
323        data.submit();
324        command.addChild(data);
325        return packet;
326    }
327
328    public Iq unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
329        final Iq packet = new Iq(Iq.Type.SET);
330        packet.setTo(appServer);
331        final Element command = packet.addChild("command", Namespace.COMMANDS);
332        command.setAttribute("node", "unregister-push-fcm");
333        command.setAttribute("action", "execute");
334        final Data data = new Data();
335        data.put("channel", channel);
336        data.put("android-id", deviceId);
337        data.submit();
338        command.addChild(data);
339        return packet;
340    }
341
342    public Iq enablePush(final Jid jid, final String node, final String secret) {
343        final Iq packet = new Iq(Iq.Type.SET);
344        Element enable = packet.addChild("enable", Namespace.PUSH);
345        enable.setAttribute("jid", jid);
346        enable.setAttribute("node", node);
347        if (secret != null) {
348            Data data = new Data();
349            data.setFormType(Namespace.PUBSUB_PUBLISH_OPTIONS);
350            data.put("secret", secret);
351            data.submit();
352            enable.addChild(data);
353        }
354        return packet;
355    }
356
357    public Iq disablePush(final Jid jid, final String node) {
358        Iq packet = new Iq(Iq.Type.SET);
359        Element disable = packet.addChild("disable", Namespace.PUSH);
360        disable.setAttribute("jid", jid);
361        disable.setAttribute("node", node);
362        return packet;
363    }
364
365    public Iq queryAffiliation(Conversation conversation, String affiliation) {
366        final Iq packet = new Iq(Iq.Type.GET);
367        packet.setTo(conversation.getJid().asBareJid());
368        packet.query("http://jabber.org/protocol/muc#admin")
369                .addChild("item")
370                .setAttribute("affiliation", affiliation);
371        return packet;
372    }
373
374    public static Bundle defaultGroupChatConfiguration() {
375        Bundle options = new Bundle();
376        options.putString("muc#roomconfig_persistentroom", "1");
377        options.putString("muc#roomconfig_membersonly", "1");
378        options.putString("muc#roomconfig_publicroom", "0");
379        options.putString("muc#roomconfig_whois", "anyone");
380        options.putString("muc#roomconfig_changesubject", "0");
381        options.putString("muc#roomconfig_allowinvites", "0");
382        options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
383        options.putString("mam", "1"); // ejabberd community
384        options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
385        return options;
386    }
387
388    public static Bundle defaultChannelConfiguration() {
389        Bundle options = new Bundle();
390        options.putString("muc#roomconfig_persistentroom", "1");
391        options.putString("muc#roomconfig_membersonly", "0");
392        options.putString("muc#roomconfig_publicroom", "1");
393        options.putString("muc#roomconfig_whois", "moderators");
394        options.putString("muc#roomconfig_changesubject", "0");
395        options.putString("muc#roomconfig_enablearchiving", "1"); // prosody
396        options.putString("mam", "1"); // ejabberd community
397        options.putString("muc#roomconfig_mam", "1"); // ejabberd saas
398        return options;
399    }
400
401    public Iq requestPubsubConfiguration(Jid jid, String node) {
402        return pubsubConfiguration(jid, node, null);
403    }
404
405    public Iq publishPubsubConfiguration(Jid jid, String node, Data data) {
406        return pubsubConfiguration(jid, node, data);
407    }
408
409    private Iq pubsubConfiguration(Jid jid, String node, Data data) {
410        final Iq packet = new Iq(data == null ? Iq.Type.GET : Iq.Type.SET);
411        packet.setTo(jid);
412        Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
413        Element configure = pubsub.addChild("configure").setAttribute("node", node);
414        if (data != null) {
415            configure.addChild(data);
416        }
417        return packet;
418    }
419}