OXInstantMessagingManager.java

  1. /**
  2.  *
  3.  * Copyright 2018 Paul Schaub.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.ox_im;

  18. import java.io.IOException;
  19. import java.util.Collections;
  20. import java.util.HashSet;
  21. import java.util.List;
  22. import java.util.Map;
  23. import java.util.Set;
  24. import java.util.WeakHashMap;

  25. import org.jivesoftware.smack.Manager;
  26. import org.jivesoftware.smack.SmackException;
  27. import org.jivesoftware.smack.XMPPConnection;
  28. import org.jivesoftware.smack.XMPPException;
  29. import org.jivesoftware.smack.chat2.ChatManager;
  30. import org.jivesoftware.smack.packet.ExtensionElement;
  31. import org.jivesoftware.smack.packet.Message;
  32. import org.jivesoftware.smack.packet.MessageBuilder;
  33. import org.jivesoftware.smack.xml.XmlPullParserException;

  34. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  35. import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement;
  36. import org.jivesoftware.smackx.hints.element.StoreHint;
  37. import org.jivesoftware.smackx.ox.OpenPgpContact;
  38. import org.jivesoftware.smackx.ox.OpenPgpManager;
  39. import org.jivesoftware.smackx.ox.OpenPgpMessage;
  40. import org.jivesoftware.smackx.ox.crypto.OpenPgpElementAndMetadata;
  41. import org.jivesoftware.smackx.ox.element.OpenPgpContentElement;
  42. import org.jivesoftware.smackx.ox.element.OpenPgpElement;
  43. import org.jivesoftware.smackx.ox.element.SigncryptElement;

  44. import org.bouncycastle.openpgp.PGPException;
  45. import org.jxmpp.jid.BareJid;
  46. import org.jxmpp.jid.Jid;
  47. import org.pgpainless.decryption_verification.OpenPgpMetadata;
  48. import org.pgpainless.encryption_signing.EncryptionResult;
  49. import org.pgpainless.key.OpenPgpV4Fingerprint;

  50. /**
  51.  * Entry point of Smacks API for XEP-0374: OpenPGP for XMPP: Instant Messaging.
  52.  *
  53.  * <h2>Setup</h2>
  54.  *
  55.  * In order to set up OX Instant Messaging, please first follow the setup routines of the {@link OpenPgpManager}, then
  56.  * do the following steps:
  57.  *
  58.  * <h3>Acquire an {@link OXInstantMessagingManager} instance.</h3>
  59.  *
  60.  * <pre>
  61.  * {@code
  62.  * OXInstantMessagingManager instantManager = OXInstantMessagingManager.getInstanceFor(connection);
  63.  * }
  64.  * </pre>
  65.  *
  66.  * <h3>Listen for OX messages</h3>
  67.  * In order to listen for incoming OX:IM messages, you have to register a listener.
  68.  *
  69.  * <pre>
  70.  * {@code
  71.  * instantManager.addOxMessageListener(
  72.  *          new OxMessageListener() {
  73.  *              void newIncomingOxMessage(OpenPgpContact contact,
  74.  *                                        Message originalMessage,
  75.  *                                        SigncryptElement decryptedPayload) {
  76.  *                  Message.Body body = decryptedPayload.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE);
  77.  *                  ...
  78.  *              }
  79.  *          });
  80.  * }
  81.  * </pre>
  82.  *
  83.  * <h3>Finally, announce support for OX:IM</h3>
  84.  * In order to let your contacts know, that you support message encrypting using the OpenPGP for XMPP: Instant Messaging
  85.  * profile, you have to announce support for OX:IM.
  86.  *
  87.  * <pre>
  88.  * {@code
  89.  * instantManager.announceSupportForOxInstantMessaging();
  90.  * }
  91.  * </pre>
  92.  *
  93.  * <h2>Sending messages</h2>
  94.  * In order to send an OX:IM message, just do
  95.  *
  96.  * <pre>
  97.  * {@code
  98.  * instantManager.sendOxMessage(openPgpManager.getOpenPgpContact(contactsJid), "Hello World");
  99.  * }
  100.  * </pre>
  101.  *
  102.  * Note, that you have to decide, whether to trust the contacts keys prior to sending a message, otherwise undecided
  103.  * keys are not included in the encryption process. You can trust keys by calling
  104.  * {@link OpenPgpContact#trust(OpenPgpV4Fingerprint)}. Same goes for your own keys! In order to determine, whether
  105.  * there are undecided keys, call {@link OpenPgpContact#hasUndecidedKeys()}. The trust state of a single key can be
  106.  * determined using {@link OpenPgpContact#getTrust(OpenPgpV4Fingerprint)}.
  107.  *
  108.  * Note: This implementation does not yet have support for sending/receiving messages to/from MUCs.
  109.  *
  110.  * @see <a href="https://xmpp.org/extensions/xep-0374.html">
  111.  *     XEP-0374: OpenPGP for XMPP: Instant Messaging</a>
  112.  */
  113. public final class OXInstantMessagingManager extends Manager {

  114.     public static final String NAMESPACE_0 = "urn:xmpp:openpgp:im:0";

  115.     private static final Map<XMPPConnection, OXInstantMessagingManager> INSTANCES = new WeakHashMap<>();

  116.     private final Set<OxMessageListener> oxMessageListeners = new HashSet<>();
  117.     private final OpenPgpManager openPgpManager;

  118.     private OXInstantMessagingManager(final XMPPConnection connection) {
  119.         super(connection);
  120.         openPgpManager = OpenPgpManager.getInstanceFor(connection);
  121.         openPgpManager.registerSigncryptReceivedListener(this::signcryptElementReceivedListener);
  122.         announceSupportForOxInstantMessaging();
  123.     }

  124.     /**
  125.      * Return an instance of the {@link OXInstantMessagingManager} that belongs to the given {@code connection}.
  126.      *
  127.      * @param connection XMPP connection
  128.      * @return manager instance
  129.      */
  130.     public static synchronized OXInstantMessagingManager getInstanceFor(XMPPConnection connection) {
  131.         OXInstantMessagingManager manager = INSTANCES.get(connection);

  132.         if (manager == null) {
  133.             manager = new OXInstantMessagingManager(connection);
  134.             INSTANCES.put(connection, manager);
  135.         }

  136.         return manager;
  137.     }

  138.     /**
  139.      * Add the OX:IM namespace as a feature to our disco features.
  140.      */
  141.     public void announceSupportForOxInstantMessaging() {
  142.         ServiceDiscoveryManager.getInstanceFor(connection())
  143.                 .addFeature(NAMESPACE_0);
  144.     }

  145.     /**
  146.      * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
  147.      *
  148.      * @param jid {@link BareJid} of the contact in question.
  149.      * @return true if contact announces support, otherwise false.
  150.      *
  151.      * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error
  152.      * @throws SmackException.NotConnectedException if we are not connected
  153.      * @throws InterruptedException if the thread gets interrupted
  154.      * @throws SmackException.NoResponseException if the server doesn't respond
  155.      */
  156.     public boolean contactSupportsOxInstantMessaging(BareJid jid)
  157.             throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
  158.             SmackException.NoResponseException {
  159.         return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, NAMESPACE_0);
  160.     }

  161.     /**
  162.      * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging.
  163.      *
  164.      * @param contact {@link OpenPgpContact} in question.
  165.      * @return true if contact announces support, otherwise false.
  166.      *
  167.      * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error
  168.      * @throws SmackException.NotConnectedException if we are not connected
  169.      * @throws InterruptedException if the thread is interrupted
  170.      * @throws SmackException.NoResponseException if the server doesn't respond
  171.      */
  172.     public boolean contactSupportsOxInstantMessaging(OpenPgpContact contact)
  173.             throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
  174.             SmackException.NoResponseException {
  175.         return contactSupportsOxInstantMessaging(contact.getJid());
  176.     }

  177.     /**
  178.      * Add an {@link OxMessageListener}. The listener gets notified about incoming {@link OpenPgpMessage}s which
  179.      * contained an OX-IM message.
  180.      *
  181.      * @param listener listener
  182.      * @return true if the listener gets added, otherwise false.
  183.      */
  184.     public boolean addOxMessageListener(OxMessageListener listener) {
  185.         return oxMessageListeners.add(listener);
  186.     }

  187.     /**
  188.      * Remove an {@link OxMessageListener}. The listener will no longer be notified about OX-IM messages.
  189.      *
  190.      * @param listener listener
  191.      * @return true, if the listener gets removed, otherwise false
  192.      */
  193.     public boolean removeOxMessageListener(OxMessageListener listener) {
  194.         return oxMessageListeners.remove(listener);
  195.     }

  196.     /**
  197.      * Send an OX message to a {@link OpenPgpContact}. The message will be encrypted to all active keys of the contact,
  198.      * as well as all of our active keys. The message is also signed with our key.
  199.      *
  200.      * @param contact contact capable of OpenPGP for XMPP: Instant Messaging.
  201.      * @param body message body.
  202.      *
  203.      * @return {@link EncryptionResult} containing metadata about the messages encryption + signatures.
  204.      *
  205.      * @throws InterruptedException if the thread is interrupted
  206.      * @throws IOException IO is dangerous
  207.      * @throws SmackException.NotConnectedException if we are not connected
  208.      * @throws SmackException.NotLoggedInException if we are not logged in
  209.      * @throws PGPException PGP is brittle
  210.      */
  211.     public EncryptionResult sendOxMessage(OpenPgpContact contact, CharSequence body)
  212.             throws InterruptedException, IOException,
  213.             SmackException.NotConnectedException, SmackException.NotLoggedInException, PGPException {
  214.         MessageBuilder messageBuilder = connection()
  215.                 .getStanzaFactory()
  216.                 .buildMessageStanza()
  217.                 .to(contact.getJid());

  218.         Message.Body mBody = new Message.Body(null, body.toString());
  219.         EncryptionResult metadata = addOxMessage(messageBuilder, contact, Collections.<ExtensionElement>singletonList(mBody));

  220.         Message message = messageBuilder.build();
  221.         ChatManager.getInstanceFor(connection()).chatWith(contact.getJid().asEntityBareJidIfPossible()).send(message);

  222.         return metadata;
  223.     }

  224.     /**
  225.      * Add an OX-IM message element to a message.
  226.      *
  227.      * @param messageBuilder a message builder.
  228.      * @param contact recipient of the message
  229.      * @param payload payload which will be encrypted and signed
  230.      *
  231.      * @return {@link EncryptionResult} containing metadata about the messages encryption + metadata.
  232.      *
  233.      * @throws SmackException.NotLoggedInException in case we are not logged in
  234.      * @throws PGPException in case something goes wrong during encryption
  235.      * @throws IOException IO is dangerous (we need to read keys)
  236.      */
  237.     public EncryptionResult addOxMessage(MessageBuilder messageBuilder, OpenPgpContact contact, List<ExtensionElement> payload)
  238.             throws SmackException.NotLoggedInException, PGPException, IOException {
  239.         return addOxMessage(messageBuilder, Collections.singleton(contact), payload);
  240.     }

  241.     /**
  242.      * Add an OX-IM message element to a message.
  243.      *
  244.      * @param messageBuilder message
  245.      * @param recipients recipients of the message
  246.      * @param payload payload which will be encrypted and signed
  247.      *
  248.      * @return {@link EncryptionResult} containing metadata about the messages encryption + signatures.
  249.      *
  250.      * @throws SmackException.NotLoggedInException in case we are not logged in
  251.      * @throws PGPException in case something goes wrong during encryption
  252.      * @throws IOException IO is dangerous (we need to read keys)
  253.      */
  254.     public EncryptionResult addOxMessage(MessageBuilder messageBuilder, Set<OpenPgpContact> recipients, List<ExtensionElement> payload)
  255.             throws SmackException.NotLoggedInException, IOException, PGPException {

  256.         OpenPgpElementAndMetadata openPgpElementAndMetadata = signAndEncrypt(recipients, payload);
  257.         messageBuilder.addExtension(openPgpElementAndMetadata.getElement());

  258.         // Set hints on message
  259.         ExplicitMessageEncryptionElement.set(messageBuilder,
  260.                 ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0);
  261.         StoreHint.set(messageBuilder);
  262.         setOXBodyHint(messageBuilder);

  263.         return openPgpElementAndMetadata.getMetadata();
  264.     }

  265.     /**
  266.      * Wrap some {@code payload} into a {@link SigncryptElement}, sign and encrypt it for {@code contacts} and ourselves.
  267.      *
  268.      * @param contacts recipients of the message
  269.      * @param payload payload which will be encrypted and signed
  270.      *
  271.      * @return encrypted and signed {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about the
  272.      * encryption + signatures.
  273.      *
  274.      * @throws SmackException.NotLoggedInException in case we are not logged in
  275.      * @throws IOException IO is dangerous (we need to read keys)
  276.      * @throws PGPException in case encryption goes wrong
  277.      */
  278.     public OpenPgpElementAndMetadata signAndEncrypt(Set<OpenPgpContact> contacts, List<ExtensionElement> payload)
  279.             throws SmackException.NotLoggedInException, IOException, PGPException {

  280.         Set<Jid> jids = new HashSet<>();
  281.         for (OpenPgpContact contact : contacts) {
  282.             jids.add(contact.getJid());
  283.         }
  284.         jids.add(openPgpManager.getOpenPgpSelf().getJid());

  285.         SigncryptElement signcryptElement = new SigncryptElement(jids, payload);
  286.         OpenPgpElementAndMetadata encrypted = openPgpManager.getOpenPgpProvider().signAndEncrypt(signcryptElement,
  287.                 openPgpManager.getOpenPgpSelf(), contacts);

  288.         return encrypted;
  289.     }

  290.     /**
  291.      * Manually decrypt and verify an {@link OpenPgpElement}.
  292.      *
  293.      * @param element encrypted, signed {@link OpenPgpElement}.
  294.      * @param sender sender of the message.
  295.      *
  296.      * @return decrypted, verified message
  297.      *
  298.      * @throws SmackException.NotLoggedInException In case we are not logged in (we need our jid to access our keys)
  299.      * @throws PGPException in case of an PGP error
  300.      * @throws IOException in case of an IO error (reading keys, streams etc)
  301.      * @throws XmlPullParserException in case that the content of the {@link OpenPgpElement} is not a valid
  302.      * {@link OpenPgpContentElement} or broken XML.
  303.      * @throws IllegalArgumentException if the elements content is not a {@link SigncryptElement}. This happens, if the
  304.      * element likely is not an OX message.
  305.      */
  306.     public OpenPgpMessage decryptAndVerify(OpenPgpElement element, OpenPgpContact sender)
  307.             throws SmackException.NotLoggedInException, PGPException, IOException, XmlPullParserException {

  308.         OpenPgpMessage decrypted = openPgpManager.decryptOpenPgpElement(element, sender);
  309.         if (decrypted.getState() != OpenPgpMessage.State.signcrypt) {
  310.             throw new IllegalArgumentException("Decrypted message does appear to not be an OX message. (State: " + decrypted.getState() + ")");
  311.         }

  312.         return decrypted;
  313.     }

  314.     /**
  315.      * Set a hint about the message being OX-IM encrypted as body of the message.
  316.      *
  317.      * @param message message
  318.      */
  319.     private static void setOXBodyHint(MessageBuilder message) {
  320.         message.setBody("This message is encrypted using XEP-0374: OpenPGP for XMPP: Instant Messaging.");
  321.     }

  322.     private void signcryptElementReceivedListener(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata) {
  323.         for (OxMessageListener listener : oxMessageListeners) {
  324.             listener.newIncomingOxMessage(contact, originalMessage, signcryptElement, metadata);
  325.         }
  326.     }
  327. }