Smack/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox_im/OXInstantMessagingManager.java

363 lines
15 KiB
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);
}
}
}