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