CachingKeyPairProvider.java

  1. /*
  2.  * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.internal.transport.sshd;

  11. import static java.text.MessageFormat.format;

  12. import java.io.File;
  13. import java.io.IOException;
  14. import java.io.InputStream;
  15. import java.nio.file.Files;
  16. import java.nio.file.Path;
  17. import java.security.GeneralSecurityException;
  18. import java.security.InvalidKeyException;
  19. import java.security.KeyPair;
  20. import java.security.PrivateKey;
  21. import java.security.PublicKey;
  22. import java.util.ArrayList;
  23. import java.util.Collection;
  24. import java.util.Collections;
  25. import java.util.HashMap;
  26. import java.util.Iterator;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.NoSuchElementException;
  30. import java.util.concurrent.CancellationException;

  31. import javax.security.auth.DestroyFailedException;

  32. import org.apache.sshd.common.AttributeRepository.AttributeKey;
  33. import org.apache.sshd.client.session.ClientSession;
  34. import org.apache.sshd.common.NamedResource;
  35. import org.apache.sshd.common.config.keys.FilePasswordProvider;
  36. import org.apache.sshd.common.config.keys.KeyUtils;
  37. import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
  38. import org.apache.sshd.common.session.SessionContext;
  39. import org.apache.sshd.common.util.io.resource.IoResource;
  40. import org.apache.sshd.common.util.security.SecurityUtils;
  41. import org.eclipse.jgit.transport.sshd.KeyCache;

  42. /**
  43.  * A {@link FileKeyPairProvider} that uses an external {@link KeyCache}.
  44.  */
  45. public class CachingKeyPairProvider extends FileKeyPairProvider
  46.         implements Iterable<KeyPair> {

  47.     /**
  48.      * An attribute set on the {@link SessionContext} recording loaded keys by
  49.      * fingerprint. This enables us to provide nicer output by showing key
  50.      * paths, if possible. Users can identify key identities used easier by
  51.      * filename than by fingerprint.
  52.      */
  53.     public static final AttributeKey<Map<String, Path>> KEY_PATHS_BY_FINGERPRINT = new AttributeKey<>();

  54.     private final KeyCache cache;

  55.     /**
  56.      * Creates a new {@link CachingKeyPairProvider} using the given
  57.      * {@link KeyCache}. If the cache is {@code null}, this is a simple
  58.      * {@link FileKeyPairProvider}.
  59.      *
  60.      * @param paths
  61.      *            to load keys from
  62.      * @param cache
  63.      *            to use, may be {@code null} if no external caching is desired
  64.      */
  65.     public CachingKeyPairProvider(List<Path> paths, KeyCache cache) {
  66.         super(paths);
  67.         this.cache = cache;
  68.     }

  69.     @Override
  70.     public Iterator<KeyPair> iterator() {
  71.         return iterator(null);
  72.     }

  73.     private Iterator<KeyPair> iterator(SessionContext session) {
  74.         Collection<? extends Path> resources = getPaths();
  75.         if (resources.isEmpty()) {
  76.             return Collections.emptyListIterator();
  77.         }
  78.         return new CancellingKeyPairIterator(session, resources);
  79.     }

  80.     @Override
  81.     public Iterable<KeyPair> loadKeys(SessionContext session) {
  82.         return () -> iterator(session);
  83.     }

  84.     static String getKeyId(ClientSession session, KeyPair identity) {
  85.         String fingerprint = KeyUtils.getFingerPrint(identity.getPublic());
  86.         Map<String, Path> registered = session
  87.                 .getAttribute(KEY_PATHS_BY_FINGERPRINT);
  88.         if (registered != null) {
  89.             Path path = registered.get(fingerprint);
  90.             if (path != null) {
  91.                 Path home = session
  92.                         .resolveAttribute(JGitSshClient.HOME_DIRECTORY);
  93.                 if (home != null && path.startsWith(home)) {
  94.                     try {
  95.                         path = home.relativize(path);
  96.                         String pathString = path.toString();
  97.                         if (!pathString.isEmpty()) {
  98.                             return "~" + File.separator + pathString; //$NON-NLS-1$
  99.                         }
  100.                     } catch (IllegalArgumentException e) {
  101.                         // Cannot be relativized. Ignore, and work with the
  102.                         // original path
  103.                     }
  104.                 }
  105.                 return path.toString();
  106.             }
  107.         }
  108.         return fingerprint;
  109.     }

  110.     private KeyPair loadKey(SessionContext session, Path path)
  111.             throws IOException, GeneralSecurityException {
  112.         if (!Files.exists(path)) {
  113.             log.warn(format(SshdText.get().identityFileNotFound, path));
  114.             return null;
  115.         }
  116.         IoResource<Path> resource = getIoResource(session, path);
  117.         if (cache == null) {
  118.             return loadKey(session, resource, path, getPasswordFinder());
  119.         }
  120.         Throwable[] t = { null };
  121.         KeyPair key = cache.get(path, p -> {
  122.             try {
  123.                 return loadKey(session, resource, p, getPasswordFinder());
  124.             } catch (IOException | GeneralSecurityException e) {
  125.                 t[0] = e;
  126.                 return null;
  127.             }
  128.         });
  129.         if (t[0] != null) {
  130.             if (t[0] instanceof CancellationException) {
  131.                 throw (CancellationException) t[0];
  132.             }
  133.             throw new IOException(
  134.                     format(SshdText.get().keyLoadFailed, resource), t[0]);
  135.         }
  136.         return key;
  137.     }

  138.     private KeyPair loadKey(SessionContext session, NamedResource resource,
  139.             Path path, FilePasswordProvider passwordProvider)
  140.             throws IOException, GeneralSecurityException {
  141.         try (InputStream stream = Files.newInputStream(path)) {
  142.             Iterable<KeyPair> ids = SecurityUtils.loadKeyPairIdentities(session,
  143.                     resource, stream, passwordProvider);
  144.             if (ids == null) {
  145.                 throw new InvalidKeyException(
  146.                         format(SshdText.get().identityFileNoKey, path));
  147.             }
  148.             Iterator<KeyPair> keys = ids.iterator();
  149.             if (!keys.hasNext()) {
  150.                 throw new InvalidKeyException(format(
  151.                         SshdText.get().identityFileUnsupportedFormat, path));
  152.             }
  153.             KeyPair result = keys.next();
  154.             PublicKey pk = result.getPublic();
  155.             if (pk != null) {
  156.                 Map<String, Path> registered = session
  157.                         .getAttribute(KEY_PATHS_BY_FINGERPRINT);
  158.                 if (registered == null) {
  159.                     registered = new HashMap<>();
  160.                     session.setAttribute(KEY_PATHS_BY_FINGERPRINT, registered);
  161.                 }
  162.                 registered.put(KeyUtils.getFingerPrint(pk), path);
  163.             }
  164.             if (keys.hasNext()) {
  165.                 log.warn(format(SshdText.get().identityFileMultipleKeys, path));
  166.                 keys.forEachRemaining(k -> {
  167.                     PrivateKey priv = k.getPrivate();
  168.                     if (priv != null) {
  169.                         try {
  170.                             priv.destroy();
  171.                         } catch (DestroyFailedException e) {
  172.                             // Ignore
  173.                         }
  174.                     }
  175.                 });
  176.             }
  177.             return result;
  178.         }
  179.     }

  180.     private class CancellingKeyPairIterator implements Iterator<KeyPair> {

  181.         private final SessionContext context;

  182.         private final Iterator<Path> paths;

  183.         private KeyPair nextItem;

  184.         private boolean nextSet;

  185.         public CancellingKeyPairIterator(SessionContext session,
  186.                 Collection<? extends Path> resources) {
  187.             List<Path> copy = new ArrayList<>(resources.size());
  188.             copy.addAll(resources);
  189.             paths = copy.iterator();
  190.             context = session;
  191.         }

  192.         @Override
  193.         public boolean hasNext() {
  194.             if (nextSet) {
  195.                 return nextItem != null;
  196.             }
  197.             nextSet = true;
  198.             while (nextItem == null && paths.hasNext()) {
  199.                 try {
  200.                     nextItem = loadKey(context, paths.next());
  201.                 } catch (CancellationException cancelled) {
  202.                     throw cancelled;
  203.                 } catch (Exception other) {
  204.                     log.warn(other.getMessage(), other);
  205.                 }
  206.             }
  207.             return nextItem != null;
  208.         }

  209.         @Override
  210.         public KeyPair next() {
  211.             if (!nextSet && !hasNext()) {
  212.                 throw new NoSuchElementException();
  213.             }
  214.             KeyPair result = nextItem;
  215.             nextItem = null;
  216.             nextSet = false;
  217.             if (result == null) {
  218.                 throw new NoSuchElementException();
  219.             }
  220.             return result;
  221.         }

  222.     }
  223. }