OXInstantMessagingManager.java
- /**
- *
- * 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.
- *
- * <h2>Setup</h2>
- *
- * In order to set up OX Instant Messaging, please first follow the setup routines of the {@link OpenPgpManager}, then
- * do the following steps:
- *
- * <h3>Acquire an {@link OXInstantMessagingManager} instance.</h3>
- *
- * <pre>
- * {@code
- * OXInstantMessagingManager instantManager = OXInstantMessagingManager.getInstanceFor(connection);
- * }
- * </pre>
- *
- * <h3>Listen for OX messages</h3>
- * In order to listen for incoming OX:IM messages, you have to register a listener.
- *
- * <pre>
- * {@code
- * instantManager.addOxMessageListener(
- * new OxMessageListener() {
- * void newIncomingOxMessage(OpenPgpContact contact,
- * Message originalMessage,
- * SigncryptElement decryptedPayload) {
- * Message.Body body = decryptedPayload.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE);
- * ...
- * }
- * });
- * }
- * </pre>
- *
- * <h3>Finally, announce support for OX:IM</h3>
- * 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.
- *
- * <pre>
- * {@code
- * instantManager.announceSupportForOxInstantMessaging();
- * }
- * </pre>
- *
- * <h2>Sending messages</h2>
- * In order to send an OX:IM message, just do
- *
- * <pre>
- * {@code
- * instantManager.sendOxMessage(openPgpManager.getOpenPgpContact(contactsJid), "Hello World");
- * }
- * </pre>
- *
- * 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 <a href="https://xmpp.org/extensions/xep-0374.html">
- * XEP-0374: OpenPGP for XMPP: Instant Messaging</a>
- */
- public final class OXInstantMessagingManager extends Manager {
- public static final String NAMESPACE_0 = "urn:xmpp:openpgp:im:0";
- private static final Map<XMPPConnection, OXInstantMessagingManager> INSTANCES = new WeakHashMap<>();
- private final Set<OxMessageListener> 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.<ExtensionElement>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<ExtensionElement> 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<OpenPgpContact> recipients, List<ExtensionElement> 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<OpenPgpContact> contacts, List<ExtensionElement> payload)
- throws SmackException.NotLoggedInException, IOException, PGPException {
- Set<Jid> 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);
- }
- }
- }