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.util; 018 019import java.lang.reflect.Constructor; 020import java.lang.reflect.Field; 021import java.lang.reflect.InvocationTargetException; 022import java.util.Date; 023import java.util.List; 024import java.util.Map; 025import java.util.logging.Level; 026import java.util.logging.Logger; 027 028import org.jivesoftware.smack.SmackException; 029import org.jivesoftware.smack.XMPPConnection; 030import org.jivesoftware.smack.XMPPException; 031import org.jivesoftware.smack.packet.StanzaError; 032 033import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 034import org.jivesoftware.smackx.ox.OpenPgpManager; 035import org.jivesoftware.smackx.ox.element.PubkeyElement; 036import org.jivesoftware.smackx.ox.element.PublicKeysListElement; 037import org.jivesoftware.smackx.ox.element.SecretkeyElement; 038import org.jivesoftware.smackx.pep.PepManager; 039import org.jivesoftware.smackx.pubsub.AccessModel; 040import org.jivesoftware.smackx.pubsub.Item; 041import org.jivesoftware.smackx.pubsub.LeafNode; 042import org.jivesoftware.smackx.pubsub.Node; 043import org.jivesoftware.smackx.pubsub.PayloadItem; 044import org.jivesoftware.smackx.pubsub.PubSubException; 045import org.jivesoftware.smackx.pubsub.PubSubManager; 046import org.jivesoftware.smackx.pubsub.form.ConfigureForm; 047import org.jivesoftware.smackx.pubsub.form.FillableConfigureForm; 048 049import org.jxmpp.jid.BareJid; 050import org.pgpainless.key.OpenPgpV4Fingerprint; 051 052public class OpenPgpPubSubUtil { 053 054 private static final Logger LOGGER = Logger.getLogger(OpenPgpPubSubUtil.class.getName()); 055 056 /** 057 * Name of the OX metadata node. 058 * 059 * @see <a href="https://xmpp.org/extensions/xep-0373.html#announcing-pubkey-list">XEP-0373 §4.2</a> 060 */ 061 public static final String PEP_NODE_PUBLIC_KEYS = "urn:xmpp:openpgp:0:public-keys"; 062 063 /** 064 * Name of the OX secret key node. 065 */ 066 public static final String PEP_NODE_SECRET_KEY = "urn:xmpp:openpgp:0:secret-key"; 067 068 /** 069 * Feature to be announced using the {@link ServiceDiscoveryManager} to subscribe to the OX metadata node. 070 * 071 * @see <a href="https://xmpp.org/extensions/xep-0373.html#pubsub-notifications">XEP-0373 §4.4</a> 072 */ 073 public static final String PEP_NODE_PUBLIC_KEYS_NOTIFY = PEP_NODE_PUBLIC_KEYS + "+notify"; 074 075 /** 076 * Name of the OX public key node, which contains the key with id {@code id}. 077 * 078 * @param id upper case hex encoded OpenPGP v4 fingerprint of the key. 079 * @return PEP node name. 080 */ 081 public static String PEP_NODE_PUBLIC_KEY(OpenPgpV4Fingerprint id) { 082 return PEP_NODE_PUBLIC_KEYS + ":" + id; 083 } 084 085 /** 086 * Query the access model of {@code node}. If it is different from {@code accessModel}, change the access model 087 * of the node to {@code accessModel}. 088 * 089 * @see <a href="https://xmpp.org/extensions/xep-0060.html#accessmodels">XEP-0060 §4.5 - Node Access Models</a> 090 * 091 * @param node {@link LeafNode} whose PubSub access model we want to change 092 * @param accessModel new access model. 093 * 094 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 095 * @throws SmackException.NotConnectedException if we are not connected. 096 * @throws InterruptedException if the thread is interrupted. 097 * @throws SmackException.NoResponseException if the server doesn't respond. 098 */ 099 public static void changeAccessModelIfNecessary(LeafNode node, AccessModel accessModel) 100 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 101 SmackException.NoResponseException { 102 ConfigureForm current = node.getNodeConfiguration(); 103 if (current.getAccessModel() != accessModel) { 104 FillableConfigureForm updateConfig = current.getFillableForm(); 105 updateConfig.setAccessModel(accessModel); 106 node.sendConfigurationForm(updateConfig); 107 } 108 } 109 110 /** 111 * Publish the users OpenPGP public key to the public key node if necessary. 112 * Also announce the key to other users by updating the metadata node. 113 * 114 * @see <a href="https://xmpp.org/extensions/xep-0373.html#announcing-pubkey">XEP-0373 §4.1</a> 115 * 116 * @param pepManager The PEP manager. 117 * @param pubkeyElement {@link PubkeyElement} containing the public key 118 * @param fingerprint fingerprint of the public key 119 * 120 * @throws InterruptedException if the thread gets interrupted. 121 * @throws PubSubException.NotALeafNodeException if either the metadata node or the public key node is not a 122 * {@link LeafNode}. 123 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 124 * @throws SmackException.NotConnectedException if we are not connected. 125 * @throws SmackException.NoResponseException if the server doesn't respond. 126 */ 127 @SuppressWarnings("JavaUtilDate") 128 public static void publishPublicKey(PepManager pepManager, PubkeyElement pubkeyElement, OpenPgpV4Fingerprint fingerprint) 129 throws InterruptedException, PubSubException.NotALeafNodeException, 130 XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { 131 132 String keyNodeName = PEP_NODE_PUBLIC_KEY(fingerprint); 133 PubSubManager pm = pepManager.getPepPubSubManager(); 134 135 // Check if key available at data node 136 // If not, publish key to data node 137 LeafNode keyNode = pm.getOrCreateLeafNode(keyNodeName); 138 changeAccessModelIfNecessary(keyNode, AccessModel.open); 139 List<Item> items = keyNode.getItems(1); 140 if (items.isEmpty()) { 141 LOGGER.log(Level.FINE, "Node " + keyNodeName + " is empty. Publish."); 142 keyNode.publish(new PayloadItem<>(pubkeyElement)); 143 } else { 144 LOGGER.log(Level.FINE, "Node " + keyNodeName + " already contains key. Skip."); 145 } 146 147 // Fetch IDs from metadata node 148 LeafNode metadataNode = pm.getOrCreateLeafNode(PEP_NODE_PUBLIC_KEYS); 149 changeAccessModelIfNecessary(metadataNode, AccessModel.open); 150 List<PayloadItem<PublicKeysListElement>> metadataItems = metadataNode.getItems(1); 151 152 PublicKeysListElement.Builder builder = PublicKeysListElement.builder(); 153 if (!metadataItems.isEmpty() && metadataItems.get(0).getPayload() != null) { 154 // Add old entries back to list. 155 PublicKeysListElement publishedList = metadataItems.get(0).getPayload(); 156 for (PublicKeysListElement.PubkeyMetadataElement meta : publishedList.getMetadata().values()) { 157 builder.addMetadata(meta); 158 } 159 } 160 builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fingerprint, new Date())); 161 162 // Publish IDs to metadata node 163 metadataNode.publish(new PayloadItem<>(builder.build())); 164 } 165 166 /** 167 * Consult the public key metadata node and fetch a list of all of our published OpenPGP public keys. 168 * 169 * @see <a href="https://xmpp.org/extensions/xep-0373.html#discover-pubkey-list"> 170 * XEP-0373 §4.3: Discovering Public Keys of a User</a> 171 * 172 * @param connection XMPP connection 173 * @return content of our metadata node. 174 * 175 * @throws InterruptedException if the thread gets interrupted. 176 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol exception. 177 * @throws PubSubException.NotAPubSubNodeException in case the queried entity is not a PubSub node 178 * @throws PubSubException.NotALeafNodeException in case the queried node is not a {@link LeafNode} 179 * @throws SmackException.NotConnectedException in case we are not connected 180 * @throws SmackException.NoResponseException in case the server doesn't respond 181 */ 182 public static PublicKeysListElement fetchPubkeysList(XMPPConnection connection) 183 throws InterruptedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException, 184 PubSubException.NotALeafNodeException, SmackException.NotConnectedException, SmackException.NoResponseException { 185 return fetchPubkeysList(connection, null); 186 } 187 188 189 /** 190 * Consult the public key metadata node of {@code contact} to fetch the list of their published OpenPGP public keys. 191 * 192 * @see <a href="https://xmpp.org/extensions/xep-0373.html#discover-pubkey-list"> 193 * XEP-0373 §4.3: Discovering Public Keys of a User</a> 194 * 195 * @param connection XMPP connection 196 * @param contact {@link BareJid} of the user we want to fetch the list from. 197 * @return content of {@code contact}'s metadata node. 198 * 199 * @throws InterruptedException if the thread gets interrupted. 200 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol exception. 201 * @throws SmackException.NoResponseException in case the server doesn't respond 202 * @throws PubSubException.NotALeafNodeException in case the queried node is not a {@link LeafNode} 203 * @throws SmackException.NotConnectedException in case we are not connected 204 * @throws PubSubException.NotAPubSubNodeException in case the queried entity is not a PubSub node 205 */ 206 public static PublicKeysListElement fetchPubkeysList(XMPPConnection connection, BareJid contact) 207 throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NoResponseException, 208 PubSubException.NotALeafNodeException, SmackException.NotConnectedException, PubSubException.NotAPubSubNodeException { 209 PubSubManager pm = PubSubManager.getInstanceFor(connection, contact); 210 211 LeafNode node = getLeafNode(pm, PEP_NODE_PUBLIC_KEYS); 212 List<PayloadItem<PublicKeysListElement>> list = node.getItems(1); 213 214 if (list.isEmpty()) { 215 return null; 216 } 217 218 return list.get(0).getPayload(); 219 } 220 221 /** 222 * Delete our metadata node. 223 * 224 * @param pepManager The PEP manager. 225 * 226 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 227 * @throws SmackException.NotConnectedException if we are not connected. 228 * @throws InterruptedException if the thread is interrupted. 229 * @throws SmackException.NoResponseException if the server doesn't respond. 230 * @return <code>true</code> if the node existed and was deleted, <code>false</code> if the node did not exist. 231 */ 232 public static boolean deletePubkeysListNode(PepManager pepManager) 233 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 234 SmackException.NoResponseException { 235 PubSubManager pm = pepManager.getPepPubSubManager(); 236 return pm.deleteNode(PEP_NODE_PUBLIC_KEYS); 237 } 238 239 /** 240 * Delete the public key node of the key with fingerprint {@code fingerprint}. 241 * 242 * @param pepManager The PEP manager. 243 * @param fingerprint fingerprint of the key we want to delete 244 * 245 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 246 * @throws SmackException.NotConnectedException if we are not connected. 247 * @throws InterruptedException if the thread gets interrupted. 248 * @throws SmackException.NoResponseException if the server doesn't respond. 249 * @return <code>true</code> if the node existed and was deleted, <code>false</code> if the node did not exist. 250 */ 251 public static boolean deletePublicKeyNode(PepManager pepManager, OpenPgpV4Fingerprint fingerprint) 252 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 253 SmackException.NoResponseException { 254 PubSubManager pm = pepManager.getPepPubSubManager(); 255 return pm.deleteNode(PEP_NODE_PUBLIC_KEY(fingerprint)); 256 } 257 258 259 /** 260 * Fetch the OpenPGP public key of a {@code contact}, identified by its OpenPGP {@code v4_fingerprint}. 261 * 262 * @see <a href="https://xmpp.org/extensions/xep-0373.html#discover-pubkey">XEP-0373 §4.3</a> 263 * 264 * @param connection XMPP connection 265 * @param contact {@link BareJid} of the contact we want to fetch a key from. 266 * @param v4_fingerprint upper case, hex encoded v4 fingerprint of the contacts key. 267 * @return {@link PubkeyElement} containing the requested public key. 268 * 269 * @throws InterruptedException if the thread gets interrupted.A 270 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 271 * @throws PubSubException.NotAPubSubNodeException in case the targeted entity is not a PubSub node. 272 * @throws PubSubException.NotALeafNodeException in case the fetched node is not a {@link LeafNode}. 273 * @throws SmackException.NotConnectedException in case we are not connected. 274 * @throws SmackException.NoResponseException if the server doesn't respond. 275 */ 276 public static PubkeyElement fetchPubkey(XMPPConnection connection, BareJid contact, OpenPgpV4Fingerprint v4_fingerprint) 277 throws InterruptedException, XMPPException.XMPPErrorException, PubSubException.NotAPubSubNodeException, 278 PubSubException.NotALeafNodeException, SmackException.NotConnectedException, SmackException.NoResponseException { 279 PubSubManager pm = PubSubManager.getInstanceFor(connection, contact); 280 String nodeName = PEP_NODE_PUBLIC_KEY(v4_fingerprint); 281 282 LeafNode node = getLeafNode(pm, nodeName); 283 284 List<PayloadItem<PubkeyElement>> list = node.getItems(1); 285 286 if (list.isEmpty()) { 287 return null; 288 } 289 290 return list.get(0).getPayload(); 291 } 292 293 /** 294 * Try to get a {@link LeafNode} the traditional way (first query information using disco#info), then query the node. 295 * If that fails, query the node directly. 296 * 297 * @param pm PubSubManager 298 * @param nodeName name of the node 299 * @return node TODO javadoc me please 300 * 301 * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. 302 * @throws PubSubException.NotALeafNodeException if the queried node is not a {@link LeafNode}. 303 * @throws InterruptedException in case the thread gets interrupted 304 * @throws PubSubException.NotAPubSubNodeException in case the queried entity is not a PubSub node. 305 * @throws SmackException.NotConnectedException in case the connection is not connected. 306 * @throws SmackException.NoResponseException in case the server doesn't respond. 307 */ 308 static LeafNode getLeafNode(PubSubManager pm, String nodeName) 309 throws XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, InterruptedException, 310 PubSubException.NotAPubSubNodeException, SmackException.NotConnectedException, SmackException.NoResponseException { 311 LeafNode node; 312 try { 313 node = pm.getLeafNode(nodeName); 314 } catch (XMPPException.XMPPErrorException e) { 315 // It might happen, that the server doesn't allow disco#info queries from strangers. 316 // In that case we have to fetch the node directly 317 if (e.getStanzaError().getCondition() == StanzaError.Condition.subscription_required) { 318 node = getOpenLeafNode(pm, nodeName); 319 } else { 320 throw e; 321 } 322 } 323 324 return node; 325 } 326 327 /** 328 * Publishes a {@link SecretkeyElement} to the secret key node. 329 * The node will be configured to use the whitelist access model to prevent access from subscribers. 330 * 331 * @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep"> 332 * XEP-0373 §5. Synchronizing the Secret Key with a Private PEP Node</a> 333 * 334 * @param connection {@link XMPPConnection} of the user 335 * @param element a {@link SecretkeyElement} containing the encrypted secret key of the user 336 * 337 * @throws InterruptedException if the thread gets interrupted. 338 * @throws PubSubException.NotALeafNodeException if something is wrong with the PubSub node 339 * @throws XMPPException.XMPPErrorException in case of an protocol related error 340 * @throws SmackException.NotConnectedException if we are not connected 341 * @throws SmackException.NoResponseException /watch?v=0peBq89ZTrc 342 * @throws SmackException.FeatureNotSupportedException if the Server doesn't support the whitelist access model 343 */ 344 public static void depositSecretKey(XMPPConnection connection, SecretkeyElement element) 345 throws InterruptedException, PubSubException.NotALeafNodeException, 346 XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, 347 SmackException.FeatureNotSupportedException { 348 if (!OpenPgpManager.serverSupportsSecretKeyBackups(connection)) { 349 throw new SmackException.FeatureNotSupportedException("http://jabber.org/protocol/pubsub#access-whitelist"); 350 } 351 PubSubManager pm = PepManager.getInstanceFor(connection).getPepPubSubManager(); 352 LeafNode secretKeyNode = pm.getOrCreateLeafNode(PEP_NODE_SECRET_KEY); 353 OpenPgpPubSubUtil.changeAccessModelIfNecessary(secretKeyNode, AccessModel.whitelist); 354 355 secretKeyNode.publish(new PayloadItem<>(element)); 356 } 357 358 /** 359 * Fetch the latest {@link SecretkeyElement} from the private backup node. 360 * 361 * @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep"> 362 * XEP-0373 §5. Synchronizing the Secret Key with a Private PEP Node</a> 363 * 364 * @param pepManager the PEP manager. 365 * @return the secret key node or null, if it doesn't exist. 366 * 367 * @throws InterruptedException if the thread gets interrupted 368 * @throws PubSubException.NotALeafNodeException if there is an issue with the PubSub node 369 * @throws XMPPException.XMPPErrorException if there is an XMPP protocol related issue 370 * @throws SmackException.NotConnectedException if we are not connected 371 * @throws SmackException.NoResponseException /watch?v=7U0FzQzJzyI 372 */ 373 public static SecretkeyElement fetchSecretKey(PepManager pepManager) 374 throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, 375 SmackException.NotConnectedException, SmackException.NoResponseException { 376 PubSubManager pm = pepManager.getPepPubSubManager(); 377 LeafNode secretKeyNode = pm.getOrCreateLeafNode(PEP_NODE_SECRET_KEY); 378 List<PayloadItem<SecretkeyElement>> list = secretKeyNode.getItems(1); 379 if (list.size() == 0) { 380 LOGGER.log(Level.INFO, "No secret key published!"); 381 return null; 382 } 383 SecretkeyElement secretkeyElement = list.get(0).getPayload(); 384 return secretkeyElement; 385 } 386 387 /** 388 * Delete the private backup node. 389 * 390 * @param pepManager the PEP manager. 391 * 392 * @throws XMPPException.XMPPErrorException if there is an XMPP protocol related issue 393 * @throws SmackException.NotConnectedException if we are not connected 394 * @throws InterruptedException if the thread gets interrupted 395 * @throws SmackException.NoResponseException if the server sends no response 396 * @return <code>true</code> if the node existed and was deleted, <code>false</code> if the node did not exist. 397 */ 398 public static boolean deleteSecretKeyNode(PepManager pepManager) 399 throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, 400 SmackException.NoResponseException { 401 PubSubManager pm = pepManager.getPepPubSubManager(); 402 return pm.deleteNode(PEP_NODE_SECRET_KEY); 403 } 404 405 /** 406 * Use reflection magic to get a {@link LeafNode} without doing a disco#info query. 407 * This method is useful for fetching nodes that are configured with the access model 'open', since 408 * some servers that announce support for that access model do not allow disco#info queries from contacts 409 * which are not subscribed to the node owner. Therefore this method fetches the node directly and puts it 410 * into the {@link PubSubManager}s node map. 411 * 412 * Note: Due to the lack of a disco#info query, it might happen, that the node doesn't exist on the server, 413 * even though we add it to the node map. 414 * 415 * @see <a href="https://github.com/processone/ejabberd/issues/2483">Ejabberd bug tracker about the issue</a> 416 * @see <a href="https://mail.jabber.org/pipermail/standards/2018-June/035206.html"> 417 * Topic on the standards mailing list</a> 418 * 419 * @param pubSubManager pubsub manager 420 * @param nodeName name of the node 421 * @return leafNode TODO javadoc me please 422 * 423 * @throws PubSubException.NotALeafNodeException in case we already have the node cached, but it is not a LeafNode. 424 */ 425 @SuppressWarnings("unchecked") 426 public static LeafNode getOpenLeafNode(PubSubManager pubSubManager, String nodeName) 427 throws PubSubException.NotALeafNodeException { 428 429 try { 430 431 // Get access to the PubSubManager's nodeMap 432 Field field = pubSubManager.getClass().getDeclaredField("nodeMap"); 433 field.setAccessible(true); 434 Map<String, Node> nodeMap = (Map<String, Node>) field.get(pubSubManager); 435 436 // Check, if the node already exists 437 Node existingNode = nodeMap.get(nodeName); 438 if (existingNode != null) { 439 440 if (existingNode instanceof LeafNode) { 441 // We already know that node 442 return (LeafNode) existingNode; 443 444 } else { 445 // Throw a new NotALeafNodeException, as the node is not a LeafNode. 446 // Again use reflections to access the exceptions constructor. 447 Constructor<PubSubException.NotALeafNodeException> exceptionConstructor = 448 PubSubException.NotALeafNodeException.class.getDeclaredConstructor(String.class, BareJid.class); 449 exceptionConstructor.setAccessible(true); 450 throw exceptionConstructor.newInstance(nodeName, pubSubManager.getServiceJid()); 451 } 452 } 453 454 // Node does not exist. Create the node 455 Constructor<LeafNode> constructor; 456 constructor = LeafNode.class.getDeclaredConstructor(PubSubManager.class, String.class); 457 constructor.setAccessible(true); 458 LeafNode node = constructor.newInstance(pubSubManager, nodeName); 459 460 // Add it to the node map 461 nodeMap.put(nodeName, node); 462 463 // And return 464 return node; 465 466 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException | 467 NoSuchFieldException e) { 468 throw new LinkageError("Using reflections to create a LeafNode and put it into PubSubManagers nodeMap failed.", e); 469 } 470 } 471}