/** * * Copyright 2018 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.ox_im; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.chat2.ChatManager; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.MessageBuilder; import org.jivesoftware.smack.xml.XmlPullParserException; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement; import org.jivesoftware.smackx.hints.element.StoreHint; import org.jivesoftware.smackx.ox.OpenPgpContact; import org.jivesoftware.smackx.ox.OpenPgpManager; import org.jivesoftware.smackx.ox.OpenPgpMessage; import org.jivesoftware.smackx.ox.crypto.OpenPgpElementAndMetadata; import org.jivesoftware.smackx.ox.element.OpenPgpContentElement; import org.jivesoftware.smackx.ox.element.OpenPgpElement; import org.jivesoftware.smackx.ox.element.SigncryptElement; import org.bouncycastle.openpgp.PGPException; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.key.OpenPgpV4Fingerprint; /** * Entry point of Smacks API for XEP-0374: OpenPGP for XMPP: Instant Messaging. * *

Setup

* * In order to set up OX Instant Messaging, please first follow the setup routines of the {@link OpenPgpManager}, then * do the following steps: * *

Acquire an {@link OXInstantMessagingManager} instance.

* *
 * {@code
 * OXInstantMessagingManager instantManager = OXInstantMessagingManager.getInstanceFor(connection);
 * }
 * 
* *

Listen for OX messages

* In order to listen for incoming OX:IM messages, you have to register a listener. * *
 * {@code
 * instantManager.addOxMessageListener(
 *          new OxMessageListener() {
 *              void newIncomingOxMessage(OpenPgpContact contact,
 *                                        Message originalMessage,
 *                                        SigncryptElement decryptedPayload) {
 *                  Message.Body body = decryptedPayload.getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE);
 *                  ...
 *              }
 *          });
 * }
 * 
* *

Finally, announce support for OX:IM

* In order to let your contacts know, that you support message encrypting using the OpenPGP for XMPP: Instant Messaging * profile, you have to announce support for OX:IM. * *
 * {@code
 * instantManager.announceSupportForOxInstantMessaging();
 * }
 * 
* *

Sending messages

* In order to send an OX:IM message, just do * *
 * {@code
 * instantManager.sendOxMessage(openPgpManager.getOpenPgpContact(contactsJid), "Hello World");
 * }
 * 
* * Note, that you have to decide, whether to trust the contacts keys prior to sending a message, otherwise undecided * keys are not included in the encryption process. You can trust keys by calling * {@link OpenPgpContact#trust(OpenPgpV4Fingerprint)}. Same goes for your own keys! In order to determine, whether * there are undecided keys, call {@link OpenPgpContact#hasUndecidedKeys()}. The trust state of a single key can be * determined using {@link OpenPgpContact#getTrust(OpenPgpV4Fingerprint)}. * * Note: This implementation does not yet have support for sending/receiving messages to/from MUCs. * * @see * XEP-0374: OpenPGP for XMPP: Instant Messaging */ public final class OXInstantMessagingManager extends Manager { public static final String NAMESPACE_0 = "urn:xmpp:openpgp:im:0"; private static final Map INSTANCES = new WeakHashMap<>(); private final Set oxMessageListeners = new HashSet<>(); private final OpenPgpManager openPgpManager; private OXInstantMessagingManager(final XMPPConnection connection) { super(connection); openPgpManager = OpenPgpManager.getInstanceFor(connection); openPgpManager.registerSigncryptReceivedListener(this::signcryptElementReceivedListener); announceSupportForOxInstantMessaging(); } /** * Return an instance of the {@link OXInstantMessagingManager} that belongs to the given {@code connection}. * * @param connection XMPP connection * @return manager instance */ public static synchronized OXInstantMessagingManager getInstanceFor(XMPPConnection connection) { OXInstantMessagingManager manager = INSTANCES.get(connection); if (manager == null) { manager = new OXInstantMessagingManager(connection); INSTANCES.put(connection, manager); } return manager; } /** * Add the OX:IM namespace as a feature to our disco features. */ public void announceSupportForOxInstantMessaging() { ServiceDiscoveryManager.getInstanceFor(connection()) .addFeature(NAMESPACE_0); } /** * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging. * * @param jid {@link BareJid} of the contact in question. * @return true if contact announces support, otherwise false. * * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error * @throws SmackException.NotConnectedException if we are not connected * @throws InterruptedException if the thread gets interrupted * @throws SmackException.NoResponseException if the server doesn't respond */ public boolean contactSupportsOxInstantMessaging(BareJid jid) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, NAMESPACE_0); } /** * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging. * * @param contact {@link OpenPgpContact} in question. * @return true if contact announces support, otherwise false. * * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error * @throws SmackException.NotConnectedException if we are not connected * @throws InterruptedException if the thread is interrupted * @throws SmackException.NoResponseException if the server doesn't respond */ public boolean contactSupportsOxInstantMessaging(OpenPgpContact contact) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { return contactSupportsOxInstantMessaging(contact.getJid()); } /** * Add an {@link OxMessageListener}. The listener gets notified about incoming {@link OpenPgpMessage}s which * contained an OX-IM message. * * @param listener listener * @return true if the listener gets added, otherwise false. */ public boolean addOxMessageListener(OxMessageListener listener) { return oxMessageListeners.add(listener); } /** * Remove an {@link OxMessageListener}. The listener will no longer be notified about OX-IM messages. * * @param listener listener * @return true, if the listener gets removed, otherwise false */ public boolean removeOxMessageListener(OxMessageListener listener) { return oxMessageListeners.remove(listener); } /** * Send an OX message to a {@link OpenPgpContact}. The message will be encrypted to all active keys of the contact, * as well as all of our active keys. The message is also signed with our key. * * @param contact contact capable of OpenPGP for XMPP: Instant Messaging. * @param body message body. * * @return {@link EncryptionResult} containing metadata about the messages encryption + signatures. * * @throws InterruptedException if the thread is interrupted * @throws IOException IO is dangerous * @throws SmackException.NotConnectedException if we are not connected * @throws SmackException.NotLoggedInException if we are not logged in * @throws PGPException PGP is brittle */ public EncryptionResult sendOxMessage(OpenPgpContact contact, CharSequence body) throws InterruptedException, IOException, SmackException.NotConnectedException, SmackException.NotLoggedInException, PGPException { MessageBuilder messageBuilder = connection() .getStanzaFactory() .buildMessageStanza() .to(contact.getJid()); Message.Body mBody = new Message.Body(null, body.toString()); EncryptionResult metadata = addOxMessage(messageBuilder, contact, Collections.singletonList(mBody)); Message message = messageBuilder.build(); ChatManager.getInstanceFor(connection()).chatWith(contact.getJid().asEntityBareJidIfPossible()).send(message); return metadata; } /** * Add an OX-IM message element to a message. * * @param messageBuilder a message builder. * @param contact recipient of the message * @param payload payload which will be encrypted and signed * * @return {@link EncryptionResult} containing metadata about the messages encryption + metadata. * * @throws SmackException.NotLoggedInException in case we are not logged in * @throws PGPException in case something goes wrong during encryption * @throws IOException IO is dangerous (we need to read keys) */ public EncryptionResult addOxMessage(MessageBuilder messageBuilder, OpenPgpContact contact, List payload) throws SmackException.NotLoggedInException, PGPException, IOException { return addOxMessage(messageBuilder, Collections.singleton(contact), payload); } /** * Add an OX-IM message element to a message. * * @param messageBuilder message * @param recipients recipients of the message * @param payload payload which will be encrypted and signed * * @return {@link EncryptionResult} containing metadata about the messages encryption + signatures. * * @throws SmackException.NotLoggedInException in case we are not logged in * @throws PGPException in case something goes wrong during encryption * @throws IOException IO is dangerous (we need to read keys) */ public EncryptionResult addOxMessage(MessageBuilder messageBuilder, Set recipients, List payload) throws SmackException.NotLoggedInException, IOException, PGPException { OpenPgpElementAndMetadata openPgpElementAndMetadata = signAndEncrypt(recipients, payload); messageBuilder.addExtension(openPgpElementAndMetadata.getElement()); // Set hints on message ExplicitMessageEncryptionElement.set(messageBuilder, ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0); StoreHint.set(messageBuilder); setOXBodyHint(messageBuilder); return openPgpElementAndMetadata.getMetadata(); } /** * Wrap some {@code payload} into a {@link SigncryptElement}, sign and encrypt it for {@code contacts} and ourselves. * * @param contacts recipients of the message * @param payload payload which will be encrypted and signed * * @return encrypted and signed {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about the * encryption + signatures. * * @throws SmackException.NotLoggedInException in case we are not logged in * @throws IOException IO is dangerous (we need to read keys) * @throws PGPException in case encryption goes wrong */ public OpenPgpElementAndMetadata signAndEncrypt(Set contacts, List payload) throws SmackException.NotLoggedInException, IOException, PGPException { Set jids = new HashSet<>(); for (OpenPgpContact contact : contacts) { jids.add(contact.getJid()); } jids.add(openPgpManager.getOpenPgpSelf().getJid()); SigncryptElement signcryptElement = new SigncryptElement(jids, payload); OpenPgpElementAndMetadata encrypted = openPgpManager.getOpenPgpProvider().signAndEncrypt(signcryptElement, openPgpManager.getOpenPgpSelf(), contacts); return encrypted; } /** * Manually decrypt and verify an {@link OpenPgpElement}. * * @param element encrypted, signed {@link OpenPgpElement}. * @param sender sender of the message. * * @return decrypted, verified message * * @throws SmackException.NotLoggedInException In case we are not logged in (we need our jid to access our keys) * @throws PGPException in case of an PGP error * @throws IOException in case of an IO error (reading keys, streams etc) * @throws XmlPullParserException in case that the content of the {@link OpenPgpElement} is not a valid * {@link OpenPgpContentElement} or broken XML. * @throws IllegalArgumentException if the elements content is not a {@link SigncryptElement}. This happens, if the * element likely is not an OX message. */ public OpenPgpMessage decryptAndVerify(OpenPgpElement element, OpenPgpContact sender) throws SmackException.NotLoggedInException, PGPException, IOException, XmlPullParserException { OpenPgpMessage decrypted = openPgpManager.decryptOpenPgpElement(element, sender); if (decrypted.getState() != OpenPgpMessage.State.signcrypt) { throw new IllegalArgumentException("Decrypted message does appear to not be an OX message. (State: " + decrypted.getState() + ")"); } return decrypted; } /** * Set a hint about the message being OX-IM encrypted as body of the message. * * @param message message */ private static void setOXBodyHint(MessageBuilder message) { message.setBody("This message is encrypted using XEP-0374: OpenPGP for XMPP: Instant Messaging."); } private void signcryptElementReceivedListener(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata) { for (OxMessageListener listener : oxMessageListeners) { listener.newIncomingOxMessage(contact, originalMessage, signcryptElement, metadata); } } }