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.OpenPgpElement;
042import org.jivesoftware.smackx.ox.element.SigncryptElement;
043import org.jivesoftware.smackx.ox.listener.SigncryptElementReceivedListener;
044
045import org.bouncycastle.openpgp.PGPException;
046import org.jxmpp.jid.BareJid;
047import org.jxmpp.jid.Jid;
048import org.pgpainless.decryption_verification.OpenPgpMetadata;
049import org.pgpainless.key.OpenPgpV4Fingerprint;
050
051/**
052 * Entry point of Smacks API for XEP-0374: OpenPGP for XMPP: Instant Messaging.
053 *
054 * <h2>Setup</h2>
055 *
056 * In order to set up OX Instant Messaging, please first follow the setup routines of the {@link OpenPgpManager}, then
057 * do the following steps:
058 *
059 * <h3>Acquire an {@link OXInstantMessagingManager} instance.</h3>
060 *
061 * <pre>
062 * {@code
063 * OXInstantMessagingManager instantManager = OXInstantMessagingManager.getInstanceFor(connection);
064 * }
065 * </pre>
066 *
067 * <h3>Listen for OX messages</h3>
068 * In order to listen for incoming OX:IM messages, you have to register a listener.
069 *
070 * <pre>
071 * {@code
072 * instantManager.addOxMessageListener(
073 *          new OxMessageListener() {
074 *              void newIncomingOxMessage(OpenPgpContact contact,
075 *                                        Message originalMessage,
076 *                                        SigncryptElement decryptedPayload) {
077 *                  Message.Body body = decryptedPayload.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE);
078 *                  ...
079 *              }
080 *          });
081 * }
082 * </pre>
083 *
084 * <h3>Finally, announce support for OX:IM</h3>
085 * In order to let your contacts know, that you support message encrypting using the OpenPGP for XMPP: Instant Messaging
086 * profile, you have to announce support for OX:IM.
087 *
088 * <pre>
089 * {@code
090 * instantManager.announceSupportForOxInstantMessaging();
091 * }
092 * </pre>
093 *
094 * <h2>Sending messages</h2>
095 * In order to send an OX:IM message, just do
096 *
097 * <pre>
098 * {@code
099 * instantManager.sendOxMessage(openPgpManager.getOpenPgpContact(contactsJid), "Hello World");
100 * }
101 * </pre>
102 *
103 * Note, that you have to decide, whether to trust the contacts keys prior to sending a message, otherwise undecided
104 * keys are not included in the encryption process. You can trust keys by calling
105 * {@link OpenPgpContact#trust(OpenPgpV4Fingerprint)}. Same goes for your own keys! In order to determine, whether
106 * there are undecided keys, call {@link OpenPgpContact#hasUndecidedKeys()}. The trust state of a single key can be
107 * determined using {@link OpenPgpContact#getTrust(OpenPgpV4Fingerprint)}.
108 *
109 * Note: This implementation does not yet have support for sending/receiving messages to/from MUCs.
110 *
111 * @see <a href="https://xmpp.org/extensions/xep-0374.html">
112 *     XEP-0374: OpenPGP for XMPP: Instant Messaging</a>
113 */
114public final class OXInstantMessagingManager extends Manager {
115
116    public static final String NAMESPACE_0 = "urn:xmpp:openpgp:im:0";
117
118    private static final Map<XMPPConnection, OXInstantMessagingManager> INSTANCES = new WeakHashMap<>();
119
120    private final Set<OxMessageListener> oxMessageListeners = new HashSet<>();
121    private final OpenPgpManager openPgpManager;
122
123    private OXInstantMessagingManager(final XMPPConnection connection) {
124        super(connection);
125        openPgpManager = OpenPgpManager.getInstanceFor(connection);
126        openPgpManager.registerSigncryptReceivedListener(signcryptElementReceivedListener);
127        announceSupportForOxInstantMessaging();
128    }
129
130    /**
131     * Return an instance of the {@link OXInstantMessagingManager} that belongs to the given {@code connection}.
132     *
133     * @param connection XMPP connection
134     * @return manager instance
135     */
136    public static OXInstantMessagingManager getInstanceFor(XMPPConnection connection) {
137        OXInstantMessagingManager manager = INSTANCES.get(connection);
138
139        if (manager == null) {
140            manager = new OXInstantMessagingManager(connection);
141            INSTANCES.put(connection, manager);
142        }
143
144        return manager;
145    }
146
147    /**
148     * Add the OX:IM namespace as a feature to our disco features.
149     */
150    public void announceSupportForOxInstantMessaging() {
151        ServiceDiscoveryManager.getInstanceFor(connection())
152                .addFeature(NAMESPACE_0);
153    }
154
155    /**
156     * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
157     *
158     * @param jid {@link BareJid} of the contact in question.
159     * @return true if contact announces support, otherwise false.
160     *
161     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error
162     * @throws SmackException.NotConnectedException if we are not connected
163     * @throws InterruptedException if the thread gets interrupted
164     * @throws SmackException.NoResponseException if the server doesn't respond
165     */
166    public boolean contactSupportsOxInstantMessaging(BareJid jid)
167            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
168            SmackException.NoResponseException {
169        return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, NAMESPACE_0);
170    }
171
172    /**
173     * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
174     *
175     * @param contact {@link OpenPgpContact} in question.
176     * @return true if contact announces support, otherwise false.
177     *
178     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error
179     * @throws SmackException.NotConnectedException if we are not connected
180     * @throws InterruptedException if the thread is interrupted
181     * @throws SmackException.NoResponseException if the server doesn't respond
182     */
183    public boolean contactSupportsOxInstantMessaging(OpenPgpContact contact)
184            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
185            SmackException.NoResponseException {
186        return contactSupportsOxInstantMessaging(contact.getJid());
187    }
188
189    /**
190     * Add an {@link OxMessageListener}. The listener gets notified about incoming {@link OpenPgpMessage}s which
191     * contained an OX-IM message.
192     *
193     * @param listener listener
194     * @return true if the listener gets added, otherwise false.
195     */
196    public boolean addOxMessageListener(OxMessageListener listener) {
197        return oxMessageListeners.add(listener);
198    }
199
200    /**
201     * Remove an {@link OxMessageListener}. The listener will no longer be notified about OX-IM messages.
202     *
203     * @param listener listener
204     * @return true, if the listener gets removed, otherwise false
205     */
206    public boolean removeOxMessageListener(OxMessageListener listener) {
207        return oxMessageListeners.remove(listener);
208    }
209
210    /**
211     * Send an OX message to a {@link OpenPgpContact}. The message will be encrypted to all active keys of the contact,
212     * as well as all of our active keys. The message is also signed with our key.
213     *
214     * @param contact contact capable of OpenPGP for XMPP: Instant Messaging.
215     * @param body message body.
216     *
217     * @return {@link OpenPgpMetadata} about the messages encryption + signatures.
218     *
219     * @throws InterruptedException if the thread is interrupted
220     * @throws IOException IO is dangerous
221     * @throws SmackException.NotConnectedException if we are not connected
222     * @throws SmackException.NotLoggedInException if we are not logged in
223     * @throws PGPException PGP is brittle
224     */
225    public OpenPgpMetadata sendOxMessage(OpenPgpContact contact, CharSequence body)
226            throws InterruptedException, IOException,
227            SmackException.NotConnectedException, SmackException.NotLoggedInException, PGPException {
228        Message message = new Message(contact.getJid());
229        Message.Body mBody = new Message.Body(null, body.toString());
230
231        OpenPgpMetadata metadata = addOxMessage(message, contact, Collections.<ExtensionElement>singletonList(mBody));
232
233        ChatManager.getInstanceFor(connection()).chatWith(contact.getJid().asEntityBareJidIfPossible()).send(message);
234
235        return metadata;
236    }
237
238    /**
239     * Add an OX-IM message element to a message.
240     *
241     * @param message message
242     * @param contact recipient of the message
243     * @param payload payload which will be encrypted and signed
244     *
245     * @return {@link OpenPgpMetadata} about the messages encryption + metadata.
246     *
247     * @throws SmackException.NotLoggedInException in case we are not logged in
248     * @throws PGPException in case something goes wrong during encryption
249     * @throws IOException IO is dangerous (we need to read keys)
250     */
251    public OpenPgpMetadata addOxMessage(Message message, OpenPgpContact contact, List<ExtensionElement> payload)
252            throws SmackException.NotLoggedInException, PGPException, IOException {
253        return addOxMessage(message, Collections.singleton(contact), payload);
254    }
255
256    /**
257     * Add an OX-IM message element to a message.
258     *
259     * @param message message
260     * @param contacts recipients of the message
261     * @param payload payload which will be encrypted and signed
262     *
263     * @return metadata about the messages encryption + signatures.
264     *
265     * @throws SmackException.NotLoggedInException in case we are not logged in
266     * @throws PGPException in case something goes wrong during encryption
267     * @throws IOException IO is dangerous (we need to read keys)
268     */
269    public OpenPgpMetadata addOxMessage(Message message, Set<OpenPgpContact> contacts, List<ExtensionElement> payload)
270            throws SmackException.NotLoggedInException, IOException, PGPException {
271
272        HashSet<OpenPgpContact> recipients = new HashSet<>(contacts);
273        OpenPgpContact self = openPgpManager.getOpenPgpSelf();
274        recipients.add(self);
275
276        OpenPgpElementAndMetadata openPgpElementAndMetadata = signAndEncrypt(recipients, payload);
277        message.addExtension(openPgpElementAndMetadata.getElement());
278
279        // Set hints on message
280        ExplicitMessageEncryptionElement.set(message,
281                ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0);
282        StoreHint.set(message);
283        setOXBodyHint(message);
284
285        return openPgpElementAndMetadata.getMetadata();
286    }
287
288    /**
289     * Wrap some {@code payload} into a {@link SigncryptElement}, sign and encrypt it for {@code contacts} and ourselves.
290     *
291     * @param contacts recipients of the message
292     * @param payload payload which will be encrypted and signed
293     *
294     * @return encrypted and signed {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about the
295     * encryption + signatures.
296     *
297     * @throws SmackException.NotLoggedInException in case we are not logged in
298     * @throws IOException IO is dangerous (we need to read keys)
299     * @throws PGPException in case encryption goes wrong
300     */
301    public OpenPgpElementAndMetadata signAndEncrypt(Set<OpenPgpContact> contacts, List<ExtensionElement> payload)
302            throws SmackException.NotLoggedInException, IOException, PGPException {
303
304        Set<Jid> jids = new HashSet<>();
305        for (OpenPgpContact contact : contacts) {
306            jids.add(contact.getJid());
307        }
308        jids.add(openPgpManager.getOpenPgpSelf().getJid());
309
310        SigncryptElement signcryptElement = new SigncryptElement(jids, payload);
311        OpenPgpElementAndMetadata encrypted = openPgpManager.getOpenPgpProvider().signAndEncrypt(signcryptElement,
312                openPgpManager.getOpenPgpSelf(), contacts);
313
314        return encrypted;
315    }
316
317    /**
318     * Set a hint about the message being OX-IM encrypted as body of the message.
319     *
320     * @param message message
321     */
322    private static void setOXBodyHint(Message message) {
323        message.setBody("This message is encrypted using XEP-0374: OpenPGP for XMPP: Instant Messaging.");
324    }
325
326    private final SigncryptElementReceivedListener signcryptElementReceivedListener = new SigncryptElementReceivedListener() {
327        @Override
328        public void signcryptElementReceived(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata) {
329            for (OxMessageListener listener : oxMessageListeners) {
330                listener.newIncomingOxMessage(contact, originalMessage, signcryptElement, metadata);
331            }
332        }
333    };
334}