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