/* * Copyright (C) 2018, 2022 Thomas Wolf and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.transport.sshd; import static java.text.MessageFormat.format; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.CancellationException; import javax.security.auth.DestroyFailedException; import org.apache.sshd.common.AttributeRepository.AttributeKey; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.config.keys.FilePasswordProvider; import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.keyprovider.FileKeyPairProvider; import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.util.io.resource.IoResource; import org.apache.sshd.common.util.security.SecurityUtils; import org.eclipse.jgit.transport.sshd.KeyCache; /** * A {@link FileKeyPairProvider} that uses an external {@link KeyCache}. */ public class CachingKeyPairProvider extends FileKeyPairProvider implements Iterable { /** * An attribute set on the {@link SessionContext} recording loaded keys by * fingerprint. This enables us to provide nicer output by showing key * paths, if possible. Users can identify key identities used easier by * filename than by fingerprint. */ public static final AttributeKey> KEY_PATHS_BY_FINGERPRINT = new AttributeKey<>(); private final KeyCache cache; /** * Creates a new {@link CachingKeyPairProvider} using the given * {@link KeyCache}. If the cache is {@code null}, this is a simple * {@link FileKeyPairProvider}. * * @param paths * to load keys from * @param cache * to use, may be {@code null} if no external caching is desired */ public CachingKeyPairProvider(List paths, KeyCache cache) { super(paths); this.cache = cache; } @Override public Iterator iterator() { return iterator(null); } private Iterator iterator(SessionContext session) { Collection resources = getPaths(); if (resources.isEmpty()) { return Collections.emptyListIterator(); } return new CancellingKeyPairIterator(session, resources); } @Override public Iterable loadKeys(SessionContext session) { return () -> iterator(session); } static String getKeyId(ClientSession session, KeyPair identity) { String fingerprint = KeyUtils.getFingerPrint(identity.getPublic()); Map registered = session .getAttribute(KEY_PATHS_BY_FINGERPRINT); if (registered != null) { Path path = registered.get(fingerprint); if (path != null) { Path home = session .resolveAttribute(JGitSshClient.HOME_DIRECTORY); if (home != null && path.startsWith(home)) { try { path = home.relativize(path); String pathString = path.toString(); if (!pathString.isEmpty()) { return "~" + File.separator + pathString; //$NON-NLS-1$ } } catch (IllegalArgumentException e) { // Cannot be relativized. Ignore, and work with the // original path } } return path.toString(); } } return fingerprint; } private KeyPair loadKey(SessionContext session, Path path) throws IOException, GeneralSecurityException { if (!Files.exists(path)) { log.warn(format(SshdText.get().identityFileNotFound, path)); return null; } IoResource resource = getIoResource(session, path); if (cache == null) { return loadKey(session, resource, path, getPasswordFinder()); } Throwable[] t = { null }; KeyPair key = cache.get(path, p -> { try { return loadKey(session, resource, p, getPasswordFinder()); } catch (IOException | GeneralSecurityException e) { t[0] = e; return null; } }); if (t[0] != null) { if (t[0] instanceof CancellationException) { throw (CancellationException) t[0]; } throw new IOException( format(SshdText.get().keyLoadFailed, resource), t[0]); } return key; } private KeyPair loadKey(SessionContext session, NamedResource resource, Path path, FilePasswordProvider passwordProvider) throws IOException, GeneralSecurityException { try (InputStream stream = Files.newInputStream(path)) { Iterable ids = SecurityUtils.loadKeyPairIdentities(session, resource, stream, passwordProvider); if (ids == null) { throw new InvalidKeyException( format(SshdText.get().identityFileNoKey, path)); } Iterator keys = ids.iterator(); if (!keys.hasNext()) { throw new InvalidKeyException(format( SshdText.get().identityFileUnsupportedFormat, path)); } KeyPair result = keys.next(); PublicKey pk = result.getPublic(); if (pk != null) { Map registered = session .getAttribute(KEY_PATHS_BY_FINGERPRINT); if (registered == null) { registered = new HashMap<>(); session.setAttribute(KEY_PATHS_BY_FINGERPRINT, registered); } registered.put(KeyUtils.getFingerPrint(pk), path); } if (keys.hasNext()) { log.warn(format(SshdText.get().identityFileMultipleKeys, path)); keys.forEachRemaining(k -> { PrivateKey priv = k.getPrivate(); if (priv != null) { try { priv.destroy(); } catch (DestroyFailedException e) { // Ignore } } }); } return result; } } private class CancellingKeyPairIterator implements Iterator { private final SessionContext context; private final Iterator paths; private KeyPair nextItem; private boolean nextSet; public CancellingKeyPairIterator(SessionContext session, Collection resources) { List copy = new ArrayList<>(resources.size()); copy.addAll(resources); paths = copy.iterator(); context = session; } @Override public boolean hasNext() { if (nextSet) { return nextItem != null; } nextSet = true; while (nextItem == null && paths.hasNext()) { try { nextItem = loadKey(context, paths.next()); } catch (CancellationException cancelled) { throw cancelled; } catch (Exception other) { log.warn(other.getMessage(), other); } } return nextItem != null; } @Override public KeyPair next() { if (!nextSet && !hasNext()) { throw new NoSuchElementException(); } KeyPair result = nextItem; nextItem = null; nextSet = false; if (result == null) { throw new NoSuchElementException(); } return result; } } }