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}