2018-07-29 18:52:45 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* 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;
|
2019-10-24 15:45:08 +02:00
|
|
|
import org.jivesoftware.smack.packet.MessageBuilder;
|
2019-05-06 22:06:13 +02:00
|
|
|
import org.jivesoftware.smack.xml.XmlPullParserException;
|
|
|
|
|
2018-07-29 18:52:45 +02:00
|
|
|
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;
|
2018-08-02 17:33:55 +02:00
|
|
|
import org.jivesoftware.smackx.ox.element.OpenPgpContentElement;
|
2018-07-29 18:52:45 +02:00
|
|
|
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.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);
|
2020-05-24 21:07:56 +02:00
|
|
|
openPgpManager.registerSigncryptReceivedListener(this::signcryptElementReceivedListener);
|
2018-07-29 18:52:45 +02:00
|
|
|
announceSupportForOxInstantMessaging();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an instance of the {@link OXInstantMessagingManager} that belongs to the given {@code connection}.
|
|
|
|
*
|
|
|
|
* @param connection XMPP connection
|
|
|
|
* @return manager instance
|
|
|
|
*/
|
2019-04-16 09:21:14 +02:00
|
|
|
public static synchronized OXInstantMessagingManager getInstanceFor(XMPPConnection connection) {
|
2018-07-29 18:52:45 +02:00
|
|
|
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 OpenPgpMetadata} 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 OpenPgpMetadata sendOxMessage(OpenPgpContact contact, CharSequence body)
|
|
|
|
throws InterruptedException, IOException,
|
|
|
|
SmackException.NotConnectedException, SmackException.NotLoggedInException, PGPException {
|
2019-10-24 15:45:08 +02:00
|
|
|
MessageBuilder messageBuilder = connection()
|
|
|
|
.getStanzaFactory()
|
|
|
|
.buildMessageStanza()
|
|
|
|
.to(contact.getJid());
|
2018-07-29 18:52:45 +02:00
|
|
|
|
2019-10-24 15:45:08 +02:00
|
|
|
Message.Body mBody = new Message.Body(null, body.toString());
|
|
|
|
OpenPgpMetadata metadata = addOxMessage(messageBuilder, contact, Collections.<ExtensionElement>singletonList(mBody));
|
2018-07-29 18:52:45 +02:00
|
|
|
|
2019-10-24 15:45:08 +02:00
|
|
|
Message message = messageBuilder.build();
|
2018-07-29 18:52:45 +02:00
|
|
|
ChatManager.getInstanceFor(connection()).chatWith(contact.getJid().asEntityBareJidIfPossible()).send(message);
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an OX-IM message element to a message.
|
|
|
|
*
|
2019-10-24 15:45:08 +02:00
|
|
|
* @param messageBuilder a message builder.
|
2018-07-29 18:52:45 +02:00
|
|
|
* @param contact recipient of the message
|
|
|
|
* @param payload payload which will be encrypted and signed
|
|
|
|
*
|
|
|
|
* @return {@link OpenPgpMetadata} 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)
|
|
|
|
*/
|
2019-10-24 15:45:08 +02:00
|
|
|
public OpenPgpMetadata addOxMessage(MessageBuilder messageBuilder, OpenPgpContact contact, List<ExtensionElement> payload)
|
2018-07-29 18:52:45 +02:00
|
|
|
throws SmackException.NotLoggedInException, PGPException, IOException {
|
2019-10-24 15:45:08 +02:00
|
|
|
return addOxMessage(messageBuilder, Collections.singleton(contact), payload);
|
2018-07-29 18:52:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an OX-IM message element to a message.
|
|
|
|
*
|
2019-10-24 15:45:08 +02:00
|
|
|
* @param messageBuilder message
|
2020-07-12 15:47:06 +02:00
|
|
|
* @param recipients recipients of the message
|
2018-07-29 18:52:45 +02:00
|
|
|
* @param payload payload which will be encrypted and signed
|
|
|
|
*
|
|
|
|
* @return 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)
|
|
|
|
*/
|
2020-07-12 15:47:06 +02:00
|
|
|
public OpenPgpMetadata addOxMessage(MessageBuilder messageBuilder, Set<OpenPgpContact> recipients, List<ExtensionElement> payload)
|
2018-07-29 18:52:45 +02:00
|
|
|
throws SmackException.NotLoggedInException, IOException, PGPException {
|
|
|
|
|
|
|
|
OpenPgpElementAndMetadata openPgpElementAndMetadata = signAndEncrypt(recipients, payload);
|
2019-10-24 15:45:08 +02:00
|
|
|
messageBuilder.addExtension(openPgpElementAndMetadata.getElement());
|
2018-07-29 18:52:45 +02:00
|
|
|
|
|
|
|
// Set hints on message
|
2019-10-24 15:45:08 +02:00
|
|
|
ExplicitMessageEncryptionElement.set(messageBuilder,
|
2018-07-29 18:52:45 +02:00
|
|
|
ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0);
|
2019-10-24 15:45:08 +02:00
|
|
|
StoreHint.set(messageBuilder);
|
|
|
|
setOXBodyHint(messageBuilder);
|
2018-07-29 18:52:45 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-08-02 17:33:55 +02:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2018-07-29 18:52:45 +02:00
|
|
|
/**
|
|
|
|
* Set a hint about the message being OX-IM encrypted as body of the message.
|
|
|
|
*
|
|
|
|
* @param message message
|
|
|
|
*/
|
2019-10-24 15:45:08 +02:00
|
|
|
private static void setOXBodyHint(MessageBuilder message) {
|
2018-07-29 18:52:45 +02:00
|
|
|
message.setBody("This message is encrypted using XEP-0374: OpenPGP for XMPP: Instant Messaging.");
|
|
|
|
}
|
|
|
|
|
2020-05-24 21:07:56 +02:00
|
|
|
private void signcryptElementReceivedListener(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata) {
|
|
|
|
for (OxMessageListener listener : oxMessageListeners) {
|
|
|
|
listener.newIncomingOxMessage(contact, originalMessage, signcryptElement, metadata);
|
2018-07-29 18:52:45 +02:00
|
|
|
}
|
2020-05-24 21:07:56 +02:00
|
|
|
}
|
2018-07-29 18:52:45 +02:00
|
|
|
}
|