package org.springframework.roo.felix.pgp;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Security;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.support.osgi.OSGiUtils;
import org.springframework.roo.url.stream.UrlInputStreamService;
/**
* Default implementation of {@link PgpService}.
* <p>
* Stores the user's PGP information in the
* <code>~/.spring_roo_pgp.bpg<code> file. Every key in this
* file is considered trusted by the user. Expiration times of keys are ignored. Default keys that
* ship with Roo are added to this file automatically when the file is not present on disk.
*
* <p>
* This implementation will only verify "detached armored signatures". Produce such a file via
* "gpg --armor --detach-sign file_to_sign.ext".
*
* @author Ben Alex
* @since 1.1
*/
@Component
@Service
public class PgpServiceImpl implements PgpService {
private static final int BUFFER_SIZE = 1024;
private static String defaultKeyServerUrl =
"http://keyserver.ubuntu.com/pks/lookup?op=get&search=";
// private static String defaultKeyServerUrl =
// "http://pgp.mit.edu/pks/lookup?op=get&search=";
private static final File ROO_PGP_FILE = FileUtils.getFile(FileUtils.getUserDirectory(),
".spring_roo_pgp.bpg");
static {
Security.addProvider(new BouncyCastleProvider());
}
private boolean automaticTrust;
private BundleContext context;
private final SortedSet<PgpKeyId> discoveredKeyIds = new TreeSet<PgpKeyId>();
@Reference
private UrlInputStreamService urlInputStreamService;
public SortedSet<PgpKeyId> getDiscoveredKeyIds() {
return Collections.unmodifiableSortedSet(discoveredKeyIds);
}
public URL getKeyServerUrlToRetrieveKeyInformation(final PgpKeyId keyId) {
Validate.notNull(keyId, "Key ID required");
final URL keyUrl = getKeyServerUrlToRetrieveKeyId(keyId);
try {
final URL keyIndexUrl =
new URL(keyUrl.getProtocol() + "://" + keyUrl.getAuthority() + keyUrl.getPath()
+ "?fingerprint=on&op=index&search=");
return new URL(keyIndexUrl.toString() + keyId);
} catch (final MalformedURLException e) {
throw new IllegalStateException(e);
}
}
public String getKeyStorePhysicalLocation() {
try {
return ROO_PGP_FILE.getCanonicalPath();
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
public PGPPublicKeyRing getPublicKey(final InputStream in) {
Object obj;
try {
final PGPObjectFactory pgpFact = new PGPObjectFactory(PGPUtil.getDecoderStream(in));
obj = pgpFact.nextObject();
} catch (final Exception e) {
throw new IllegalStateException(e);
}
if (obj instanceof PGPPublicKeyRing) {
final PGPPublicKeyRing keyRing = (PGPPublicKeyRing) obj;
rememberKey(keyRing);
return keyRing;
}
throw new IllegalStateException("Pblic key not available");
}
public PGPPublicKeyRing getPublicKey(final PgpKeyId keyId) {
Validate.notNull(keyId, "Key ID required");
InputStream in = null;
try {
final URL lookup = getKeyServerUrlToRetrieveKeyId(keyId);
in = urlInputStreamService.openConnection(lookup);
return getPublicKey(in);
} catch (final Exception e) {
throw new IllegalStateException(
"Public key ID '" + keyId + "' not available from key server", e);
} finally {
IOUtils.closeQuietly(in);
}
}
@SuppressWarnings("unchecked")
public List<PGPPublicKeyRing> getTrustedKeys() {
if (!ROO_PGP_FILE.exists()) {
return new ArrayList<PGPPublicKeyRing>();
}
FileInputStream fis = null;
try {
fis = new FileInputStream(ROO_PGP_FILE);
final PGPPublicKeyRingCollection pubRings =
new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(fis));
final Iterator<PGPPublicKeyRing> rIt = pubRings.getKeyRings();
final List<PGPPublicKeyRing> result = new ArrayList<PGPPublicKeyRing>();
while (rIt.hasNext()) {
final PGPPublicKeyRing pgpPub = rIt.next();
rememberKey(pgpPub);
result.add(pgpPub);
}
return result;
} catch (final Exception e) {
throw new IllegalArgumentException("Unable to get trusted keys", ObjectUtils.defaultIfNull(
ExceptionUtils.getRootCause(e), e));
} finally {
IOUtils.closeQuietly(fis);
}
}
public boolean isAutomaticTrust() {
return automaticTrust;
}
public boolean isResourceSignedBySignature(final InputStream resource, InputStream signature) {
PGPPublicKey publicKey = null;
PGPSignature pgpSignature = null;
try {
if (!(signature instanceof ArmoredInputStream)) {
signature = new ArmoredInputStream(signature);
}
pgpSignature = isSignatureAcceptable(signature).getPgpSignature();
final PGPPublicKeyRing keyRing = getPublicKey(new PgpKeyId(pgpSignature));
rememberKey(keyRing);
publicKey = keyRing.getPublicKey();
Validate.notNull(publicKey, "Could not obtain public key for signer key ID '%s'",
pgpSignature);
pgpSignature.initVerify(publicKey, "BC");
// Now verify the signed content
final byte[] buff = new byte[BUFFER_SIZE];
int chunk;
do {
chunk = resource.read(buff);
if (chunk > 0) {
pgpSignature.update(buff, 0, chunk);
}
} while (chunk >= 0);
return pgpSignature.verify();
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}
public SignatureDecision isSignatureAcceptable(final InputStream signature) throws IOException {
Validate.notNull(signature, "Signature input stream required");
PGPObjectFactory factory = new PGPObjectFactory(PGPUtil.getDecoderStream(signature));
final Object obj = factory.nextObject();
Validate.notNull(obj, "Unable to retrieve signature from stream");
PGPSignatureList p3;
if (obj instanceof PGPCompressedData) {
try {
factory = new PGPObjectFactory(((PGPCompressedData) obj).getDataStream());
} catch (final Exception e) {
throw new IllegalStateException(e);
}
p3 = (PGPSignatureList) factory.nextObject();
} else {
p3 = (PGPSignatureList) obj;
}
final PGPSignature pgpSignature = p3.get(0);
Validate.notNull(pgpSignature, "Unable to retrieve signature from stream");
final PgpKeyId keyIdInHex = new PgpKeyId(pgpSignature);
// Special case where we directly store the key ID, as we know it's
// valid
discoveredKeyIds.add(keyIdInHex);
boolean signatureAcceptable = false;
// Loop to see if the user trusts this key
for (final PGPPublicKeyRing keyRing : getTrustedKeys()) {
final PgpKeyId candidate = new PgpKeyId(keyRing.getPublicKey());
if (candidate.equals(keyIdInHex)) {
signatureAcceptable = true;
break;
}
}
if (!signatureAcceptable && automaticTrust) {
// We don't approve of this signature, but the user has told us it's
// OK
trust(keyIdInHex);
signatureAcceptable = true;
}
return new SignatureDecision(pgpSignature, keyIdInHex, signatureAcceptable);
}
public SortedMap<PgpKeyId, String> refresh() {
final SortedMap<PgpKeyId, String> result = new TreeMap<PgpKeyId, String>();
// Get the keys we currently trust
final List<PGPPublicKeyRing> trusted = getTrustedKeys();
// Build a new list of our refreshed keys
final List<PGPPublicKeyRing> stillTrusted = new ArrayList<PGPPublicKeyRing>();
// Locate the element to remove (we need to record it so the method can
// return it)
for (final PGPPublicKeyRing candidate : trusted) {
final PGPPublicKey firstKey = candidate.getPublicKey();
final PgpKeyId candidateKeyId = new PgpKeyId(firstKey);
// Try to refresh
PGPPublicKeyRing newKeyRing;
try {
newKeyRing = getPublicKey(candidateKeyId);
} catch (final Exception e) {
// Can't retrieve, so keep the old one for now
stillTrusted.add(candidate);
result.put(candidateKeyId, "WARNING: Retained original (download issue)");
continue;
}
// Do not store if the first key is revoked
if (newKeyRing.getPublicKey().isRevoked()) {
result.put(candidateKeyId, "WARNING: Key revoked, so removed from trust list");
} else {
stillTrusted.add(newKeyRing);
result.put(candidateKeyId, "SUCCESS");
}
}
// Write back to disk
OutputStream fos = null;
try {
final PGPPublicKeyRingCollection newCollection = new PGPPublicKeyRingCollection(stillTrusted);
fos = new FileOutputStream(ROO_PGP_FILE);
newCollection.encode(fos);
} catch (final Exception e) {
throw new IllegalStateException(e);
} finally {
IOUtils.closeQuietly(fos);
}
return result;
}
public void setAutomaticTrust(final boolean automaticTrust) {
this.automaticTrust = automaticTrust;
}
public PGPPublicKeyRing trust(final PgpKeyId keyId) {
Validate.notNull(keyId, "Key ID required");
final PGPPublicKeyRing keyRing = getPublicKey(keyId);
return trust(keyRing);
}
@SuppressWarnings("unchecked")
public PGPPublicKeyRing untrust(final PgpKeyId keyId) {
Validate.notNull(keyId, "Key ID required");
// Get the keys we currently trust
final List<PGPPublicKeyRing> trusted = getTrustedKeys();
// Build a new list of keys we'll continue to trust after this method
// ends
final List<PGPPublicKeyRing> stillTrusted = new ArrayList<PGPPublicKeyRing>();
// Locate the element to remove (we need to record it so the method can
// return it)
PGPPublicKeyRing removed = null;
for (final PGPPublicKeyRing candidate : trusted) {
boolean stillTrust = true;
final Iterator<PGPPublicKey> it = candidate.getPublicKeys();
while (it.hasNext()) {
final PGPPublicKey pgpKey = it.next();
final PgpKeyId candidateKeyId = new PgpKeyId(pgpKey);
if (removed == null && candidateKeyId.equals(keyId)) {
stillTrust = false;
removed = candidate;
break;
}
}
if (stillTrust) {
stillTrusted.add(candidate);
}
}
Validate.notNull(removed, "The public key ID '%s' is not currently trusted", keyId);
// Write back to disk
OutputStream fos = null;
try {
final PGPPublicKeyRingCollection newCollection = new PGPPublicKeyRingCollection(stillTrusted);
fos = new FileOutputStream(ROO_PGP_FILE);
newCollection.encode(fos);
} catch (final Exception e) {
throw new IllegalStateException(e);
} finally {
IOUtils.closeQuietly(fos);
}
return removed;
}
protected void activate(final ComponentContext context) {
this.context = context.getBundleContext();
final String keyserver = context.getBundleContext().getProperty("pgp.keyserver.url");
if (StringUtils.isNotBlank(keyserver)) {
defaultKeyServerUrl = keyserver;
}
trustDefaultKeysIfRequired();
// Seed the discovered keys database
getTrustedKeys();
}
protected void trustDefaultKeysIfRequired() {
// Setup default keys we trust automatically
trustDefaultKeys();
}
/**
* Obtains a URL that should allow the download of the specified public key.
* <p>
* The key server may not contain the specified public key if it has never
* been uploaded.
*
* @param keyId hex-encoded key ID to download (required)
* @return the URL (never null)
*/
private URL getKeyServerUrlToRetrieveKeyId(final PgpKeyId keyId) {
try {
return new URL(defaultKeyServerUrl + keyId);
} catch (final MalformedURLException e) {
throw new IllegalStateException(e);
}
}
/**
* Simply stores the key ID in {@link #discoveredKeyIds} for future
* reference of all Key IDs we've come across. This method uses a
* {@link PGPPublicKeyRing} to ensure the input is actually a valid key,
* plus locating any key IDs that have signed the key.
* <p>
* Please note {@link #discoveredKeyIds} is not used for any key functions
* of this class. It is simply for user interface convenience.
*
* @param keyRing the key ID to store (required)
*/
@SuppressWarnings("unchecked")
private void rememberKey(final PGPPublicKeyRing keyRing) {
final PGPPublicKey key = keyRing.getPublicKey();
if (key != null) {
final PgpKeyId keyId = new PgpKeyId(key);
discoveredKeyIds.add(keyId);
final Iterator<String> userIdIterator = key.getUserIDs();
while (userIdIterator.hasNext()) {
final String userId = userIdIterator.next();
final Iterator<PGPSignature> signatureIterator = key.getSignaturesForID(userId);
while (signatureIterator.hasNext()) {
final PGPSignature signature = signatureIterator.next();
final PgpKeyId signatureKeyId = new PgpKeyId(signature);
discoveredKeyIds.add(signatureKeyId);
}
}
}
}
private PGPPublicKeyRing trust(final PGPPublicKeyRing keyRing) {
rememberKey(keyRing);
// Get the keys we currently trust
final List<PGPPublicKeyRing> trusted = getTrustedKeys();
// Do not store if the first key is revoked
Validate.validState(!keyRing.getPublicKey().isRevoked(),
"The public key ID '%s' has been revoked and cannot be trusted",
new PgpKeyId(keyRing.getPublicKey()));
// trust it and write back to disk
trusted.add(keyRing);
OutputStream fos = null;
try {
final PGPPublicKeyRingCollection newCollection = new PGPPublicKeyRingCollection(trusted);
fos = new FileOutputStream(ROO_PGP_FILE);
newCollection.encode(fos);
} catch (final Exception e) {
throw new IllegalStateException(e);
} finally {
IOUtils.closeQuietly(fos);
}
return keyRing;
}
private void trustDefaultKeys() {
// Get the URIs of all PGP keystore files within installed OSGi bundles
final List<URL> urls =
new ArrayList<URL>(OSGiUtils.findEntriesByPattern(context,
"/org/springframework/roo/felix/pgp/*.asc"));
Collections.sort(urls, new Comparator<URL>() {
public int compare(final URL url1, final URL url2) {
return url1.toExternalForm().compareTo(url2.toExternalForm());
}
});
// Trust each one
for (final URL url : urls) {
InputStream inputStream = null;
try {
inputStream = url.openStream();
trust(getPublicKey(inputStream));
} catch (final IOException ignored) {
} finally {
IOUtils.closeQuietly(inputStream);
}
}
}
}