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.OpenPgpContentElement; 042import org.jivesoftware.smackx.ox.element.OpenPgpElement; 043import org.jivesoftware.smackx.ox.element.SigncryptElement; 044import org.jivesoftware.smackx.ox.listener.SigncryptElementReceivedListener; 045 046import org.bouncycastle.openpgp.PGPException; 047import org.jxmpp.jid.BareJid; 048import org.jxmpp.jid.Jid; 049import org.pgpainless.decryption_verification.OpenPgpMetadata; 050import org.pgpainless.key.OpenPgpV4Fingerprint; 051import org.xmlpull.v1.XmlPullParserException; 052 053/** 054 * Entry point of Smacks API for XEP-0374: OpenPGP for XMPP: Instant Messaging. 055 * 056 * <h2>Setup</h2> 057 * 058 * In order to set up OX Instant Messaging, please first follow the setup routines of the {@link OpenPgpManager}, then 059 * do the following steps: 060 * 061 * <h3>Acquire an {@link OXInstantMessagingManager} instance.</h3> 062 * 063 * <pre> 064 * {@code 065 * OXInstantMessagingManager instantManager = OXInstantMessagingManager.getInstanceFor(connection); 066 * } 067 * </pre> 068 * 069 * <h3>Listen for OX messages</h3> 070 * In order to listen for incoming OX:IM messages, you have to register a listener. 071 * 072 * <pre> 073 * {@code 074 * instantManager.addOxMessageListener( 075 * new OxMessageListener() { 076 * void newIncomingOxMessage(OpenPgpContact contact, 077 * Message originalMessage, 078 * SigncryptElement decryptedPayload) { 079 * Message.Body body = decryptedPayload.<Message.Body>getExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE); 080 * ... 081 * } 082 * }); 083 * } 084 * </pre> 085 * 086 * <h3>Finally, announce support for OX:IM</h3> 087 * In order to let your contacts know, that you support message encrypting using the OpenPGP for XMPP: Instant Messaging 088 * profile, you have to announce support for OX:IM. 089 * 090 * <pre> 091 * {@code 092 * instantManager.announceSupportForOxInstantMessaging(); 093 * } 094 * </pre> 095 * 096 * <h2>Sending messages</h2> 097 * In order to send an OX:IM message, just do 098 * 099 * <pre> 100 * {@code 101 * instantManager.sendOxMessage(openPgpManager.getOpenPgpContact(contactsJid), "Hello World"); 102 * } 103 * </pre> 104 * 105 * Note, that you have to decide, whether to trust the contacts keys prior to sending a message, otherwise undecided 106 * keys are not included in the encryption process. You can trust keys by calling 107 * {@link OpenPgpContact#trust(OpenPgpV4Fingerprint)}. Same goes for your own keys! In order to determine, whether 108 * there are undecided keys, call {@link OpenPgpContact#hasUndecidedKeys()}. The trust state of a single key can be 109 * determined using {@link OpenPgpContact#getTrust(OpenPgpV4Fingerprint)}. 110 * 111 * Note: This implementation does not yet have support for sending/receiving messages to/from MUCs. 112 * 113 * @see <a href="https://xmpp.org/extensions/xep-0374.html"> 114 * XEP-0374: OpenPGP for XMPP: Instant Messaging</a> 115 */ 116public final class OXInstantMessagingManager extends Manager { 117 118 public static final String NAMESPACE_0 = "urn:xmpp:openpgp:im:0"; 119 120 private static final Map<XMPPConnection, OXInstantMessagingManager> INSTANCES = new WeakHashMap<>(); 121 122 private final Set<OxMessageListener> oxMessageListeners = new HashSet<>(); 123 private final OpenPgpManager openPgpManager; 124 125 private OXInstantMessagingManager(final XMPPConnection connection) { 126 super(connection); 127 openPgpManager = OpenPgpManager.getInstanceFor(connection); 128 openPgpManager.registerSigncryptReceivedListener(signcryptElementReceivedListener); 129 announceSupportForOxInstantMessaging(); 130 } 131 132 /** 133 * Return an instance of the {@link OXInstantMessagingManager} that belongs to the given {@code connection}. 134 * 135 * @param connection XMPP connection 136 * @return manager instance 137 */ 138 public static OXInstantMessagingManager getInstanceFor(XMPPConnection connection) { 139 OXInstantMessagingManager manager = INSTANCES.get(connection); 140 141 if (manager == null) { 142 manager = new OXInstantMessagingManager(connection); 143 INSTANCES.put(connection, manager); 144 } 145 146 return manager; 147 } 148 149 /** 150 * Add the OX:IM namespace as a feature to our disco features. 151 */ 152 public void announceSupportForOxInstantMessaging() { 153 ServiceDiscoveryManager.getInstanceFor(connection()) 154 .addFeature(NAMESPACE_0); 155 } 156 157 /** 158 * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging. 159 * 160 * @param jid {@link BareJid} of the contact in question. 161 * @return true if contact announces support, otherwise false. 162 * 163 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error 164 * @throws SmackException.NotConnectedException if we are not connected 165 * @throws InterruptedException if the thread gets interrupted 166 * @throws SmackException.NoResponseException if the server doesn't respond 167 */ 168 public boolean contactSupportsOxInstantMessaging(BareJid jid) 169 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 170 SmackException.NoResponseException { 171 return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, NAMESPACE_0); 172 } 173 174 /** 175 * Determine, whether a contact announces support for XEP-0374: OpenPGP for XMPP: Instant Messaging. 176 * 177 * @param contact {@link OpenPgpContact} in question. 178 * @return true if contact announces support, otherwise false. 179 * 180 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error 181 * @throws SmackException.NotConnectedException if we are not connected 182 * @throws InterruptedException if the thread is interrupted 183 * @throws SmackException.NoResponseException if the server doesn't respond 184 */ 185 public boolean contactSupportsOxInstantMessaging(OpenPgpContact contact) 186 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 187 SmackException.NoResponseException { 188 return contactSupportsOxInstantMessaging(contact.getJid()); 189 } 190 191 /** 192 * Add an {@link OxMessageListener}. The listener gets notified about incoming {@link OpenPgpMessage}s which 193 * contained an OX-IM message. 194 * 195 * @param listener listener 196 * @return true if the listener gets added, otherwise false. 197 */ 198 public boolean addOxMessageListener(OxMessageListener listener) { 199 return oxMessageListeners.add(listener); 200 } 201 202 /** 203 * Remove an {@link OxMessageListener}. The listener will no longer be notified about OX-IM messages. 204 * 205 * @param listener listener 206 * @return true, if the listener gets removed, otherwise false 207 */ 208 public boolean removeOxMessageListener(OxMessageListener listener) { 209 return oxMessageListeners.remove(listener); 210 } 211 212 /** 213 * Send an OX message to a {@link OpenPgpContact}. The message will be encrypted to all active keys of the contact, 214 * as well as all of our active keys. The message is also signed with our key. 215 * 216 * @param contact contact capable of OpenPGP for XMPP: Instant Messaging. 217 * @param body message body. 218 * 219 * @return {@link OpenPgpMetadata} about the messages encryption + signatures. 220 * 221 * @throws InterruptedException if the thread is interrupted 222 * @throws IOException IO is dangerous 223 * @throws SmackException.NotConnectedException if we are not connected 224 * @throws SmackException.NotLoggedInException if we are not logged in 225 * @throws PGPException PGP is brittle 226 */ 227 public OpenPgpMetadata sendOxMessage(OpenPgpContact contact, CharSequence body) 228 throws InterruptedException, IOException, 229 SmackException.NotConnectedException, SmackException.NotLoggedInException, PGPException { 230 Message message = new Message(contact.getJid()); 231 Message.Body mBody = new Message.Body(null, body.toString()); 232 233 OpenPgpMetadata metadata = addOxMessage(message, contact, Collections.<ExtensionElement>singletonList(mBody)); 234 235 ChatManager.getInstanceFor(connection()).chatWith(contact.getJid().asEntityBareJidIfPossible()).send(message); 236 237 return metadata; 238 } 239 240 /** 241 * Add an OX-IM message element to a message. 242 * 243 * @param message message 244 * @param contact recipient of the message 245 * @param payload payload which will be encrypted and signed 246 * 247 * @return {@link OpenPgpMetadata} about the messages encryption + metadata. 248 * 249 * @throws SmackException.NotLoggedInException in case we are not logged in 250 * @throws PGPException in case something goes wrong during encryption 251 * @throws IOException IO is dangerous (we need to read keys) 252 */ 253 public OpenPgpMetadata addOxMessage(Message message, OpenPgpContact contact, List<ExtensionElement> payload) 254 throws SmackException.NotLoggedInException, PGPException, IOException { 255 return addOxMessage(message, Collections.singleton(contact), payload); 256 } 257 258 /** 259 * Add an OX-IM message element to a message. 260 * 261 * @param message message 262 * @param contacts recipients of the message 263 * @param payload payload which will be encrypted and signed 264 * 265 * @return metadata about the messages encryption + signatures. 266 * 267 * @throws SmackException.NotLoggedInException in case we are not logged in 268 * @throws PGPException in case something goes wrong during encryption 269 * @throws IOException IO is dangerous (we need to read keys) 270 */ 271 public OpenPgpMetadata addOxMessage(Message message, Set<OpenPgpContact> contacts, List<ExtensionElement> payload) 272 throws SmackException.NotLoggedInException, IOException, PGPException { 273 274 HashSet<OpenPgpContact> recipients = new HashSet<>(contacts); 275 OpenPgpContact self = openPgpManager.getOpenPgpSelf(); 276 recipients.add(self); 277 278 OpenPgpElementAndMetadata openPgpElementAndMetadata = signAndEncrypt(recipients, payload); 279 message.addExtension(openPgpElementAndMetadata.getElement()); 280 281 // Set hints on message 282 ExplicitMessageEncryptionElement.set(message, 283 ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.openpgpV0); 284 StoreHint.set(message); 285 setOXBodyHint(message); 286 287 return openPgpElementAndMetadata.getMetadata(); 288 } 289 290 /** 291 * Wrap some {@code payload} into a {@link SigncryptElement}, sign and encrypt it for {@code contacts} and ourselves. 292 * 293 * @param contacts recipients of the message 294 * @param payload payload which will be encrypted and signed 295 * 296 * @return encrypted and signed {@link OpenPgpElement}, along with {@link OpenPgpMetadata} about the 297 * encryption + signatures. 298 * 299 * @throws SmackException.NotLoggedInException in case we are not logged in 300 * @throws IOException IO is dangerous (we need to read keys) 301 * @throws PGPException in case encryption goes wrong 302 */ 303 public OpenPgpElementAndMetadata signAndEncrypt(Set<OpenPgpContact> contacts, List<ExtensionElement> payload) 304 throws SmackException.NotLoggedInException, IOException, PGPException { 305 306 Set<Jid> jids = new HashSet<>(); 307 for (OpenPgpContact contact : contacts) { 308 jids.add(contact.getJid()); 309 } 310 jids.add(openPgpManager.getOpenPgpSelf().getJid()); 311 312 SigncryptElement signcryptElement = new SigncryptElement(jids, payload); 313 OpenPgpElementAndMetadata encrypted = openPgpManager.getOpenPgpProvider().signAndEncrypt(signcryptElement, 314 openPgpManager.getOpenPgpSelf(), contacts); 315 316 return encrypted; 317 } 318 319 /** 320 * Manually decrypt and verify an {@link OpenPgpElement}. 321 * 322 * @param element encrypted, signed {@link OpenPgpElement}. 323 * @param sender sender of the message. 324 * 325 * @return decrypted, verified message 326 * 327 * @throws SmackException.NotLoggedInException In case we are not logged in (we need our jid to access our keys) 328 * @throws PGPException in case of an PGP error 329 * @throws IOException in case of an IO error (reading keys, streams etc) 330 * @throws XmlPullParserException in case that the content of the {@link OpenPgpElement} is not a valid 331 * {@link OpenPgpContentElement} or broken XML. 332 * @throws IllegalArgumentException if the elements content is not a {@link SigncryptElement}. This happens, if the 333 * element likely is not an OX message. 334 */ 335 public OpenPgpMessage decryptAndVerify(OpenPgpElement element, OpenPgpContact sender) 336 throws SmackException.NotLoggedInException, PGPException, IOException, XmlPullParserException { 337 338 OpenPgpMessage decrypted = openPgpManager.decryptOpenPgpElement(element, sender); 339 if (decrypted.getState() != OpenPgpMessage.State.signcrypt) { 340 throw new IllegalArgumentException("Decrypted message does appear to not be an OX message. (State: " + decrypted.getState() + ")"); 341 } 342 343 return decrypted; 344 } 345 346 /** 347 * Set a hint about the message being OX-IM encrypted as body of the message. 348 * 349 * @param message message 350 */ 351 private static void setOXBodyHint(Message message) { 352 message.setBody("This message is encrypted using XEP-0374: OpenPGP for XMPP: Instant Messaging."); 353 } 354 355 private final SigncryptElementReceivedListener signcryptElementReceivedListener = new SigncryptElementReceivedListener() { 356 @Override 357 public void signcryptElementReceived(OpenPgpContact contact, Message originalMessage, SigncryptElement signcryptElement, OpenPgpMetadata metadata) { 358 for (OxMessageListener listener : oxMessageListeners) { 359 listener.newIncomingOxMessage(contact, originalMessage, signcryptElement, metadata); 360 } 361 } 362 }; 363}