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}