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