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