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}