Smack/smack-openpgp/src/main/java/org/jivesoftware/smackx/ox/OpenPgpManager.java

412 lines
17 KiB
Java
Raw Normal View History

/**
*
* Copyright 2017 Florian Schmaus, 2018 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smackx.ox;
2018-05-23 14:57:37 +02:00
import java.security.SecureRandom;
2018-05-21 12:44:27 +02:00
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
2018-05-21 15:42:04 +02:00
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.Manager;
2018-05-21 12:44:27 +02:00
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
2018-05-21 12:44:27 +02:00
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
2018-05-21 15:42:04 +02:00
import org.jivesoftware.smack.util.Async;
2018-05-21 12:44:27 +02:00
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
2018-05-23 15:21:10 +02:00
import org.jivesoftware.smackx.ox.callback.AskForBackupCodeCallback;
2018-05-23 14:57:37 +02:00
import org.jivesoftware.smackx.ox.callback.DisplayBackupCodeCallback;
2018-05-21 12:44:27 +02:00
import org.jivesoftware.smackx.ox.element.PubkeyElement;
import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
2018-05-23 14:57:37 +02:00
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
2018-05-21 20:07:17 +02:00
import org.jivesoftware.smackx.ox.exception.CorruptedOpenPgpKeyException;
2018-05-21 12:44:27 +02:00
import org.jivesoftware.smackx.pep.PEPListener;
import org.jivesoftware.smackx.pep.PEPManager;
import org.jivesoftware.smackx.pubsub.EventElement;
import org.jivesoftware.smackx.pubsub.Item;
2018-05-21 15:42:04 +02:00
import org.jivesoftware.smackx.pubsub.ItemsExtension;
2018-05-21 12:44:27 +02:00
import org.jivesoftware.smackx.pubsub.LeafNode;
import org.jivesoftware.smackx.pubsub.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.jivesoftware.smackx.pubsub.PubSubManager;
import org.jxmpp.jid.BareJid;
2018-05-21 12:44:27 +02:00
import org.jxmpp.jid.EntityBareJid;
2018-05-21 15:42:04 +02:00
public final class OpenPgpManager extends Manager {
private static final Logger LOGGER = Logger.getLogger(OpenPgpManager.class.getName());
2018-05-21 20:07:17 +02:00
/**
* Name of the OX metadata node.
*
2018-05-22 11:42:13 +02:00
* @see <a href="https://xmpp.org/extensions/xep-0373.html#announcing-pubkey-list">XEP-0373 §4.2</a>
2018-05-21 20:07:17 +02:00
*/
public static final String PEP_NODE_PUBLIC_KEYS = "urn:xmpp:openpgp:0:public-keys";
2018-05-21 20:07:17 +02:00
2018-05-23 14:57:37 +02:00
/**
* Name of the OX secret key node.
*/
public static final String PEP_NODE_SECRET_KEY = "urn:xmpp:openpgp:0:secret-key";
2018-05-21 20:07:17 +02:00
/**
* Feature to be announced using the {@link ServiceDiscoveryManager} to subscribe to the OX metadata node.
*
2018-05-22 11:42:13 +02:00
* @see <a href="https://xmpp.org/extensions/xep-0373.html#pubsub-notifications">XEP-0373 §4.4</a>
2018-05-21 20:07:17 +02:00
*/
2018-05-21 12:44:27 +02:00
public static final String PEP_NODE_PUBLIC_KEYS_NOTIFY = PEP_NODE_PUBLIC_KEYS + "+notify";
2018-05-21 20:07:17 +02:00
/**
* Name of the OX public key node, which contains the key with id {@code id}.
*
* @param id upper case hex encoded OpenPGP v4 fingerprint of the key.
* @return PEP node name.
*/
public static String PEP_NODE_PUBLIC_KEY(String id) {
return PEP_NODE_PUBLIC_KEYS + ":" + id;
}
2018-05-21 20:07:17 +02:00
/**
* Map of instances.
*/
private static final Map<XMPPConnection, OpenPgpManager> INSTANCES = new WeakHashMap<>();
2018-05-21 20:07:17 +02:00
/**
* {@link OpenPgpProvider} responsible for processing keys, encrypting and decrypting messages and so on.
*/
2018-05-21 12:44:27 +02:00
private OpenPgpProvider provider;
2018-05-21 20:07:17 +02:00
/**
* Private constructor to avoid instantiation without putting the object into {@code INSTANCES}.
*
* @param connection xmpp connection.
*/
private OpenPgpManager(XMPPConnection connection) {
super(connection);
2018-05-21 12:44:27 +02:00
// Subscribe to public key changes
2018-05-21 15:42:04 +02:00
PEPManager.getInstanceFor(connection()).addPEPListener(metadataListener);
2018-05-21 12:44:27 +02:00
ServiceDiscoveryManager.getInstanceFor(connection())
.addFeature(PEP_NODE_PUBLIC_KEYS_NOTIFY);
}
2018-05-21 20:07:17 +02:00
/**
* Get the instance of the {@link OpenPgpManager} which belongs to the {@code connection}.
*
* @param connection xmpp connection.
* @return instance of the manager.
*/
public static OpenPgpManager getInstanceFor(XMPPConnection connection) {
OpenPgpManager manager = INSTANCES.get(connection);
if (manager == null) {
manager = new OpenPgpManager(connection);
INSTANCES.put(connection, manager);
}
return manager;
}
2018-05-21 20:07:17 +02:00
/**
* Set the {@link OpenPgpProvider} which will be used to process incoming OpenPGP elements,
* as well as to execute cryptographic operations.
*
* @param provider OpenPgpProvider.
*/
2018-05-21 12:44:27 +02:00
public void setOpenPgpProvider(OpenPgpProvider provider) {
this.provider = provider;
}
2018-05-21 20:07:17 +02:00
/**
* Publish the users OpenPGP public key to the public key node if necessary.
* Also announce the key to other users by updating the metadata node.
*
2018-05-22 11:42:13 +02:00
* @see <a href="https://xmpp.org/extensions/xep-0373.html#annoucning-pubkey">XEP-0373 §4.1</a>
2018-05-21 20:07:17 +02:00
*
* @throws CorruptedOpenPgpKeyException if our OpenPGP key is corrupted and for that reason cannot be serialized.
* @throws InterruptedException
* @throws PubSubException.NotALeafNodeException
* @throws XMPPException.XMPPErrorException
* @throws SmackException.NotConnectedException
* @throws SmackException.NoResponseException
*/
public void publishPublicKey()
throws CorruptedOpenPgpKeyException, InterruptedException, PubSubException.NotALeafNodeException,
XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException {
2018-05-21 12:44:27 +02:00
ensureProviderIsSet();
PubkeyElement pubkeyElement = provider.createPubkeyElement();
String fingerprint = provider.getFingerprint();
String keyNodeName = PEP_NODE_PUBLIC_KEY(fingerprint);
PubSubManager pm = PubSubManager.getInstance(connection(), connection().getUser().asBareJid());
// Check if key available at data node
// If not, publish key to data node
2018-05-21 12:44:27 +02:00
LeafNode keyNode = pm.getOrCreateLeafNode(keyNodeName);
List<Item> items = keyNode.getItems(1);
if (items.isEmpty()) {
2018-05-21 15:42:04 +02:00
LOGGER.log(Level.FINE, "Node " + keyNodeName + " is empty. Publish.");
2018-05-21 12:44:27 +02:00
keyNode.publish(new PayloadItem<>(pubkeyElement));
2018-05-21 20:07:17 +02:00
} else {
LOGGER.log(Level.FINE, "Node " + keyNodeName + " already contains key. Skip.");
2018-05-21 12:44:27 +02:00
}
2018-05-21 20:07:17 +02:00
// Fetch IDs from metadata node
2018-05-21 12:44:27 +02:00
LeafNode metadataNode = pm.getOrCreateLeafNode(PEP_NODE_PUBLIC_KEYS);
List<PayloadItem<PublicKeysListElement>> metadataItems = metadataNode.getItems(1);
PublicKeysListElement.Builder builder = PublicKeysListElement.builder();
if (!metadataItems.isEmpty() && metadataItems.get(0).getPayload() != null) {
// Add old entries back to list.
PublicKeysListElement publishedList = metadataItems.get(0).getPayload();
for (PublicKeysListElement.PubkeyMetadataElement meta : publishedList.getMetadata().values()) {
builder.addMetadata(meta);
}
}
builder.addMetadata(new PublicKeysListElement.PubkeyMetadataElement(fingerprint, new Date()));
2018-05-21 20:07:17 +02:00
// Publish IDs to metadata node
2018-05-21 12:44:27 +02:00
metadataNode.publish(new PayloadItem<>(builder.build()));
}
2018-05-21 20:07:17 +02:00
/**
* Consult the public key metadata node and fetch a list of all of our published OpenPGP public keys.
* TODO: Add @see which points to the (for now missing) respective example in XEP-0373.
*
* @return content of our metadata node.
* @throws InterruptedException
* @throws PubSubException.NotALeafNodeException
* @throws SmackException.NoResponseException
* @throws SmackException.NotConnectedException
* @throws XMPPException.XMPPErrorException
* @throws PubSubException.NotAPubSubNodeException
*/
2018-05-21 12:44:27 +02:00
public PublicKeysListElement fetchPubkeysList()
throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
SmackException.NotConnectedException, XMPPException.XMPPErrorException,
PubSubException.NotAPubSubNodeException {
return fetchPubkeysList(connection().getUser().asBareJid());
}
2018-05-21 20:07:17 +02:00
/**
* Consult the public key metadata node of {@code contact} to fetch the list of their published OpenPGP public keys.
* TODO: Add @see which points to the (for now missing) respective example in XEP-0373.
*
* @param contact {@link BareJid} of the user we want to fetch the list from.
* @return content of {@code contact}'s metadata node.
* @throws InterruptedException
* @throws PubSubException.NotALeafNodeException
* @throws SmackException.NoResponseException
* @throws SmackException.NotConnectedException
* @throws XMPPException.XMPPErrorException
* @throws PubSubException.NotAPubSubNodeException
*/
public PublicKeysListElement fetchPubkeysList(BareJid contact)
2018-05-21 12:44:27 +02:00
throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
SmackException.NotConnectedException, XMPPException.XMPPErrorException,
2018-05-21 15:42:04 +02:00
PubSubException.NotAPubSubNodeException {
2018-05-21 20:07:17 +02:00
PubSubManager pm = PubSubManager.getInstance(connection(), contact);
2018-05-21 12:44:27 +02:00
LeafNode node = pm.getLeafNode(PEP_NODE_PUBLIC_KEYS);
List<PayloadItem<PublicKeysListElement>> list = node.getItems(1);
if (list.isEmpty()) {
return null;
}
return list.get(0).getPayload();
}
2018-05-21 20:07:17 +02:00
/**
* Delete our metadata node.
*
* @throws XMPPException.XMPPErrorException
* @throws SmackException.NotConnectedException
* @throws InterruptedException
* @throws SmackException.NoResponseException
*/
public void deletePubkeysListNode()
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
PubSubManager pm = PubSubManager.getInstance(connection(), connection().getUser().asBareJid());
pm.deleteNode(PEP_NODE_PUBLIC_KEYS);
}
/**
* Fetch the OpenPGP public key of a {@code contact}, identified by its OpenPGP {@code v4_fingerprint}.
*
2018-05-22 11:42:13 +02:00
* @see <a href="https://xmpp.org/extensions/xep-0373.html#discover-pubkey">XEP-0373 §4.3</a>
2018-05-21 20:07:17 +02:00
*
* @param contact {@link BareJid} of the contact we want to fetch a key from.
* @param v4_fingerprint upper case, hex encoded v4 fingerprint of the contacts key.
* @return {@link PubkeyElement} containing the requested public key.
* @throws InterruptedException
* @throws PubSubException.NotALeafNodeException
* @throws SmackException.NoResponseException
* @throws SmackException.NotConnectedException
* @throws XMPPException.XMPPErrorException
* @throws PubSubException.NotAPubSubNodeException
*/
public PubkeyElement fetchPubkey(BareJid contact, String v4_fingerprint)
2018-05-21 12:44:27 +02:00
throws InterruptedException, PubSubException.NotALeafNodeException, SmackException.NoResponseException,
SmackException.NotConnectedException, XMPPException.XMPPErrorException,
PubSubException.NotAPubSubNodeException {
2018-05-21 20:07:17 +02:00
PubSubManager pm = PubSubManager.getInstance(connection(), contact);
2018-05-21 12:44:27 +02:00
LeafNode node = pm.getLeafNode(PEP_NODE_PUBLIC_KEY(v4_fingerprint));
List<PayloadItem<PubkeyElement>> list = node.getItems(1);
if (list.isEmpty()) {
return null;
}
return list.get(0).getPayload();
}
2018-05-21 20:07:17 +02:00
/**
* TODO: Implement and document.
*/
2018-05-23 14:57:37 +02:00
public void depositSecretKey(DisplayBackupCodeCallback callback)
throws CorruptedOpenPgpKeyException, InterruptedException, PubSubException.NotALeafNodeException,
XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException {
2018-05-21 12:44:27 +02:00
ensureProviderIsSet();
2018-05-23 14:57:37 +02:00
String password = generateBackupPassword();
SecretkeyElement secretKeyElement = provider.createSecretkeyElement(password);
PubSubManager pm = PubSubManager.getInstance(connection());
LeafNode secretKeyNode = pm.getOrCreateLeafNode(PEP_NODE_SECRET_KEY);
PubSubHelper.whitelist(secretKeyNode);
secretKeyNode.publish(new PayloadItem<>(secretKeyElement));
callback.displayBackupCode(password);
2018-05-21 12:44:27 +02:00
}
2018-05-23 15:21:10 +02:00
public void fetchSecretKey(AskForBackupCodeCallback callback)
throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, SmackException.NoResponseException, CorruptedOpenPgpKeyException {
PubSubManager pm = PubSubManager.getInstance(connection());
LeafNode secretKeyNode = pm.getOrCreateLeafNode(PEP_NODE_SECRET_KEY);
List<PayloadItem<SecretkeyElement>> list = secretKeyNode.getItems(1);
if (list.size() == 0) {
LOGGER.log(Level.INFO, "No secret key published!");
return;
}
SecretkeyElement secretkeyElement = list.get(0).getPayload();
provider.restoreSecretKeyElement(secretkeyElement, callback.askForBackupCode());
}
2018-05-21 20:07:17 +02:00
/**
* Return the upper-case hex encoded OpenPGP v4 fingerprint of our key pair.
*
* @return fingerprint.
* @throws CorruptedOpenPgpKeyException if for some reason we cannot determine our fingerprint.
*/
public String getOurFingerprint() throws CorruptedOpenPgpKeyException {
2018-05-21 12:44:27 +02:00
ensureProviderIsSet();
return provider.getFingerprint();
}
/**
* Throw an {@link IllegalStateException} if no {@link OpenPgpProvider} is set.
2018-05-21 20:07:17 +02:00
* The OpenPgpProvider is used to process information related to RFC-4880.
2018-05-21 12:44:27 +02:00
*/
private void ensureProviderIsSet() {
if (provider == null) {
throw new IllegalStateException("No OpenPgpProvider set!");
}
}
2018-05-21 20:07:17 +02:00
/**
* Determine, if we can sync secret keys using private PEP nodes as described in the XEP.
* Requirements on the server side are support for PEP and support for the whitelist access model of PubSub.
*
2018-05-22 11:42:13 +02:00
* @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a>
2018-05-21 20:07:17 +02:00
*
* @return
* @throws XMPPException.XMPPErrorException
* @throws SmackException.NotConnectedException
* @throws InterruptedException
* @throws SmackException.NoResponseException
*/
2018-05-21 15:42:04 +02:00
public boolean canSyncSecretKey()
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
SmackException.NoResponseException {
boolean pep = PEPManager.getInstanceFor(connection()).isSupported();
boolean whitelist = PubSubManager.getInstance(connection(), connection().getUser().asBareJid())
.getSupportedFeatures().containsFeature("http://jabber.org/protocol/pubsub#access-whitelist");
return pep && whitelist;
}
2018-05-21 15:42:04 +02:00
2018-05-21 20:07:17 +02:00
/**
* {@link PEPListener} that listens for changes to the OX public keys metadata node.
*
2018-05-22 11:42:13 +02:00
* @see <a href="https://xmpp.org/extensions/xep-0373.html#pubsub-notifications">XEP-0373 §4.4</a>
2018-05-21 20:07:17 +02:00
*/
2018-05-21 15:42:04 +02:00
private final PEPListener metadataListener = new PEPListener() {
@Override
2018-05-21 20:07:17 +02:00
public void eventReceived(final EntityBareJid from, final EventElement event, Message message) {
2018-05-21 15:42:04 +02:00
if (PEP_NODE_PUBLIC_KEYS.equals(event.getEvent().getNode())) {
LOGGER.log(Level.INFO, "Received OpenPGP metadata update from " + from);
Async.go(new Runnable() {
@Override
public void run() {
ItemsExtension items = (ItemsExtension) event.getExtensions().get(0);
PayloadItem<?> payload = (PayloadItem) items.getItems().get(0);
PublicKeysListElement listElement = (PublicKeysListElement) payload.getPayload();
2018-05-21 20:07:17 +02:00
try {
provider.processPublicKeysListElement(listElement, from.asBareJid());
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Error processing OpenPGP metadata update from " + from, e);
}
2018-05-21 15:42:04 +02:00
}
}, "ProcessOXPublicKey");
}
}
};
2018-05-23 14:57:37 +02:00
/**
* Generate a secure backup code.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#sect-idm140425111347232">XEP-0373 §5.3</a>
* @return backup code
*/
private String generateBackupPassword() {
final String alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
SecureRandom random = new SecureRandom();
StringBuilder code = new StringBuilder();
// 6 blocks
for (int i = 0; i < 6; i++) {
// of 4 chars
for (int j = 0; j < 4; j++) {
char c = alphabet.charAt(random.nextInt(alphabet.length()));
code.append(c);
}
// dash after every block except the last one
if (i != 5) {
code.append('-');
}
}
return code.toString();
}
}