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