001/** 002 * 003 * Copyright 2017 Florian Schmaus, 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; 018 019import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEYS; 020import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEYS_NOTIFY; 021import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.publishPublicKey; 022 023import java.io.ByteArrayOutputStream; 024import java.io.IOException; 025import java.security.InvalidAlgorithmParameterException; 026import java.security.NoSuchAlgorithmException; 027import java.security.NoSuchProviderException; 028import java.util.Date; 029import java.util.HashSet; 030import java.util.Map; 031import java.util.Set; 032import java.util.WeakHashMap; 033import java.util.logging.Level; 034import java.util.logging.Logger; 035 036import org.jivesoftware.smack.Manager; 037import org.jivesoftware.smack.SmackException; 038import org.jivesoftware.smack.XMPPConnection; 039import org.jivesoftware.smack.XMPPException; 040import org.jivesoftware.smack.chat2.Chat; 041import org.jivesoftware.smack.chat2.ChatManager; 042import org.jivesoftware.smack.chat2.IncomingChatMessageListener; 043import org.jivesoftware.smack.packet.Message; 044import org.jivesoftware.smack.util.Async; 045import org.jivesoftware.smack.util.stringencoder.Base64; 046import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 047import org.jivesoftware.smackx.ox.callback.backup.AskForBackupCodeCallback; 048import org.jivesoftware.smackx.ox.callback.backup.DisplayBackupCodeCallback; 049import org.jivesoftware.smackx.ox.callback.backup.SecretKeyBackupSelectionCallback; 050import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider; 051import org.jivesoftware.smackx.ox.element.CryptElement; 052import org.jivesoftware.smackx.ox.element.OpenPgpContentElement; 053import org.jivesoftware.smackx.ox.element.OpenPgpElement; 054import org.jivesoftware.smackx.ox.element.PubkeyElement; 055import org.jivesoftware.smackx.ox.element.PublicKeysListElement; 056import org.jivesoftware.smackx.ox.element.SecretkeyElement; 057import org.jivesoftware.smackx.ox.element.SignElement; 058import org.jivesoftware.smackx.ox.element.SigncryptElement; 059import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException; 060import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException; 061import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException; 062import org.jivesoftware.smackx.ox.exception.NoBackupFoundException; 063import org.jivesoftware.smackx.ox.listener.CryptElementReceivedListener; 064import org.jivesoftware.smackx.ox.listener.SignElementReceivedListener; 065import org.jivesoftware.smackx.ox.listener.SigncryptElementReceivedListener; 066import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore; 067import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore; 068import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil; 069import org.jivesoftware.smackx.ox.util.SecretKeyBackupHelper; 070import org.jivesoftware.smackx.pep.PepListener; 071import org.jivesoftware.smackx.pep.PepManager; 072import org.jivesoftware.smackx.pubsub.EventElement; 073import org.jivesoftware.smackx.pubsub.ItemsExtension; 074import org.jivesoftware.smackx.pubsub.LeafNode; 075import org.jivesoftware.smackx.pubsub.PayloadItem; 076import org.jivesoftware.smackx.pubsub.PubSubException; 077import org.jivesoftware.smackx.pubsub.PubSubFeature; 078 079import org.bouncycastle.openpgp.PGPException; 080import org.bouncycastle.openpgp.PGPPublicKey; 081import org.bouncycastle.openpgp.PGPPublicKeyRing; 082import org.bouncycastle.openpgp.PGPSecretKey; 083import org.bouncycastle.openpgp.PGPSecretKeyRing; 084import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; 085import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; 086import org.jxmpp.jid.BareJid; 087import org.jxmpp.jid.EntityBareJid; 088import org.pgpainless.key.OpenPgpV4Fingerprint; 089import org.pgpainless.key.collection.PGPKeyRing; 090import org.pgpainless.key.protection.SecretKeyRingProtector; 091import org.pgpainless.util.BCUtil; 092import org.xmlpull.v1.XmlPullParserException; 093 094/** 095 * Entry point for Smacks API for OpenPGP for XMPP. 096 * 097 * <h2>Setup</h2> 098 * 099 * In order to use OpenPGP for XMPP in Smack, just follow the following procedure.<br> 100 * <br> 101 * First, acquire an instance of the {@link OpenPgpManager} for your {@link XMPPConnection} using 102 * {@link #getInstanceFor(XMPPConnection)}. 103 * 104 * <pre> 105 * {@code 106 * OpenPgpManager openPgpManager = OpenPgpManager.getInstanceFor(connection); 107 * } 108 * </pre> 109 * 110 * You also need an {@link OpenPgpProvider}, as well as an {@link OpenPgpStore}. 111 * The provider must be registered using {@link #setOpenPgpProvider(OpenPgpProvider)}. 112 * 113 * <pre> 114 * {@code 115 * OpenPgpStore store = new FileBasedOpenPgpStore(storePath); 116 * OpenPgpProvider provider = new PainlessOpenPgpProvider(connection, store); 117 * openPgpManager.setOpenPgpProvider(provider); 118 * } 119 * </pre> 120 * 121 * It is also advised to register a custom {@link SecretKeyRingProtector} using 122 * {@link OpenPgpStore#setKeyRingProtector(SecretKeyRingProtector)} in order to be able to handle password protected 123 * secret keys.<br> 124 * <br> 125 * Speaking of keys, you can now check, if you have any keys available in your {@link OpenPgpStore} by doing 126 * {@link #hasSecretKeysAvailable()}.<br> 127 * <br> 128 * If you do, you can now announce support for OX and publish those keys using {@link #announceSupportAndPublish()}.<br> 129 * <br> 130 * Otherwise, you can either generate fresh keys using {@link #generateAndImportKeyPair(BareJid)}, 131 * or try to restore a secret key backup from your private PubSub node by doing 132 * {@link #restoreSecretKeyServerBackup(AskForBackupCodeCallback)}.<br> 133 * <br> 134 * In any case you should still do an {@link #announceSupportAndPublish()} afterwards. 135 * <br> 136 * <br> 137 * Contacts are represented by {@link OpenPgpContact}s in the context of OpenPGP for XMPP. You can get those by using 138 * {@link #getOpenPgpContact(EntityBareJid)}. The main function of {@link OpenPgpContact}s is to bundle information 139 * about the OpenPGP capabilities of a contact in one spot. The pendant to the {@link OpenPgpContact} is the 140 * {@link OpenPgpSelf}, which encapsulates your own OpenPGP identity. Both classes can be used to acquire information 141 * about the OpenPGP keys of a user. 142 * 143 * <h2>Elements</h2> 144 * 145 * OpenPGP for XMPP defines multiple different element classes which contain the users messages. 146 * The outermost element is the {@link OpenPgpElement}, which contains an OpenPGP encrypted content element. 147 * 148 * The content can be either a {@link SignElement}, {@link CryptElement} or {@link SigncryptElement}, depending on the use-case. 149 * Those content elements contain the actual payload. If an {@link OpenPgpElement} is decrypted, it will be returned in 150 * form of an {@link OpenPgpMessage}, which represents the decrypted message + metadata. 151 * 152 * @see <a href="https://xmpp.org/extensions/xep-0373.html"> 153 * XEP-0373: OpenPGP for XMPP</a> 154 */ 155public final class OpenPgpManager extends Manager { 156 157 private static final Logger LOGGER = Logger.getLogger(OpenPgpManager.class.getName()); 158 159 /** 160 * Map of instances. 161 */ 162 private static final Map<XMPPConnection, OpenPgpManager> INSTANCES = new WeakHashMap<>(); 163 164 /** 165 * {@link OpenPgpProvider} responsible for processing keys, encrypting and decrypting messages and so on. 166 */ 167 private OpenPgpProvider provider; 168 169 private final PepManager pepManager; 170 171 private final Set<SigncryptElementReceivedListener> signcryptElementReceivedListeners = new HashSet<>(); 172 private final Set<SignElementReceivedListener> signElementReceivedListeners = new HashSet<>(); 173 private final Set<CryptElementReceivedListener> cryptElementReceivedListeners = new HashSet<>(); 174 175 /** 176 * Private constructor to avoid instantiation without putting the object into {@code INSTANCES}. 177 * 178 * @param connection xmpp connection. 179 */ 180 private OpenPgpManager(XMPPConnection connection) { 181 super(connection); 182 ChatManager.getInstanceFor(connection).addIncomingListener(incomingOpenPgpMessageListener); 183 pepManager = PepManager.getInstanceFor(connection); 184 } 185 186 /** 187 * Get the instance of the {@link OpenPgpManager} which belongs to the {@code connection}. 188 * 189 * @param connection xmpp connection. 190 * @return instance of the manager. 191 */ 192 public static OpenPgpManager getInstanceFor(XMPPConnection connection) { 193 OpenPgpManager manager = INSTANCES.get(connection); 194 if (manager == null) { 195 manager = new OpenPgpManager(connection); 196 INSTANCES.put(connection, manager); 197 } 198 return manager; 199 } 200 201 /** 202 * Return our own {@link BareJid}. 203 * 204 * @return our bareJid 205 * 206 * @throws SmackException.NotLoggedInException in case our connection is not logged in, which means our BareJid is unknown. 207 */ 208 public BareJid getJidOrThrow() throws SmackException.NotLoggedInException { 209 throwIfNotAuthenticated(); 210 return connection().getUser().asEntityBareJidOrThrow(); 211 } 212 213 /** 214 * Set the {@link OpenPgpProvider} which will be used to process incoming OpenPGP elements, 215 * as well as to execute cryptographic operations. 216 * 217 * @param provider OpenPgpProvider. 218 */ 219 public void setOpenPgpProvider(OpenPgpProvider provider) { 220 this.provider = provider; 221 } 222 223 public OpenPgpProvider getOpenPgpProvider() { 224 return provider; 225 } 226 227 /** 228 * Get our OpenPGP self. 229 * 230 * @return self 231 * @throws SmackException.NotLoggedInException if we are not logged in 232 */ 233 public OpenPgpSelf getOpenPgpSelf() throws SmackException.NotLoggedInException { 234 throwIfNoProviderSet(); 235 return new OpenPgpSelf(getJidOrThrow(), provider.getStore()); 236 } 237 238 /** 239 * Generate a fresh OpenPGP key pair, given we don't have one already. 240 * Publish the public key to the Public Key Node and update the Public Key Metadata Node with our keys fingerprint. 241 * Lastly register a {@link PepListener} which listens for updates to Public Key Metadata Nodes. 242 * 243 * @throws NoSuchAlgorithmException if we are missing an algorithm to generate a fresh key pair. 244 * @throws NoSuchProviderException if we are missing a suitable {@link java.security.Provider}. 245 * @throws InterruptedException if the thread gets interrupted. 246 * @throws PubSubException.NotALeafNodeException if one of the PubSub nodes is not a {@link LeafNode}. 247 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 248 * @throws SmackException.NotConnectedException if we are not connected. 249 * @throws SmackException.NoResponseException if the server doesn't respond. 250 * @throws IOException IO is dangerous. 251 * @throws InvalidAlgorithmParameterException if illegal algorithm parameters are used for key generation. 252 * @throws SmackException.NotLoggedInException if we are not logged in. 253 * @throws PGPException if something goes wrong during key loading/generating 254 */ 255 public void announceSupportAndPublish() 256 throws NoSuchAlgorithmException, NoSuchProviderException, InterruptedException, 257 PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, 258 SmackException.NotConnectedException, SmackException.NoResponseException, IOException, 259 InvalidAlgorithmParameterException, SmackException.NotLoggedInException, PGPException { 260 throwIfNoProviderSet(); 261 throwIfNotAuthenticated(); 262 263 OpenPgpV4Fingerprint primaryFingerprint = getOurFingerprint(); 264 265 if (primaryFingerprint == null) { 266 primaryFingerprint = generateAndImportKeyPair(getJidOrThrow()); 267 } 268 269 // Create <pubkey/> element 270 PubkeyElement pubkeyElement; 271 try { 272 pubkeyElement = createPubkeyElement(getJidOrThrow(), primaryFingerprint, new Date()); 273 } catch (MissingOpenPgpKeyException e) { 274 throw new AssertionError("Cannot publish our public key, since it is missing (MUST NOT happen!)"); 275 } 276 277 // publish it 278 publishPublicKey(pepManager, pubkeyElement, primaryFingerprint); 279 280 // Subscribe to public key changes 281 PepManager.getInstanceFor(connection()).addPepListener(metadataListener); 282 ServiceDiscoveryManager.getInstanceFor(connection()) 283 .addFeature(PEP_NODE_PUBLIC_KEYS_NOTIFY); 284 } 285 286 /** 287 * Generate a fresh OpenPGP key pair and import it. 288 * 289 * @param ourJid our {@link BareJid}. 290 * @return {@link OpenPgpV4Fingerprint} of the generated key. 291 * @throws NoSuchAlgorithmException if the JVM doesn't support one of the used algorithms. 292 * @throws InvalidAlgorithmParameterException if the used algorithm parameters are invalid. 293 * @throws NoSuchProviderException if we are missing a cryptographic provider. 294 * @throws PGPException PGP is brittle. 295 * @throws IOException IO is dangerous. 296 */ 297 public OpenPgpV4Fingerprint generateAndImportKeyPair(BareJid ourJid) 298 throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException, 299 PGPException, IOException { 300 301 throwIfNoProviderSet(); 302 OpenPgpStore store = provider.getStore(); 303 PGPKeyRing keys = store.generateKeyRing(ourJid); 304 try { 305 store.importSecretKey(ourJid, keys.getSecretKeys()); 306 store.importPublicKey(ourJid, keys.getPublicKeys()); 307 } catch (MissingUserIdOnKeyException e) { 308 // This should never throw, since we set our jid literally one line above this comment. 309 throw new AssertionError(e); 310 } 311 312 OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(keys.getSecretKeys()); 313 314 store.setTrust(ourJid, fingerprint, OpenPgpTrustStore.Trust.trusted); 315 316 return fingerprint; 317 } 318 319 /** 320 * Return the upper-case hex encoded OpenPGP v4 fingerprint of our key pair. 321 * 322 * @return fingerprint. 323 * @throws SmackException.NotLoggedInException in case we are not logged in. 324 * @throws IOException IO is dangerous. 325 * @throws PGPException PGP is brittle. 326 */ 327 public OpenPgpV4Fingerprint getOurFingerprint() 328 throws SmackException.NotLoggedInException, IOException, PGPException { 329 return getOpenPgpSelf().getSigningKeyFingerprint(); 330 } 331 332 /** 333 * Return an OpenPGP capable contact. 334 * This object can be used as an entry point to OpenPGP related API. 335 * 336 * @param jid {@link BareJid} of the contact. 337 * @return {@link OpenPgpContact}. 338 */ 339 public OpenPgpContact getOpenPgpContact(EntityBareJid jid) { 340 throwIfNoProviderSet(); 341 return provider.getStore().getOpenPgpContact(jid); 342 } 343 344 /** 345 * Return true, if we have a secret key available, otherwise false. 346 * 347 * @return true if secret key available 348 * 349 * @throws SmackException.NotLoggedInException If we are not logged in (we need to know our jid in order to look up 350 * our keys in the key store. 351 * @throws PGPException in case the keys in the store are damaged somehow. 352 * @throws IOException IO is dangerous. 353 */ 354 public boolean hasSecretKeysAvailable() throws SmackException.NotLoggedInException, PGPException, IOException { 355 throwIfNoProviderSet(); 356 return getOpenPgpSelf().hasSecretKeyAvailable(); 357 } 358 359 /** 360 * Determine, if we can sync secret keys using private PEP nodes as described in the XEP. 361 * Requirements on the server side are support for PEP and support for the whitelist access model of PubSub. 362 * 363 * @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a> 364 * 365 * @param connection XMPP connection 366 * @return true, if the server supports secret key backups, otherwise false. 367 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 368 * @throws SmackException.NotConnectedException if we are not connected. 369 * @throws InterruptedException if the thread is interrupted. 370 * @throws SmackException.NoResponseException if the server doesn't respond. 371 */ 372 public static boolean serverSupportsSecretKeyBackups(XMPPConnection connection) 373 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 374 SmackException.NoResponseException { 375 return ServiceDiscoveryManager.getInstanceFor(connection) 376 .serverSupportsFeature(PubSubFeature.access_whitelist.toString()); 377 } 378 379 /** 380 * Remove the metadata listener. This method is mainly used in tests. 381 */ 382 public void stopMetadataListener() { 383 PepManager.getInstanceFor(connection()).removePepListener(metadataListener); 384 } 385 386 /** 387 * Upload the encrypted secret key to a private PEP node. 388 * 389 * @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a> 390 * 391 * @param displayCodeCallback callback, which will receive the backup password used to encrypt the secret key. 392 * @param selectKeyCallback callback, which will receive the users choice of which keys will be backed up. 393 * @throws InterruptedException if the thread is interrupted. 394 * @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}. 395 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 396 * @throws SmackException.NotConnectedException if we are not connected. 397 * @throws SmackException.NoResponseException if the server doesn't respond. 398 * @throws SmackException.NotLoggedInException if we are not logged in. 399 * @throws IOException IO is dangerous. 400 * @throws SmackException.FeatureNotSupportedException if the server doesn't support the PubSub whitelist access model. 401 * @throws PGPException PGP is brittle 402 * @throws MissingOpenPgpKeyException in case we have no OpenPGP key pair to back up. 403 */ 404 public void backupSecretKeyToServer(DisplayBackupCodeCallback displayCodeCallback, 405 SecretKeyBackupSelectionCallback selectKeyCallback) 406 throws InterruptedException, PubSubException.NotALeafNodeException, 407 XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, 408 SmackException.NotLoggedInException, IOException, 409 SmackException.FeatureNotSupportedException, PGPException, MissingOpenPgpKeyException { 410 throwIfNoProviderSet(); 411 throwIfNotAuthenticated(); 412 413 BareJid ownJid = connection().getUser().asBareJid(); 414 415 String backupCode = SecretKeyBackupHelper.generateBackupPassword(); 416 417 PGPSecretKeyRingCollection secretKeyRings = provider.getStore().getSecretKeysOf(ownJid); 418 419 Set<OpenPgpV4Fingerprint> availableKeyPairs = new HashSet<>(); 420 for (PGPSecretKeyRing ring : secretKeyRings) { 421 availableKeyPairs.add(new OpenPgpV4Fingerprint(ring)); 422 } 423 424 Set<OpenPgpV4Fingerprint> selectedKeyPairs = selectKeyCallback.selectKeysToBackup(availableKeyPairs); 425 426 SecretkeyElement secretKey = SecretKeyBackupHelper.createSecretkeyElement(provider, ownJid, selectedKeyPairs, backupCode); 427 428 OpenPgpPubSubUtil.depositSecretKey(connection(), secretKey); 429 displayCodeCallback.displayBackupCode(backupCode); 430 } 431 432 /** 433 * Delete the private {@link LeafNode} containing our secret key backup. 434 * 435 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 436 * @throws SmackException.NotConnectedException if we are not connected. 437 * @throws InterruptedException if the thread gets interrupted. 438 * @throws SmackException.NoResponseException if the server doesn't respond. 439 * @throws SmackException.NotLoggedInException if we are not logged in. 440 */ 441 public void deleteSecretKeyServerBackup() 442 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 443 SmackException.NoResponseException, SmackException.NotLoggedInException { 444 throwIfNotAuthenticated(); 445 OpenPgpPubSubUtil.deleteSecretKeyNode(pepManager); 446 } 447 448 /** 449 * Fetch a secret key backup from the server and try to restore a selected secret key from it. 450 * 451 * @param codeCallback callback for prompting the user to provide the secret backup code. 452 * @return fingerprint of the restored secret key 453 * 454 * @throws InterruptedException if the thread gets interrupted. 455 * @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}. 456 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 457 * @throws SmackException.NotConnectedException if we are not connected. 458 * @throws SmackException.NoResponseException if the server doesn't respond. 459 * @throws InvalidBackupCodeException if the user-provided backup code is invalid. 460 * @throws SmackException.NotLoggedInException if we are not logged in 461 * @throws IOException IO is dangerous 462 * @throws MissingUserIdOnKeyException if the key that is to be imported is missing a user-id with our jid 463 * @throws NoBackupFoundException if no secret key backup has been found 464 * @throws PGPException in case the restored secret key is damaged. 465 */ 466 public OpenPgpV4Fingerprint restoreSecretKeyServerBackup(AskForBackupCodeCallback codeCallback) 467 throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, 468 SmackException.NotConnectedException, SmackException.NoResponseException, 469 InvalidBackupCodeException, SmackException.NotLoggedInException, IOException, MissingUserIdOnKeyException, 470 NoBackupFoundException, PGPException { 471 throwIfNoProviderSet(); 472 throwIfNotAuthenticated(); 473 SecretkeyElement backup = OpenPgpPubSubUtil.fetchSecretKey(pepManager); 474 if (backup == null) { 475 throw new NoBackupFoundException(); 476 } 477 478 String backupCode = codeCallback.askForBackupCode(); 479 480 PGPSecretKeyRing secretKeys = SecretKeyBackupHelper.restoreSecretKeyBackup(backup, backupCode); 481 provider.getStore().importSecretKey(getJidOrThrow(), secretKeys); 482 provider.getStore().importPublicKey(getJidOrThrow(), BCUtil.publicKeyRingFromSecretKeyRing(secretKeys)); 483 484 ByteArrayOutputStream buffer = new ByteArrayOutputStream(2048); 485 for (PGPSecretKey sk : secretKeys) { 486 PGPPublicKey pk = sk.getPublicKey(); 487 if (pk != null) pk.encode(buffer); 488 } 489 PGPPublicKeyRing publicKeys = new PGPPublicKeyRing(buffer.toByteArray(), new BcKeyFingerprintCalculator()); 490 provider.getStore().importPublicKey(getJidOrThrow(), publicKeys); 491 492 return new OpenPgpV4Fingerprint(secretKeys); 493 } 494 495 /* 496 Private stuff. 497 */ 498 499 /** 500 * {@link PepListener} that listens for changes to the OX public keys metadata node. 501 * 502 * @see <a href="https://xmpp.org/extensions/xep-0373.html#pubsub-notifications">XEP-0373 §4.4</a> 503 */ 504 private final PepListener metadataListener = new PepListener() { 505 @Override 506 public void eventReceived(final EntityBareJid from, final EventElement event, final Message message) { 507 if (PEP_NODE_PUBLIC_KEYS.equals(event.getEvent().getNode())) { 508 final BareJid contact = from.asBareJid(); 509 LOGGER.log(Level.INFO, "Received OpenPGP metadata update from " + contact); 510 Async.go(new Runnable() { 511 @Override 512 public void run() { 513 ItemsExtension items = (ItemsExtension) event.getExtensions().get(0); 514 PayloadItem<?> payload = (PayloadItem) items.getItems().get(0); 515 PublicKeysListElement listElement = (PublicKeysListElement) payload.getPayload(); 516 517 processPublicKeysListElement(from, listElement); 518 } 519 }, "ProcessOXMetadata"); 520 } 521 } 522 }; 523 524 private void processPublicKeysListElement(BareJid contact, PublicKeysListElement listElement) { 525 OpenPgpContact openPgpContact = getOpenPgpContact(contact.asEntityBareJidIfPossible()); 526 try { 527 openPgpContact.updateKeys(connection(), listElement); 528 } catch (Exception e) { 529 LOGGER.log(Level.WARNING, "Could not update contacts keys", e); 530 } 531 } 532 533 /** 534 * Decrypt and or verify an {@link OpenPgpElement} and return the decrypted {@link OpenPgpMessage}. 535 * 536 * @param element {@link OpenPgpElement} containing the message. 537 * @param sender {@link OpenPgpContact} who sent the message. 538 * 539 * @return decrypted and/or verified message 540 * 541 * @throws SmackException.NotLoggedInException in case we aren't logged in (we need to know our jid) 542 * @throws IOException IO error (reading keys, streams etc) 543 * @throws PGPException in case of an PGP error 544 */ 545 public OpenPgpMessage decryptOpenPgpElement(OpenPgpElement element, OpenPgpContact sender) 546 throws SmackException.NotLoggedInException, IOException, PGPException { 547 return provider.decryptAndOrVerify(element, getOpenPgpSelf(), sender); 548 } 549 550 private final IncomingChatMessageListener incomingOpenPgpMessageListener = 551 new IncomingChatMessageListener() { 552 @Override 553 public void newIncomingMessage(final EntityBareJid from, final Message message, Chat chat) { 554 Async.go(new Runnable() { 555 @Override 556 public void run() { 557 OpenPgpElement element = message.getExtension(OpenPgpElement.ELEMENT, OpenPgpElement.NAMESPACE); 558 if (element == null) { 559 // Message does not contain an OpenPgpElement -> discard 560 return; 561 } 562 563 OpenPgpContact contact = getOpenPgpContact(from); 564 565 OpenPgpMessage decrypted = null; 566 OpenPgpContentElement contentElement = null; 567 try { 568 decrypted = decryptOpenPgpElement(element, contact); 569 contentElement = decrypted.getOpenPgpContentElement(); 570 } catch (PGPException e) { 571 LOGGER.log(Level.WARNING, "Could not decrypt incoming OpenPGP encrypted message", e); 572 } catch (XmlPullParserException | IOException e) { 573 LOGGER.log(Level.WARNING, "Invalid XML content of incoming OpenPGP encrypted message", e); 574 } catch (SmackException.NotLoggedInException e) { 575 LOGGER.log(Level.WARNING, "Cannot determine our JID, since we are not logged in.", e); 576 } 577 578 if (contentElement instanceof SigncryptElement) { 579 for (SigncryptElementReceivedListener l : signcryptElementReceivedListeners) { 580 l.signcryptElementReceived(contact, message, (SigncryptElement) contentElement, decrypted.getMetadata()); 581 } 582 return; 583 } 584 585 if (contentElement instanceof SignElement) { 586 for (SignElementReceivedListener l : signElementReceivedListeners) { 587 l.signElementReceived(contact, message, (SignElement) contentElement, decrypted.getMetadata()); 588 } 589 return; 590 } 591 592 if (contentElement instanceof CryptElement) { 593 for (CryptElementReceivedListener l : cryptElementReceivedListeners) { 594 l.cryptElementReceived(contact, message, (CryptElement) contentElement, decrypted.getMetadata()); 595 } 596 return; 597 } 598 599 else { 600 throw new AssertionError("Invalid element received: " + contentElement.getClass().getName()); 601 } 602 } 603 }); 604 } 605 }; 606 607 /** 608 * Create a {@link PubkeyElement} which contains the OpenPGP public key of {@code owner} which belongs to 609 * the {@link OpenPgpV4Fingerprint} {@code fingerprint}. 610 * 611 * @param owner owner of the public key 612 * @param fingerprint fingerprint of the key 613 * @param date date of creation of the element 614 * @return {@link PubkeyElement} containing the key 615 * 616 * @throws MissingOpenPgpKeyException if the public key notated by the fingerprint cannot be found 617 */ 618 private PubkeyElement createPubkeyElement(BareJid owner, 619 OpenPgpV4Fingerprint fingerprint, 620 Date date) 621 throws MissingOpenPgpKeyException, IOException, PGPException { 622 PGPPublicKeyRing ring = provider.getStore().getPublicKeyRing(owner, fingerprint); 623 if (ring != null) { 624 byte[] keyBytes = ring.getEncoded(true); 625 return createPubkeyElement(keyBytes, date); 626 } 627 throw new MissingOpenPgpKeyException(owner, fingerprint); 628 } 629 630 /** 631 * Create a {@link PubkeyElement} which contains the given {@code data} base64 encoded. 632 * 633 * @param bytes byte representation of an OpenPGP public key 634 * @param date date of creation of the element 635 * @return {@link PubkeyElement} containing the key 636 */ 637 private static PubkeyElement createPubkeyElement(byte[] bytes, Date date) { 638 return new PubkeyElement(new PubkeyElement.PubkeyDataElement(Base64.encode(bytes)), date); 639 } 640 641 /** 642 * Register a {@link SigncryptElementReceivedListener} on the {@link OpenPgpManager}. 643 * That listener will get informed whenever a {@link SigncryptElement} has been received and successfully decrypted. 644 * 645 * Note: This method is not intended for clients to listen for incoming {@link SigncryptElement}s. 646 * Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as 647 * OpenPGP for XMPP: Instant Messaging. 648 * 649 * @param listener listener that gets registered 650 */ 651 public void registerSigncryptReceivedListener(SigncryptElementReceivedListener listener) { 652 signcryptElementReceivedListeners.add(listener); 653 } 654 655 /** 656 * Unregister a prior registered {@link SigncryptElementReceivedListener}. That listener will no longer get 657 * informed about incoming decrypted {@link SigncryptElement}s. 658 * 659 * @param listener listener that gets unregistered 660 */ 661 void unregisterSigncryptElementReceivedListener(SigncryptElementReceivedListener listener) { 662 signcryptElementReceivedListeners.remove(listener); 663 } 664 665 /** 666 * Register a {@link SignElementReceivedListener} on the {@link OpenPgpManager}. 667 * That listener will get informed whenever a {@link SignElement} has been received and successfully verified. 668 * 669 * Note: This method is not intended for clients to listen for incoming {@link SignElement}s. 670 * Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as 671 * OpenPGP for XMPP: Instant Messaging. 672 * 673 * @param listener listener that gets registered 674 */ 675 void registerSignElementReceivedListener(SignElementReceivedListener listener) { 676 signElementReceivedListeners.add(listener); 677 } 678 679 /** 680 * Unregister a prior registered {@link SignElementReceivedListener}. That listener will no longer get 681 * informed about incoming decrypted {@link SignElement}s. 682 * 683 * @param listener listener that gets unregistered 684 */ 685 void unregisterSignElementReceivedListener(SignElementReceivedListener listener) { 686 signElementReceivedListeners.remove(listener); 687 } 688 689 /** 690 * Register a {@link CryptElementReceivedListener} on the {@link OpenPgpManager}. 691 * That listener will get informed whenever a {@link CryptElement} has been received and successfully decrypted. 692 * 693 * Note: This method is not intended for clients to listen for incoming {@link CryptElement}s. 694 * Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as 695 * OpenPGP for XMPP: Instant Messaging. 696 * 697 * @param listener listener that gets registered 698 */ 699 void registerCryptElementReceivedListener(CryptElementReceivedListener listener) { 700 cryptElementReceivedListeners.add(listener); 701 } 702 703 /** 704 * Unregister a prior registered {@link CryptElementReceivedListener}. That listener will no longer get 705 * informed about incoming decrypted {@link CryptElement}s. 706 * 707 * @param listener listener that gets unregistered 708 */ 709 void unregisterCryptElementReceivedListener(CryptElementReceivedListener listener) { 710 cryptElementReceivedListeners.remove(listener); 711 } 712 713 /** 714 * Throw an {@link IllegalStateException} if no {@link OpenPgpProvider} is set. 715 * The OpenPgpProvider is used to process information related to RFC-4880. 716 */ 717 private void throwIfNoProviderSet() { 718 if (provider == null) { 719 throw new IllegalStateException("No OpenPgpProvider set!"); 720 } 721 } 722 723 /** 724 * Throw a {@link org.jivesoftware.smack.SmackException.NotLoggedInException} if the {@link XMPPConnection} of this 725 * manager is not authenticated at this point. 726 * 727 * @throws SmackException.NotLoggedInException if we are not authenticated 728 */ 729 private void throwIfNotAuthenticated() throws SmackException.NotLoggedInException { 730 if (!connection().isAuthenticated()) { 731 throw new SmackException.NotLoggedInException(); 732 } 733 } 734}