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