| /******************************************************************************* |
| * Copyright (c) 2022 Eclipse contributors and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| *******************************************************************************/ |
| package org.eclipse.equinox.internal.provisional.p2.repository; |
| |
| import java.io.*; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.nio.ByteBuffer; |
| import java.nio.file.*; |
| import java.nio.file.Path; |
| import java.nio.file.attribute.FileTime; |
| import java.util.*; |
| import java.util.function.*; |
| import org.bouncycastle.bcpg.ArmoredInputStream; |
| import org.bouncycastle.bcpg.ArmoredOutputStream; |
| import org.bouncycastle.gpg.keybox.*; |
| import org.bouncycastle.gpg.keybox.jcajce.JcaKeyBoxBuilder; |
| import org.bouncycastle.openpgp.*; |
| import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; |
| import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; |
| import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.equinox.internal.p2.core.helpers.LogHelper; |
| import org.eclipse.equinox.internal.p2.repository.Transport; |
| import org.eclipse.equinox.internal.p2.repository.helpers.DebugHelper; |
| import org.eclipse.equinox.p2.core.IAgentLocation; |
| import org.eclipse.equinox.p2.core.IProvisioningAgent; |
| import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService; |
| |
| /** |
| * @since 2.6 |
| */ |
| public class DefaultPGPPublicKeyService extends PGPPublicKeyService { |
| |
| /** |
| * Enable debug tracing either via debug options or via a system property. |
| */ |
| private static final boolean DEBUG_KEY_SERVICE = DebugHelper.DEBUG_KEY_SERVICE |
| || Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty("p2.keyserver.debug")); //$NON-NLS-1$ |
| |
| /** |
| * The system property used to initialized the {@link #keyServer}. |
| */ |
| private static final String KEY_SERVERS_PROPERTY = "p2.keyservers"; //$NON-NLS-1$ |
| |
| /** |
| * The system property used to determine where to look for the GPG pubring. |
| * |
| * @see #getGPPDirectory() |
| */ |
| private static final String GPG_HOME_PROPERTY = "p2.gpg.home"; //$NON-NLS-1$ |
| |
| /** |
| * The system property used to determine whether to enable GPG pubring lookup. |
| * |
| * @see #gpg |
| * @see #setGPG(boolean) |
| */ |
| private static final String GPG_PROPERTY = "p2.gpg"; //$NON-NLS-1$ |
| |
| /** |
| * The number of elapsed milliseconds after which keys cached from a key server |
| * are considered stale such that they will be re-fetched if possible. |
| */ |
| private static final long STALE_AFTER_MILLIS = Long.getLong("p2.keyserver.cache.stale", 24) * 1000 * 60 * 60; //$NON-NLS-1$ |
| |
| /** |
| * Reuse p2's transport layer for fetching keys from the key server. |
| */ |
| private final Transport transport; |
| |
| /** |
| * Keys {@link #addKey(PGPPublicKey) added} to this key service are cached via |
| * this map. |
| */ |
| private final Map<Long, LocalKeyCache> localKeys = new LinkedHashMap<>(); |
| |
| /** |
| * A folder with locally cached keys, indexed on {@link PGPPublicKey#getKeyID() |
| * key ID}. |
| */ |
| private final Path keyCache; |
| |
| /** |
| * The current key servers. |
| */ |
| private final Map<String, PGPKeyServer> keyServers = new LinkedHashMap<>(); |
| |
| /** |
| * Whether to load from GPG's pubring. |
| */ |
| private boolean gpg; |
| |
| /** |
| * Creates an instance associated with the given agent. |
| * |
| * @param agent the agent for which a key service is provided. |
| */ |
| public DefaultPGPPublicKeyService(IProvisioningAgent agent) { |
| IAgentLocation agentLocation = agent.getService(IAgentLocation.class); |
| URI dataArea = agentLocation.getDataArea(org.eclipse.equinox.internal.p2.repository.Activator.ID); |
| keyCache = Paths.get(dataArea).resolve("pgp"); //$NON-NLS-1$ |
| try { |
| Files.createDirectories(keyCache); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| |
| if (DEBUG_KEY_SERVICE) { |
| DebugHelper.debug("KeyServer", "Cache", "location", keyCache); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| } |
| |
| String keyServersProperty = System.getProperty(KEY_SERVERS_PROPERTY, ""); //$NON-NLS-1$ |
| if (!keyServersProperty.isBlank()) { |
| Set<String> keyServersSet = new LinkedHashSet<>(); |
| for (String keyServer : keyServersProperty.split("[,; \t]+")) { //$NON-NLS-1$ |
| if (!keyServer.isEmpty()) { |
| keyServersSet.add(keyServer); |
| } |
| } |
| |
| setKeyServers(keyServersSet); |
| } |
| |
| setGPG(Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty(GPG_PROPERTY, Boolean.TRUE.toString())) |
| || !System.getProperty(GPG_HOME_PROPERTY, "").isBlank()); //$NON-NLS-1$ |
| |
| transport = agent.getService(Transport.class); |
| } |
| |
| public Set<String> getKeyServers() { |
| return Collections.unmodifiableSet(keyServers.keySet()); |
| } |
| |
| public void setKeyServers(Set<String> keyServers) { |
| Map<String, PGPKeyServer> newKeyServers = new LinkedHashMap<>(); |
| for (String keyServer : keyServers) { |
| PGPKeyServer pgpKeyServer = this.keyServers.get(keyServer); |
| if (pgpKeyServer == null) { |
| pgpKeyServer = new PGPKeyServer(keyServer, this.keyCache) { |
| @Override |
| protected boolean isStale(Path path) { |
| return DefaultPGPPublicKeyService.this.isStale(path); |
| } |
| |
| @Override |
| protected IStatus download(URI uri, OutputStream receiver, IProgressMonitor monitor) { |
| return DefaultPGPPublicKeyService.this.download(uri, receiver, monitor); |
| } |
| |
| @Override |
| protected void log(Throwable throwable) { |
| DefaultPGPPublicKeyService.this.log(throwable); |
| } |
| }; |
| } |
| newKeyServers.put(keyServer, pgpKeyServer); |
| } |
| |
| this.keyServers.clear(); |
| this.keyServers.putAll(newKeyServers); |
| } |
| |
| @Override |
| public PGPPublicKey getKey(String fingerprint) { |
| int length = fingerprint.length(); |
| if (length >= 16) { |
| long keyID = Long.parseUnsignedLong(fingerprint.substring(length - 16, length), 16); |
| Collection<PGPPublicKey> keys = getKeys(keyID); |
| for (PGPPublicKey key : keys) { |
| if (toHexFingerprint(key).equalsIgnoreCase(fingerprint)) { |
| return key; |
| } |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public Collection<PGPPublicKey> getKeys(long keyID) { |
| List<PGPPublicKey> keys = new ArrayList<>(); |
| for (PGPKeyServer keyServer : keyServers.values()) { |
| keys.addAll(keyServer.getKeys(keyID)); |
| } |
| |
| keys.addAll(getLocalKeyCache(keyID).get()); |
| |
| keys.addAll(getDefaultKeys(keyID)); |
| |
| return reconcileKeys(keys); |
| } |
| |
| public boolean isGGP() { |
| return gpg; |
| } |
| |
| public void setGPG(boolean gpg) { |
| this.gpg = gpg; |
| } |
| |
| protected List<PGPPublicKey> getDefaultKeys(long keyID) { |
| return gpg ? getGPGPubringKeys(keyID) : Collections.emptyList(); |
| } |
| |
| protected List<PGPPublicKey> reconcileKeys(List<PGPPublicKey> keys) { |
| if (keys.size() <= 1) { |
| return new ArrayList<>(keys); |
| } |
| |
| Map<ByteBuffer, PGPPublicKey> encodings = new LinkedHashMap<>(); |
| Map<ByteBuffer, PGPPublicKey> fingerprints = new LinkedHashMap<>(); |
| for (PGPPublicKey key : keys) { |
| try { |
| ByteBuffer encoding = ByteBuffer.wrap(key.getEncoded()); |
| PGPPublicKey existingKey = encodings.put(encoding, key); |
| if (existingKey == null) { |
| ByteBuffer fingerprint = ByteBuffer.wrap(key.getFingerprint()); |
| PGPPublicKey otherKey = fingerprints.put(fingerprint, key); |
| if (otherKey != null) { |
| fingerprints.put(fingerprint, choose(otherKey, key)); |
| } |
| } |
| } catch (IOException e) { |
| log(e); |
| } |
| } |
| |
| return new ArrayList<>(fingerprints.values()); |
| } |
| |
| /** |
| * While {@link #reconcileKeys(List) reconciling}, when two keys have the same |
| * fingerprint, this method must be chosen in favor of the other to be retained |
| * in the result. |
| * |
| * @param key1 the first key from which to choose. |
| * @param key2 the second key from which to choose. |
| * @return the key with the newest or most complete details. |
| */ |
| protected PGPPublicKey choose(PGPPublicKey key1, PGPPublicKey key2) { |
| // Favor the one with the newest information. |
| long signatureTime1 = getNewestSignature(key1); |
| long signatureTime2 = getNewestSignature(key2); |
| if (signatureTime1 > signatureTime2) { |
| return key1; |
| } else if (signatureTime1 < signatureTime2) { |
| return key2; |
| } |
| |
| // Favor the one with the most information. |
| int signatureCount1 = getSignatureCount(key1); |
| int signatureCount2 = getSignatureCount(key2); |
| if (signatureCount1 > signatureCount2) { |
| return key1; |
| } else if (signatureCount1 < signatureCount2) { |
| return key2; |
| } |
| |
| return key1; |
| } |
| |
| protected static int getSignatureCount(PGPPublicKey key) { |
| int result = 0; |
| for (Iterator<PGPSignature> signatures = key.getSignatures(); signatures.hasNext(); signatures.next()) { |
| ++result; |
| } |
| for (Iterator<PGPSignature> signatures = key.getKeySignatures(); signatures.hasNext(); signatures.next()) { |
| ++result; |
| } |
| return result; |
| } |
| |
| protected static long getNewestSignature(PGPPublicKey key) { |
| long result = 0; |
| for (Iterator<PGPSignature> signatures = key.getSignatures(); signatures.hasNext();) { |
| PGPSignature signature = signatures.next(); |
| long time = signature.getCreationTime().getTime(); |
| result = Math.max(result, time); |
| } |
| for (Iterator<PGPSignature> signatures = key.getKeySignatures(); signatures.hasNext();) { |
| PGPSignature signature = signatures.next(); |
| long time = signature.getCreationTime().getTime(); |
| result = Math.max(result, time); |
| } |
| |
| return result; |
| } |
| |
| @Override |
| public PGPPublicKey addKey(PGPPublicKey key) { |
| long keyID = key.getKeyID(); |
| LocalKeyCache localKeyCache = getLocalKeyCache(keyID); |
| localKeyCache.add(key); |
| |
| Collection<PGPPublicKey> keys = getKeys(keyID); |
| byte[] fingerprint = key.getFingerprint(); |
| for (PGPPublicKey otherKey : keys) { |
| if (Arrays.equals(otherKey.getFingerprint(), fingerprint)) { |
| return otherKey; |
| } |
| } |
| |
| // We should never get this far. |
| return key; |
| } |
| |
| protected boolean isStale(Path path) { |
| try { |
| FileTime lastModifiedTime = Files.getLastModifiedTime(path); |
| long lastModified = lastModifiedTime.toMillis(); |
| long currentTime = System.currentTimeMillis(); |
| return currentTime - lastModified > STALE_AFTER_MILLIS; |
| } catch (IOException e) { |
| return true; |
| } |
| } |
| |
| @Override |
| public Set<PGPPublicKey> getVerifiedCertifications(PGPPublicKey key) { |
| Set<PGPPublicKey> certifications = new LinkedHashSet<>(); |
| LOOP: for (Iterator<PGPSignature> signatures = key.getSignatures(); signatures.hasNext();) { |
| PGPSignature signature = signatures.next(); |
| long signingKeyID = signature.getKeyID(); |
| for (PGPPublicKey signingKey : getKeys(signingKeyID)) { |
| switch (signature.getSignatureType()) { |
| case PGPSignature.SUBKEY_BINDING: |
| case PGPSignature.PRIMARYKEY_BINDING: { |
| try { |
| signature.init(new BcPGPContentVerifierBuilderProvider(), signingKey); |
| if (signature.verifyCertification(signingKey, key) |
| && isCreatedBeforeRevocation(signature, signingKey)) { |
| certifications.add(signingKey); |
| continue LOOP; |
| } |
| } catch (PGPException e) { |
| //$FALL-THROUGH$ |
| } |
| break; |
| } |
| case PGPSignature.DEFAULT_CERTIFICATION: |
| case PGPSignature.NO_CERTIFICATION: |
| case PGPSignature.CASUAL_CERTIFICATION: |
| case PGPSignature.POSITIVE_CERTIFICATION: { |
| for (Iterator<String> userIDs = key.getUserIDs(); userIDs.hasNext();) { |
| String userID = userIDs.next(); |
| try { |
| signature.init(new BcPGPContentVerifierBuilderProvider(), signingKey); |
| if (signature.verifyCertification(userID, key) |
| && isCreatedBeforeRevocation(signature, signingKey)) { |
| certifications.add(signingKey); |
| continue LOOP; |
| } |
| } catch (PGPException e) { |
| //$FALL-THROUGH$ |
| } |
| } |
| break; |
| } |
| } |
| } |
| } |
| return certifications; |
| } |
| |
| @Override |
| public Date getVerifiedRevocationDate(PGPPublicKey key) { |
| for (Iterator<PGPSignature> signatures = key.getSignatures(); signatures.hasNext();) { |
| PGPSignature signature = signatures.next(); |
| long signingKeyID = signature.getKeyID(); |
| for (PGPPublicKey signingKey : getKeys(signingKeyID)) { |
| switch (signature.getSignatureType()) { |
| case PGPSignature.KEY_REVOCATION: |
| case PGPSignature.CERTIFICATION_REVOCATION: { |
| try { |
| signature.init(new BcPGPContentVerifierBuilderProvider(), signingKey); |
| if (signature.verifyCertification(key)) { |
| return signature.getCreationTime(); |
| } |
| } catch (PGPException e) { |
| //$FALL-THROUGH$ |
| } |
| break; |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| private LocalKeyCache getLocalKeyCache(long keyID) { |
| LocalKeyCache localKeyCache = localKeys.get(keyID); |
| if (localKeyCache == null) { |
| String hexKeyID = toHex(keyID); |
| Path cache = keyCache.resolve(hexKeyID + ".asc"); //$NON-NLS-1$ |
| localKeyCache = new LocalKeyCache(cache) { |
| @Override |
| protected List<PGPPublicKey> reconcileKeys(List<PGPPublicKey> keys) { |
| return DefaultPGPPublicKeyService.this.reconcileKeys(keys); |
| } |
| |
| @Override |
| protected void log(Throwable throwable) { |
| DefaultPGPPublicKeyService.this.log(throwable); |
| } |
| }; |
| localKeys.put(keyID, localKeyCache); |
| } |
| return localKeyCache; |
| } |
| |
| protected Collection<PGPPublicKey> fetchKeys(URI uri, Path cache) throws IOException { |
| try { |
| ByteArrayOutputStream reciever = new ByteArrayOutputStream(); |
| IStatus download = download(uri, reciever, new NullProgressMonitor()); |
| if (!download.isOK()) { |
| Throwable exception = download.getException(); |
| if (exception != null) { |
| throw new IOException(download.getMessage(), exception); |
| } |
| throw new IOException(download.getMessage()); |
| } |
| List<PGPPublicKey> result = new ArrayList<>(); |
| byte[] bytes = reciever.toByteArray(); |
| try (InputStream input = new ArmoredInputStream(new ByteArrayInputStream(bytes))) { |
| result.addAll(loadKeys(input)); |
| } |
| |
| try (OutputStream out = newAtomicOutputStream(cache)) { |
| out.write(bytes); |
| } |
| |
| return result; |
| } catch (IOException ex) { |
| if (Files.isRegularFile(cache)) { |
| try (InputStream input = new ArmoredInputStream(new BufferedInputStream(Files.newInputStream(cache)))) { |
| return loadKeys(input); |
| } catch (IOException ex1) { |
| try { |
| // Assume the cache is corrupt so delete it. |
| Files.delete(cache); |
| } catch (IOException ex2) { |
| log(ex2); |
| } |
| // Rethrow original network failure exception |
| throw new IOException("Error while processing " + uri + " as well while processing the cache " //$NON-NLS-1$ //$NON-NLS-2$ |
| + cache + ": " + ex1.getMessage(), ex); //$NON-NLS-1$ |
| } |
| } |
| throw new IOException("Error while processing " + uri, ex); //$NON-NLS-1$ |
| } |
| } |
| |
| protected IStatus download(URI uri, OutputStream receiver, IProgressMonitor monitor) { |
| return transport.download(uri, receiver, monitor); |
| } |
| |
| protected void log(Throwable throwable) { |
| if (DEBUG_KEY_SERVICE) { |
| LogHelper.log(new Status(IStatus.ERROR, org.eclipse.equinox.internal.p2.repository.Activator.ID, |
| throwable.getMessage(), throwable)); |
| } |
| } |
| |
| protected static OutputStream newAtomicOutputStream(Path cache) throws IOException { |
| Path temp = Files.createTempFile(cache.getParent(), "out", ".tmp"); //$NON-NLS-1$ //$NON-NLS-2$ |
| return new BufferedOutputStream(Files.newOutputStream(temp)) { |
| @Override |
| public void close() throws IOException { |
| super.close(); |
| Files.move(temp, cache, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); |
| } |
| }; |
| } |
| |
| protected static List<PGPPublicKey> loadKeys(InputStream input) throws IOException { |
| try { |
| List<PGPPublicKey> result = new ArrayList<>(); |
| for (Object o : new JcaPGPObjectFactory(input)) { |
| if (o instanceof PGPPublicKeyRingCollection) { |
| collectKeys((PGPPublicKeyRingCollection) o, result::add); |
| } else if (o instanceof PGPPublicKeyRing) { |
| collectKeys((PGPPublicKeyRing) o, result::add); |
| } else if (o instanceof PGPPublicKey) { |
| result.add((PGPPublicKey) o); |
| } |
| } |
| return result; |
| } catch (RuntimeException ex) { |
| throw new IOException(ex); |
| } |
| } |
| |
| private static void collectKeys(PGPPublicKeyRingCollection pgpPublicKeyRingCollection, |
| Consumer<PGPPublicKey> collector) { |
| pgpPublicKeyRingCollection.forEach(keyring -> collectKeys(keyring, collector)); |
| } |
| |
| private static void collectKeys(PGPPublicKeyRing pgpPublicKeyRing, Consumer<PGPPublicKey> collector) { |
| pgpPublicKeyRing.getPublicKeys().forEachRemaining(collector::accept); |
| } |
| |
| private static abstract class LocalKeyCache { |
| private Path cache; |
| private FileTime lastModifiedTime; |
| private List<PGPPublicKey> keys; |
| |
| public LocalKeyCache(Path cache) { |
| this.cache = cache; |
| } |
| |
| protected abstract void log(Throwable throwable); |
| |
| protected abstract List<PGPPublicKey> reconcileKeys(List<PGPPublicKey> keysToReconcile); |
| |
| public List<PGPPublicKey> get() { |
| if (keys != null) { |
| try { |
| FileTime newLastModifiedTime = Files.getLastModifiedTime(cache); |
| if (lastModifiedTime == null || lastModifiedTime.compareTo(newLastModifiedTime) < 0) { |
| lastModifiedTime = newLastModifiedTime; |
| } else { |
| return keys; |
| } |
| } catch (Exception e) { |
| //$FALL-THROUGH$ |
| } |
| } |
| |
| if (!Files.isRegularFile(cache)) { |
| return List.of(); |
| } |
| |
| try (InputStream input = new ArmoredInputStream(new BufferedInputStream(Files.newInputStream(cache)))) { |
| keys = loadKeys(input); |
| return keys; |
| } catch (IOException ex) { |
| log(ex); |
| try { |
| // Assume the cache is corrupt so delete it. |
| Files.delete(cache); |
| } catch (IOException ex2) { |
| log(ex2); |
| } |
| return List.of(); |
| } |
| } |
| |
| public void add(PGPPublicKey key) { |
| List<PGPPublicKey> oldKeys = get(); |
| List<PGPPublicKey> newKeys = new ArrayList<>(oldKeys); |
| newKeys.add(key); |
| newKeys = reconcileKeys(newKeys); |
| if (!oldKeys.equals(newKeys)) { |
| try (OutputStream underlyingStream = newAtomicOutputStream(cache); |
| OutputStream output = new ArmoredOutputStream(underlyingStream)) { |
| for (PGPPublicKey newKey : newKeys) { |
| newKey.encode(output); |
| } |
| } catch (IOException e) { |
| log(e); |
| return; |
| } |
| keys = newKeys; |
| } |
| } |
| } |
| |
| private static abstract class PGPKeyServer { |
| private final Map<Long, List<PGPPublicKey>> keyIDMap = new LinkedHashMap<>(); |
| |
| private final String keyServer; |
| |
| private final Path keyCache; |
| |
| public PGPKeyServer(String keyServer, Path baseCache) { |
| this.keyServer = keyServer; |
| keyCache = baseCache.resolve(keyServer.replace(':', '_')); |
| if (!Files.isDirectory(this.keyCache)) { |
| try { |
| Files.createDirectory(keyCache); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| protected abstract boolean isStale(Path path); |
| |
| protected abstract IStatus download(URI uri, OutputStream receiver, IProgressMonitor monitor); |
| |
| protected abstract void log(Throwable throwable); |
| |
| public List<PGPPublicKey> getKeys(long keyID) { |
| List<PGPPublicKey> keys = keyIDMap.get(keyID); |
| String hexKeyID = toHex(keyID); |
| Path cache = keyCache.resolve(hexKeyID + ".asc"); //$NON-NLS-1$ |
| boolean needsRemoteFetch = !Files.isRegularFile(cache) || isStale(cache); |
| if (keys == null || needsRemoteFetch) { |
| try { |
| Iterable<PGPPublicKey> fetchedKeys; |
| if (needsRemoteFetch) { |
| String link = "https://" + keyServer + "/pks/lookup?op=get&search=0x" + hexKeyID; //$NON-NLS-1$ //$NON-NLS-2$ |
| if (DEBUG_KEY_SERVICE) { |
| DebugHelper.debug("KeyServer", "Searching", "uri", link); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| } |
| URI uri = new URI(link); |
| fetchedKeys = fetchKeys(uri, cache); |
| } else { |
| try (InputStream input = new ArmoredInputStream( |
| new BufferedInputStream(Files.newInputStream(cache)))) { |
| fetchedKeys = loadKeys(input); |
| } |
| } |
| |
| List<PGPPublicKey> newKeys = new ArrayList<>(); |
| for (PGPPublicKey fetchedKey : fetchedKeys) { |
| long fetchedKeyID = fetchedKey.getKeyID(); |
| if (fetchedKeyID == keyID) { |
| newKeys.add(fetchedKey); |
| } |
| } |
| |
| keyIDMap.put(keyID, newKeys); |
| keys = newKeys; |
| } catch (URISyntaxException | IOException e) { |
| log(e); |
| if (keys == null || keys.isEmpty()) { |
| List<PGPPublicKey> newKeys = List.of(); |
| keyIDMap.put(keyID, newKeys); |
| keys = newKeys; |
| } |
| } |
| } |
| |
| return Collections.unmodifiableList(keys); |
| } |
| |
| protected Collection<PGPPublicKey> fetchKeys(URI uri, Path cache) throws IOException { |
| try { |
| ByteArrayOutputStream reciever = new ByteArrayOutputStream(); |
| IStatus download = download(uri, reciever, new NullProgressMonitor()); |
| if (!download.isOK()) { |
| // If the file is not found, save an empty file to prevent repeated attempts to |
| // download from this URI. |
| Throwable exception = download.getException(); |
| if (exception instanceof FileNotFoundException) { |
| log(exception); |
| } else { |
| if (exception != null) { |
| throw new IOException(download.getMessage(), exception); |
| } |
| throw new IOException(download.getMessage()); |
| } |
| } |
| List<PGPPublicKey> result; |
| byte[] bytes = reciever.toByteArray(); |
| try { |
| try (InputStream input = new ArmoredInputStream(new ByteArrayInputStream(bytes))) { |
| result = loadKeys(input); |
| } |
| } catch (IOException ex) { |
| log(ex); |
| // If the bytes can't be processed cache an empty file to prevent repeated |
| // attempts. |
| bytes = new byte[0]; |
| result = List.of(); |
| } |
| |
| try (OutputStream out = newAtomicOutputStream(cache)) { |
| out.write(bytes); |
| } |
| |
| return result; |
| } catch (IOException ex) { |
| // If the key server fails, load the cache if it exists. |
| if (Files.isRegularFile(cache)) { |
| try (InputStream input = new ArmoredInputStream( |
| new BufferedInputStream(Files.newInputStream(cache)))) { |
| return loadKeys(input); |
| } catch (IOException ex1) { |
| try { |
| // Assume the cache is corrupt so delete it. |
| Files.delete(cache); |
| } catch (IOException ex2) { |
| log(ex2); |
| } |
| // Rethrow original network failure exception with additional details |
| throw new IOException("Error while processing " + uri + " as well while processing the cache " //$NON-NLS-1$ //$NON-NLS-2$ |
| + cache + ": " + ex1.getMessage(), ex); //$NON-NLS-1$ |
| } |
| } |
| throw new IOException("Error while processing " + uri, ex); //$NON-NLS-1$ |
| } |
| } |
| |
| } |
| |
| private static List<PGPPublicKey> getGPGPubringKeys(long keyID) { |
| return GPGPubringCache.getKeys(keyID); |
| } |
| |
| private static class GPGPubringCache { |
| private static final Supplier<PGPPublicKeyRingCollection> GPG_PUBRING = getGPGPubring(); |
| private static volatile PGPPublicKeyRingCollection cachePubring; |
| private static volatile Map<Long, List<PGPPublicKey>> cache; |
| |
| public static List<PGPPublicKey> getKeys(long keyID) { |
| PGPPublicKeyRingCollection pubring = GPG_PUBRING.get(); |
| if (pubring != cachePubring) { |
| Map<Long, List<PGPPublicKey>> newCache = new LinkedHashMap<>(); |
| for (Iterator<PGPPublicKeyRing> keyRings = pubring.getKeyRings(); keyRings.hasNext();) { |
| for (PGPPublicKey key : keyRings.next()) { |
| long keyID2 = key.getKeyID(); |
| List<PGPPublicKey> keys = newCache.computeIfAbsent(keyID2, it -> new ArrayList<>()); |
| keys.add(key); |
| } |
| } |
| cache = newCache; |
| cachePubring = pubring; |
| } |
| |
| List<PGPPublicKey> result = cache.get(keyID); |
| return result == null ? List.of() : result; |
| } |
| } |
| |
| private static abstract class GPGPubringSupplier implements Supplier<PGPPublicKeyRingCollection> { |
| |
| private final Path pubring; |
| |
| private PGPPublicKeyRingCollection keyRingCollection; |
| |
| private FileTime lastModifiedTime; |
| |
| public GPGPubringSupplier(Path pubring) { |
| this.pubring = pubring; |
| try { |
| keyRingCollection = new PGPPublicKeyRingCollection(Collections.emptyList()); |
| } catch (IOException | PGPException e) { |
| // Cannot happen for an empty collection. |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Override |
| public PGPPublicKeyRingCollection get() { |
| try { |
| FileTime newLastModifiedTime = Files.getLastModifiedTime(pubring); |
| if (lastModifiedTime == null || lastModifiedTime.compareTo(newLastModifiedTime) < 0) { |
| lastModifiedTime = newLastModifiedTime; |
| keyRingCollection = buildPubring(); |
| } |
| } catch (Exception e) { |
| //$FALL-THROUGH$ |
| } |
| return keyRingCollection; |
| } |
| |
| protected abstract PGPPublicKeyRingCollection buildPubring() throws Exception; |
| } |
| |
| private static Supplier<PGPPublicKeyRingCollection> getGPGPubring() { |
| Path gpgDirectory = getGPPDirectory(); |
| Path pubringGpg = gpgDirectory.resolve("pubring.gpg"); //$NON-NLS-1$ |
| Path pubringKbx = gpgDirectory.resolve("pubring.kbx"); //$NON-NLS-1$ |
| |
| if (Files.isRegularFile(pubringGpg)) { |
| return new GPGPubringSupplier(pubringGpg) { |
| @Override |
| protected PGPPublicKeyRingCollection buildPubring() throws Exception { |
| try (InputStream input = new BufferedInputStream(Files.newInputStream(pubringGpg))) { |
| PGPPublicKeyRingCollection keyRingCollection = new PGPPublicKeyRingCollection(input, |
| new JcaKeyFingerprintCalculator()); |
| return keyRingCollection; |
| } |
| } |
| }; |
| } else if (Files.isRegularFile(pubringKbx)) { |
| return new GPGPubringSupplier(pubringKbx) { |
| @Override |
| protected PGPPublicKeyRingCollection buildPubring() throws Exception { |
| try (InputStream input = new BufferedInputStream(Files.newInputStream(pubringKbx))) { |
| KeyBox keyBox = new JcaKeyBoxBuilder().build(input); |
| List<PGPPublicKeyRing> pgpPublicKeyRings = new ArrayList<>(); |
| for (KeyBlob keyBlob : keyBox.getKeyBlobs()) { |
| switch (keyBlob.getType()) { |
| case OPEN_PGP_BLOB: { |
| PGPPublicKeyRing pgpPublicKeyRing = ((PublicKeyRingBlob) keyBlob).getPGPPublicKeyRing(); |
| pgpPublicKeyRings.add(pgpPublicKeyRing); |
| } |
| default: { |
| //$FALL-THROUGH$ |
| } |
| } |
| } |
| PGPPublicKeyRingCollection keyRingCollection = new PGPPublicKeyRingCollection( |
| pgpPublicKeyRings); |
| return keyRingCollection; |
| } |
| } |
| }; |
| } else { |
| PGPPublicKeyRingCollection empty; |
| try { |
| empty = new PGPPublicKeyRingCollection(Collections.emptyList()); |
| } catch (IOException | PGPException e) { |
| // Cannot happen for an empty collection. |
| throw new RuntimeException(e); |
| } |
| return () -> empty; |
| } |
| } |
| |
| @SuppressWarnings("nls") |
| private static Path getGPPDirectory() { |
| // Handle ~ as might be used on macos and linux. |
| Function<String, Path> resolveTilde = s -> { |
| if (s.startsWith("~/") || s.startsWith("~" + File.separatorChar)) { |
| return new File(System.getProperty("user.home"), s.substring(2)).getAbsoluteFile().toPath(); |
| } |
| return Paths.get(s); |
| }; |
| |
| // Allow the user to specify the GPG home used by p2 specifically. |
| Path path = checkDirectory(System.getProperty(GPG_HOME_PROPERTY), resolveTilde); |
| if (path != null) { |
| return path; |
| } |
| |
| path = checkDirectory(System.getenv("GNUPGHOME"), resolveTilde); |
| if (path != null) { |
| return path; |
| } |
| |
| if ("win32".equals(System.getProperty("osgi.os"))) { |
| // On Windows prefer %APPDATA%\gnupg if it exists, even if Cygwin is used. |
| path = checkDirectory(System.getenv("APPDATA"), //$NON-NLS-1$ |
| s -> Paths.get(s).resolve("gnupg")); //$NON-NLS-1$ |
| if (path != null) { |
| return path; |
| } |
| } |
| // All systems, including Cygwin and even Windows if %APPDATA%\gnupg doesn't |
| // exist. |
| return resolveTilde.apply("~/.gnupg"); //$NON-NLS-1$ |
| } |
| |
| private static Path checkDirectory(String dir, Function<String, Path> toPath) { |
| if (dir != null && !dir.isBlank()) { |
| try { |
| Path directory = toPath.apply(dir); |
| if (Files.isDirectory(directory)) { |
| return directory; |
| } |
| } catch (RuntimeException e) { |
| //$FALL-THROUGH$ |
| } |
| } |
| return null; |
| } |
| } |