001/**
002 *
003 * Copyright 2018 Paul Schaub.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.ox_im;
018
019import java.io.IOException;
020import java.util.Collections;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.WeakHashMap;
026
027import org.jivesoftware.smack.Manager;
028import org.jivesoftware.smack.SmackException;
029import org.jivesoftware.smack.XMPPConnection;
030import org.jivesoftware.smack.XMPPException;
031import org.jivesoftware.smack.chat2.ChatManager;
032import org.jivesoftware.smack.packet.ExtensionElement;
033import org.jivesoftware.smack.packet.Message;
034import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
035import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement;
036import org.jivesoftware.smackx.hints.element.StoreHint;
037import org.jivesoftware.smackx.ox.OpenPgpContact;
038import org.jivesoftware.smackx.ox.OpenPgpManager;
039import org.jivesoftware.smackx.ox.OpenPgpMessage;
040import org.jivesoftware.smackx.ox.crypto.OpenPgpElementAndMetadata;
041import org.jivesoftware.smackx.ox.element.OpenPgpContentElement;
042import org.jivesoftware.smackx.ox.element.OpenPgpElement;
043import org.jivesoftware.smackx.ox.element.SigncryptElement;
044import org.jivesoftware.smackx.ox.listener.SigncryptElementReceivedListener;
045
046import org.bouncycastle.openpgp.PGPException;
047import org.jxmpp.jid.BareJid;
048import org.jxmpp.jid.Jid;
049import org.pgpainless.decryption_verification.OpenPgpMetadata;
050import org.pgpainless.key.OpenPgpV4Fingerprint;
051import org.xmlpull.v1.XmlPullParserException;
052
053/**
054 * Entry point of Smacks API for XEP-0374: OpenPGP for XMPP: Instant Messaging.
055 *
056 * <h2>Setup</h2>
057 *
058 * In order to set up OX Instant Messaging, please first follow the setup routines of the {@link OpenPgpManager}, then
059 * do the following steps:
060 *
061 * <h3>Acquire an {@link OXInstantMessagingManager} instance.</h3>
062 *
063 * <pre>
064 * {@code
065 * OXInstantMessagingManager instantManager = OXInstantMessagingManager.getInstanceFor(connection);
066 * }
067 * </pre>
068 *
069 * <h3>Listen for OX messages</h3>
070 * In order to listen for incoming OX:IM messages, you have to register a listener.
071 *
072 * <pre>
073 * {@code
074 * instantManager.addOxMessageListener(
075 *          new OxMessageListener() {
076 *              void newIncomingOxMessage(OpenPgpContact contact,
077 *                                        Message originalMessage,
078 *                                        SigncryptElement decryptedPayload) {
079 *                  Message.Body body = decryptedPayload.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE);
080 *                  ...
081 *              }
082 *          });
083 * }
084 * </pre>
085 *
086 * <h3>Finally, announce support for OX:IM</h3>
087 * In order to let your contacts know, that you support message encrypting using the OpenPGP for XMPP: Instant Messaging
088 * profile, you have to announce support for OX:IM.
089 *
090 * <pre>
091 * {@code
092 * instantManager.announceSupportForOxInstantMessaging();
093 * }
094 * </pre>
095 *
096 * <h2>Sending messages</h2>
097 * In order to send an OX:IM message, just do
098 *
099 * <pre>
100 * {@code
101 * instantManager.sendOxMessage(openPgpManager.getOpenPgpContact(contactsJid), "Hello World");
102 * }
103 * </pre>
104 *
105 * Note, that you have to decide, whether to trust the contacts keys prior to sending a message, otherwise undecided
106 * keys are not included in the encryption process. You can trust keys by calling
107 * {@link OpenPgpContact#trust(OpenPgpV4Fingerprint)}. Same goes for your own keys! In order to determine, whether
108 * there are undecided keys, call {@link OpenPgpContact#hasUndecidedKeys()}. The trust state of a single key can be
109 * determined using {@link OpenPgpContact#getTrust(OpenPgpV4Fingerprint)}.
110 *
111 * Note: This implementation does not yet have support for sending/receiving messages to/from MUCs.
112 *
113 * @see <a href="https://xmpp.org/extensions/xep-0374.html">
114 *     XEP-0374: OpenPGP for XMPP: Instant Messaging</a>
115 */
116public final class OXInstantMessagingManager extends Manager {
117
118    public static final String NAMESPACE_0 = "urn:xmpp:openpgp:im:0";
119
120    private static final Map<XMPPConnection, OXInstantMessagingManager> INSTANCES = new WeakHashMap<>();
121
122    private final Set<OxMessageListener> oxMessageListeners = new HashSet<>();
123    private final OpenPgpManager openPgpManager;
124
125    private OXInstantMessagingManager(final XMPPConnection connection) {
126        super(connection);
127        openPgpManager = OpenPgpManager.getInstanceFor(connection);
128        openPgpManager.registerSigncryptReceivedListener(signcryptElementReceivedListener);
129        announceSupportForOxInstantMessaging();
130    }
131
132    /**
133     * Return an instance of the {@link OXInstantMessagingManager} that belongs to the given {@code connection}.
134     *
135     * @param connection XMPP connection
136     * @return manager instance
137     */
138    public static OXInstantMessagingManager getInstanceFor(XMPPConnection connection) {
139        OXInstantMessagingManager manager = INSTANCES.get(connection);
140
141        if (manager == null) {
142            manager = new OXInstantMessagingManager(connection);
143            INSTANCES.put(connection, manager);
144        }
145
146        return manager;
147    }
148
149    /**
150     * Add the OX:IM namespace as a feature to our disco features.
151     */
152    public void announceSupportForOxInstantMessaging() {
153        ServiceDiscoveryManager.getInstanceFor(connection())
154                .addFeature(NAMESPACE_0);
155    }
156
157    /**
158     * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
159     *
160     * @param jid {@link BareJid} of the contact in question.
161     * @return true if contact announces support, otherwise false.
162     *
163     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error
164     * @throws SmackException.NotConnectedException if we are not connected
165     * @throws InterruptedException if the thread gets interrupted
166     * @throws SmackException.NoResponseException if the server doesn't respond
167     */
168    public boolean contactSupportsOxInstantMessaging(BareJid jid)
169            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
170            SmackException.NoResponseException {
171        return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, NAMESPACE_0);
172    }
173
174    /**
175     * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
176     *
177     * @param contact {@link OpenPgpContact} in question.
178     * @return true if contact announces support, otherwise false.
179     *
180     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error
181     * @throws SmackException.NotConnectedException if we are not connected
182     * @throws InterruptedException if the thread is interrupted
183     * @throws SmackException.NoResponseException if the server doesn't respond
184     */
185    public boolean contactSupportsOxInstantMessaging(OpenPgpContact contact)
186            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
187            SmackException.NoResponseException {
188        return contactSupportsOxInstantMessaging(contact.getJid());
189    }
190
191    /**
192     * Add an {@link OxMessageListener}. The listener gets notified about incoming {@link OpenPgpMessage}s which
193     * contained an OX-IM message.
194     *
195     * @param listener listener
196     * @return true if the listener gets added, otherwise false.
197     */
198    public boolean addOxMessageListener(OxMessageListener listener) {
199        return oxMessageListeners.add(listener);
200    }
201
202    /**
203     * Remove an {@link OxMessageListener}. The listener will no longer be notified about OX-IM messages.
204     *
205     * @param listener listener
206     * @return true, if the listener gets removed, otherwise false
207     */
208    public boolean removeOxMessageListener(OxMessageListener listener) {
209        return oxMessageListeners.remove(listener);
210    }
211
212    /**
213     * Send an OX message to a {@link OpenPgpContact}. The message will be encrypted to all active keys of the contact,
214     * as well as all of our active keys. The message is also signed with our key.
215     *
216     * @param contact contact capable of OpenPGP for XMPP: Instant Messaging.
217     * @param body message body.
218     *
219     * @return {@link OpenPgpMetadata} about the messages encryption + signatures.
220     *
221     * @throws InterruptedException if the thread is interrupted
222     * @throws IOException IO is dangerous
223     * @throws SmackException.NotConnectedException if we are not connected
224     * @throws SmackException.NotLoggedInException if we are not logged in
225     * @throws PGPException PGP is brittle
226     */
227    public OpenPgpMetadata sendOxMessage(OpenPgpContact contact, CharSequence body)
228            throws InterruptedException, IOException,
229            SmackException.NotConnectedException, SmackException.NotLoggedInException, PGPException {
230        Message message = new Message(contact.getJid());
231        Message.Body mBody = new Message.Body(null, body.toString());
232
233        OpenPgpMetadata metadata = addOxMessage(message, contact, Collections.<ExtensionElement>singletonList(mBody));
234
235        ChatManager.getInstanceFor(connection()).chatWith(contact.getJid().asEntityBareJidIfPossible()).send(message);
236
237        return metadata;
238    }
239
240    /**
241     * Add an OX-IM message element to a message.
242     *
243     * @param message message
244     * @param contact recipient of the message
245     * @param payload payload which will be encrypted and signed
246     *
247     * @return {@link OpenPgpMetadata} about the messages encryption + metadata.
248     *
249     * @throws SmackException.NotLoggedInException in case we are not logged in
250     * @throws PGPException in case something goes wrong during encryption
251     * @throws IOException IO is dangerous (we need to read keys)
252     */
253    public OpenPgpMetadata addOxMessage(Message message, OpenPgpContact contact, List<ExtensionElement> payload)
254            throws SmackException.NotLoggedInException, PGPException, IOException {
255        return addOxMessage(message, Collections.singleton(contact), payload);
256    }
257
258    /**
259     * Add an OX-IM message element to a message.
260     *
261     * @param message message
262     * @param contacts recipients of the message
263     * @param payload payload which will be encrypted and signed
264     *
265     * @return metadata about the messages encryption + signatures.
266     *
267     * @throws SmackException.NotLoggedInException in case we are not logged in
268     * @throws PGPException in case something goes wrong during encryption
269     * @throws IOException IO is dangerous (we need to read keys)
270     */
271    public OpenPgpMetadata addOxMessage(Message message, Set<OpenPgpContact> contacts, List<ExtensionElement> payload)
272            throws SmackException.NotLoggedInException, IOException, PGPException {
273
274        HashSet<OpenPgpContact> recipients = new HashSet<>(contacts);
275        OpenPgpContact self = openPgpManager.getOpenPgpSelf();
276        recipients.add(self);
277
278        OpenPgpElementAndMetadata openPgpElementAndMetadata = signAndEncrypt(recipients, payload);
279        message.addExtension(openPgpElementAndMetadata.getElement());
280
281        // Set hints on message
282        ExplicitMessageEncryptionElement.set(message,
283                ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0);
284        StoreHint.set(message);
285        setOXBodyHint(message);
286
287        return openPgpElementAndMetadata.getMetadata();
288    }
289
290    /**
291     * Wrap some {@code payload} into a {@link SigncryptElement}, sign and encrypt it for {@code contacts} and ourselves.
292     *
293     * @param contacts recipients of the message
294     * @param payload payload which will be encrypted and signed
295     *
296     * @return encrypted and signed {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about the
297     * encryption + signatures.
298     *
299     * @throws SmackException.NotLoggedInException in case we are not logged in
300     * @throws IOException IO is dangerous (we need to read keys)
301     * @throws PGPException in case encryption goes wrong
302     */
303    public OpenPgpElementAndMetadata signAndEncrypt(Set<OpenPgpContact> contacts, List<ExtensionElement> payload)
304            throws SmackException.NotLoggedInException, IOException, PGPException {
305
306        Set<Jid> jids = new HashSet<>();
307        for (OpenPgpContact contact : contacts) {
308            jids.add(contact.getJid());
309        }
310        jids.add(openPgpManager.getOpenPgpSelf().getJid());
311
312        SigncryptElement signcryptElement = new SigncryptElement(jids, payload);
313        OpenPgpElementAndMetadata encrypted = openPgpManager.getOpenPgpProvider().signAndEncrypt(signcryptElement,
314                openPgpManager.getOpenPgpSelf(), contacts);
315
316        return encrypted;
317    }
318
319    /**
320     * Manually decrypt and verify an {@link OpenPgpElement}.
321     *
322     * @param element encrypted, signed {@link OpenPgpElement}.
323     * @param sender sender of the message.
324     *
325     * @return decrypted, verified message
326     *
327     * @throws SmackException.NotLoggedInException In case we are not logged in (we need our jid to access our keys)
328     * @throws PGPException in case of an PGP error
329     * @throws IOException in case of an IO error (reading keys, streams etc)
330     * @throws XmlPullParserException in case that the content of the {@link OpenPgpElement} is not a valid
331     * {@link OpenPgpContentElement} or broken XML.
332     * @throws IllegalArgumentException if the elements content is not a {@link SigncryptElement}. This happens, if the
333     * element likely is not an OX message.
334     */
335    public OpenPgpMessage decryptAndVerify(OpenPgpElement element, OpenPgpContact sender)
336            throws SmackException.NotLoggedInException, PGPException, IOException, XmlPullParserException {
337
338        OpenPgpMessage decrypted = openPgpManager.decryptOpenPgpElement(element, sender);
339        if (decrypted.getState() != OpenPgpMessage.State.signcrypt) {
340            throw new IllegalArgumentException("Decrypted message does appear to not be an OX message. (State: " + decrypted.getState() + ")");
341        }
342
343        return decrypted;
344    }
345
346    /**
347     * Set a hint about the message being OX-IM encrypted as body of the message.
348     *
349     * @param message message
350     */
351    private static void setOXBodyHint(Message message) {
352        message.setBody("This message is encrypted using XEP-0374: OpenPGP for XMPP: Instant Messaging.");
353    }
354
355    private final SigncryptElementReceivedListener signcryptElementReceivedListener = new SigncryptElementReceivedListener() {
356        @Override
357        public void signcryptElementReceived(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata) {
358            for (OxMessageListener listener : oxMessageListeners) {
359                listener.newIncomingOxMessage(contact, originalMessage, signcryptElement, metadata);
360            }
361        }
362    };
363}