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