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}