001/** 002 * 003 * Copyright 2018 Paul Schaub. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.ox_im; 018 019import java.io.IOException; 020import java.util.Collections; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import java.util.WeakHashMap; 026 027import org.jivesoftware.smack.Manager; 028import org.jivesoftware.smack.SmackException; 029import org.jivesoftware.smack.XMPPConnection; 030import org.jivesoftware.smack.XMPPException; 031import org.jivesoftware.smack.chat2.ChatManager; 032import org.jivesoftware.smack.packet.ExtensionElement; 033import org.jivesoftware.smack.packet.Message; 034import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 035import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement; 036import org.jivesoftware.smackx.hints.element.StoreHint; 037import org.jivesoftware.smackx.ox.OpenPgpContact; 038import org.jivesoftware.smackx.ox.OpenPgpManager; 039import org.jivesoftware.smackx.ox.OpenPgpMessage; 040import org.jivesoftware.smackx.ox.crypto.OpenPgpElementAndMetadata; 041import org.jivesoftware.smackx.ox.element.OpenPgpElement; 042import org.jivesoftware.smackx.ox.element.SigncryptElement; 043import org.jivesoftware.smackx.ox.listener.SigncryptElementReceivedListener; 044 045import org.bouncycastle.openpgp.PGPException; 046import org.jxmpp.jid.BareJid; 047import org.jxmpp.jid.Jid; 048import org.pgpainless.decryption_verification.OpenPgpMetadata; 049import org.pgpainless.key.OpenPgpV4Fingerprint; 050 051/** 052 * Entry point of Smacks API for XEP-0374: OpenPGP for XMPP: Instant Messaging. 053 * 054 * <h2>Setup</h2> 055 * 056 * In order to set up OX Instant Messaging, please first follow the setup routines of the {@link OpenPgpManager}, then 057 * do the following steps: 058 * 059 * <h3>Acquire an {@link OXInstantMessagingManager} instance.</h3> 060 * 061 * <pre> 062 * {@code 063 * OXInstantMessagingManager instantManager = OXInstantMessagingManager.getInstanceFor(connection); 064 * } 065 * </pre> 066 * 067 * <h3>Listen for OX messages</h3> 068 * In order to listen for incoming OX:IM messages, you have to register a listener. 069 * 070 * <pre> 071 * {@code 072 * instantManager.addOxMessageListener( 073 * new OxMessageListener() { 074 * void newIncomingOxMessage(OpenPgpContact contact, 075 * Message originalMessage, 076 * SigncryptElement decryptedPayload) { 077 * Message.Body body = decryptedPayload.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE); 078 * ... 079 * } 080 * }); 081 * } 082 * </pre> 083 * 084 * <h3>Finally, announce support for OX:IM</h3> 085 * In order to let your contacts know, that you support message encrypting using the OpenPGP for XMPP: Instant Messaging 086 * profile, you have to announce support for OX:IM. 087 * 088 * <pre> 089 * {@code 090 * instantManager.announceSupportForOxInstantMessaging(); 091 * } 092 * </pre> 093 * 094 * <h2>Sending messages</h2> 095 * In order to send an OX:IM message, just do 096 * 097 * <pre> 098 * {@code 099 * instantManager.sendOxMessage(openPgpManager.getOpenPgpContact(contactsJid), "Hello World"); 100 * } 101 * </pre> 102 * 103 * Note, that you have to decide, whether to trust the contacts keys prior to sending a message, otherwise undecided 104 * keys are not included in the encryption process. You can trust keys by calling 105 * {@link OpenPgpContact#trust(OpenPgpV4Fingerprint)}. Same goes for your own keys! In order to determine, whether 106 * there are undecided keys, call {@link OpenPgpContact#hasUndecidedKeys()}. The trust state of a single key can be 107 * determined using {@link OpenPgpContact#getTrust(OpenPgpV4Fingerprint)}. 108 * 109 * Note: This implementation does not yet have support for sending/receiving messages to/from MUCs. 110 * 111 * @see <a href="https://xmpp.org/extensions/xep-0374.html"> 112 * XEP-0374: OpenPGP for XMPP: Instant Messaging</a> 113 */ 114public final class OXInstantMessagingManager extends Manager { 115 116 public static final String NAMESPACE_0 = "urn:xmpp:openpgp:im:0"; 117 118 private static final Map<XMPPConnection, OXInstantMessagingManager> INSTANCES = new WeakHashMap<>(); 119 120 private final Set<OxMessageListener> oxMessageListeners = new HashSet<>(); 121 private final OpenPgpManager openPgpManager; 122 123 private OXInstantMessagingManager(final XMPPConnection connection) { 124 super(connection); 125 openPgpManager = OpenPgpManager.getInstanceFor(connection); 126 openPgpManager.registerSigncryptReceivedListener(signcryptElementReceivedListener); 127 announceSupportForOxInstantMessaging(); 128 } 129 130 /** 131 * Return an instance of the {@link OXInstantMessagingManager} that belongs to the given {@code connection}. 132 * 133 * @param connection XMPP connection 134 * @return manager instance 135 */ 136 public static OXInstantMessagingManager getInstanceFor(XMPPConnection connection) { 137 OXInstantMessagingManager manager = INSTANCES.get(connection); 138 139 if (manager == null) { 140 manager = new OXInstantMessagingManager(connection); 141 INSTANCES.put(connection, manager); 142 } 143 144 return manager; 145 } 146 147 /** 148 * Add the OX:IM namespace as a feature to our disco features. 149 */ 150 public void announceSupportForOxInstantMessaging() { 151 ServiceDiscoveryManager.getInstanceFor(connection()) 152 .addFeature(NAMESPACE_0); 153 } 154 155 /** 156 * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging. 157 * 158 * @param jid {@link BareJid} of the contact in question. 159 * @return true if contact announces support, otherwise false. 160 * 161 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error 162 * @throws SmackException.NotConnectedException if we are not connected 163 * @throws InterruptedException if the thread gets interrupted 164 * @throws SmackException.NoResponseException if the server doesn't respond 165 */ 166 public boolean contactSupportsOxInstantMessaging(BareJid jid) 167 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 168 SmackException.NoResponseException { 169 return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, NAMESPACE_0); 170 } 171 172 /** 173 * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging. 174 * 175 * @param contact {@link OpenPgpContact} in question. 176 * @return true if contact announces support, otherwise false. 177 * 178 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error 179 * @throws SmackException.NotConnectedException if we are not connected 180 * @throws InterruptedException if the thread is interrupted 181 * @throws SmackException.NoResponseException if the server doesn't respond 182 */ 183 public boolean contactSupportsOxInstantMessaging(OpenPgpContact contact) 184 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 185 SmackException.NoResponseException { 186 return contactSupportsOxInstantMessaging(contact.getJid()); 187 } 188 189 /** 190 * Add an {@link OxMessageListener}. The listener gets notified about incoming {@link OpenPgpMessage}s which 191 * contained an OX-IM message. 192 * 193 * @param listener listener 194 * @return true if the listener gets added, otherwise false. 195 */ 196 public boolean addOxMessageListener(OxMessageListener listener) { 197 return oxMessageListeners.add(listener); 198 } 199 200 /** 201 * Remove an {@link OxMessageListener}. The listener will no longer be notified about OX-IM messages. 202 * 203 * @param listener listener 204 * @return true, if the listener gets removed, otherwise false 205 */ 206 public boolean removeOxMessageListener(OxMessageListener listener) { 207 return oxMessageListeners.remove(listener); 208 } 209 210 /** 211 * Send an OX message to a {@link OpenPgpContact}. The message will be encrypted to all active keys of the contact, 212 * as well as all of our active keys. The message is also signed with our key. 213 * 214 * @param contact contact capable of OpenPGP for XMPP: Instant Messaging. 215 * @param body message body. 216 * 217 * @return {@link OpenPgpMetadata} about the messages encryption + signatures. 218 * 219 * @throws InterruptedException if the thread is interrupted 220 * @throws IOException IO is dangerous 221 * @throws SmackException.NotConnectedException if we are not connected 222 * @throws SmackException.NotLoggedInException if we are not logged in 223 * @throws PGPException PGP is brittle 224 */ 225 public OpenPgpMetadata sendOxMessage(OpenPgpContact contact, CharSequence body) 226 throws InterruptedException, IOException, 227 SmackException.NotConnectedException, SmackException.NotLoggedInException, PGPException { 228 Message message = new Message(contact.getJid()); 229 Message.Body mBody = new Message.Body(null, body.toString()); 230 231 OpenPgpMetadata metadata = addOxMessage(message, contact, Collections.<ExtensionElement>singletonList(mBody)); 232 233 ChatManager.getInstanceFor(connection()).chatWith(contact.getJid().asEntityBareJidIfPossible()).send(message); 234 235 return metadata; 236 } 237 238 /** 239 * Add an OX-IM message element to a message. 240 * 241 * @param message message 242 * @param contact recipient of the message 243 * @param payload payload which will be encrypted and signed 244 * 245 * @return {@link OpenPgpMetadata} about the messages encryption + metadata. 246 * 247 * @throws SmackException.NotLoggedInException in case we are not logged in 248 * @throws PGPException in case something goes wrong during encryption 249 * @throws IOException IO is dangerous (we need to read keys) 250 */ 251 public OpenPgpMetadata addOxMessage(Message message, OpenPgpContact contact, List<ExtensionElement> payload) 252 throws SmackException.NotLoggedInException, PGPException, IOException { 253 return addOxMessage(message, Collections.singleton(contact), payload); 254 } 255 256 /** 257 * Add an OX-IM message element to a message. 258 * 259 * @param message message 260 * @param contacts recipients of the message 261 * @param payload payload which will be encrypted and signed 262 * 263 * @return metadata about the messages encryption + signatures. 264 * 265 * @throws SmackException.NotLoggedInException in case we are not logged in 266 * @throws PGPException in case something goes wrong during encryption 267 * @throws IOException IO is dangerous (we need to read keys) 268 */ 269 public OpenPgpMetadata addOxMessage(Message message, Set<OpenPgpContact> contacts, List<ExtensionElement> payload) 270 throws SmackException.NotLoggedInException, IOException, PGPException { 271 272 HashSet<OpenPgpContact> recipients = new HashSet<>(contacts); 273 OpenPgpContact self = openPgpManager.getOpenPgpSelf(); 274 recipients.add(self); 275 276 OpenPgpElementAndMetadata openPgpElementAndMetadata = signAndEncrypt(recipients, payload); 277 message.addExtension(openPgpElementAndMetadata.getElement()); 278 279 // Set hints on message 280 ExplicitMessageEncryptionElement.set(message, 281 ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0); 282 StoreHint.set(message); 283 setOXBodyHint(message); 284 285 return openPgpElementAndMetadata.getMetadata(); 286 } 287 288 /** 289 * Wrap some {@code payload} into a {@link SigncryptElement}, sign and encrypt it for {@code contacts} and ourselves. 290 * 291 * @param contacts recipients of the message 292 * @param payload payload which will be encrypted and signed 293 * 294 * @return encrypted and signed {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about the 295 * encryption + signatures. 296 * 297 * @throws SmackException.NotLoggedInException in case we are not logged in 298 * @throws IOException IO is dangerous (we need to read keys) 299 * @throws PGPException in case encryption goes wrong 300 */ 301 public OpenPgpElementAndMetadata signAndEncrypt(Set<OpenPgpContact> contacts, List<ExtensionElement> payload) 302 throws SmackException.NotLoggedInException, IOException, PGPException { 303 304 Set<Jid> jids = new HashSet<>(); 305 for (OpenPgpContact contact : contacts) { 306 jids.add(contact.getJid()); 307 } 308 jids.add(openPgpManager.getOpenPgpSelf().getJid()); 309 310 SigncryptElement signcryptElement = new SigncryptElement(jids, payload); 311 OpenPgpElementAndMetadata encrypted = openPgpManager.getOpenPgpProvider().signAndEncrypt(signcryptElement, 312 openPgpManager.getOpenPgpSelf(), contacts); 313 314 return encrypted; 315 } 316 317 /** 318 * Set a hint about the message being OX-IM encrypted as body of the message. 319 * 320 * @param message message 321 */ 322 private static void setOXBodyHint(Message message) { 323 message.setBody("This message is encrypted using XEP-0374: OpenPGP for XMPP: Instant Messaging."); 324 } 325 326 private final SigncryptElementReceivedListener signcryptElementReceivedListener = new SigncryptElementReceivedListener() { 327 @Override 328 public void signcryptElementReceived(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata) { 329 for (OxMessageListener listener : oxMessageListeners) { 330 listener.newIncomingOxMessage(contact, originalMessage, signcryptElement, metadata); 331 } 332 } 333 }; 334}