View Javadoc
1   /*
2    * Copyright (C) 2018, 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  
12  import static java.text.MessageFormat.format;
13  
14  import java.io.IOException;
15  import java.io.InputStream;
16  import java.nio.file.Files;
17  import java.nio.file.Path;
18  import java.security.GeneralSecurityException;
19  import java.security.InvalidKeyException;
20  import java.security.KeyPair;
21  import java.security.PrivateKey;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.NoSuchElementException;
28  import java.util.concurrent.CancellationException;
29  
30  import javax.security.auth.DestroyFailedException;
31  
32  import org.apache.sshd.common.NamedResource;
33  import org.apache.sshd.common.config.keys.FilePasswordProvider;
34  import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
35  import org.apache.sshd.common.session.SessionContext;
36  import org.apache.sshd.common.util.io.resource.IoResource;
37  import org.apache.sshd.common.util.security.SecurityUtils;
38  import org.eclipse.jgit.transport.sshd.KeyCache;
39  
40  /**
41   * A {@link FileKeyPairProvider} that uses an external {@link KeyCache}.
42   */
43  public class CachingKeyPairProvider extends FileKeyPairProvider
44  		implements Iterable<KeyPair> {
45  
46  	private final KeyCache cache;
47  
48  	/**
49  	 * Creates a new {@link CachingKeyPairProvider} using the given
50  	 * {@link KeyCache}. If the cache is {@code null}, this is a simple
51  	 * {@link FileKeyPairProvider}.
52  	 *
53  	 * @param paths
54  	 *            to load keys from
55  	 * @param cache
56  	 *            to use, may be {@code null} if no external caching is desired
57  	 */
58  	public CachingKeyPairProvider(List<Path> paths, KeyCache cache) {
59  		super(paths);
60  		this.cache = cache;
61  	}
62  
63  	@Override
64  	public Iterator<KeyPair> iterator() {
65  		return iterator(null);
66  	}
67  
68  	private Iterator<KeyPair> iterator(SessionContext session) {
69  		Collection<? extends Path> resources = getPaths();
70  		if (resources.isEmpty()) {
71  			return Collections.emptyListIterator();
72  		}
73  		return new CancellingKeyPairIterator(session, resources);
74  	}
75  
76  	@Override
77  	public Iterable<KeyPair> loadKeys(SessionContext session) {
78  		return () -> iterator(session);
79  	}
80  
81  	private KeyPair loadKey(SessionContext session, Path path)
82  			throws IOException, GeneralSecurityException {
83  		if (!Files.exists(path)) {
84  			log.warn(format(SshdText.get().identityFileNotFound, path));
85  			return null;
86  		}
87  		IoResource<Path> resource = getIoResource(session, path);
88  		if (cache == null) {
89  			return loadKey(session, resource, path, getPasswordFinder());
90  		}
91  		Throwable[] t = { null };
92  		KeyPair key = cache.get(path, p -> {
93  			try {
94  				return loadKey(session, resource, p, getPasswordFinder());
95  			} catch (IOException | GeneralSecurityException e) {
96  				t[0] = e;
97  				return null;
98  			}
99  		});
100 		if (t[0] != null) {
101 			if (t[0] instanceof CancellationException) {
102 				throw (CancellationException) t[0];
103 			}
104 			throw new IOException(
105 					format(SshdText.get().keyLoadFailed, resource), t[0]);
106 		}
107 		return key;
108 	}
109 
110 	private KeyPair loadKey(SessionContext session, NamedResource resource,
111 			Path path, FilePasswordProvider passwordProvider)
112 			throws IOException, GeneralSecurityException {
113 		try (InputStream stream = Files.newInputStream(path)) {
114 			Iterable<KeyPair> ids = SecurityUtils.loadKeyPairIdentities(session,
115 					resource, stream, passwordProvider);
116 			if (ids == null) {
117 				throw new InvalidKeyException(
118 						format(SshdText.get().identityFileNoKey, path));
119 			}
120 			Iterator<KeyPair> keys = ids.iterator();
121 			if (!keys.hasNext()) {
122 				throw new InvalidKeyException(format(
123 						SshdText.get().identityFileUnsupportedFormat, path));
124 			}
125 			KeyPair result = keys.next();
126 			if (keys.hasNext()) {
127 				log.warn(format(SshdText.get().identityFileMultipleKeys, path));
128 				keys.forEachRemaining(k -> {
129 					PrivateKey pk = k.getPrivate();
130 					if (pk != null) {
131 						try {
132 							pk.destroy();
133 						} catch (DestroyFailedException e) {
134 							// Ignore
135 						}
136 					}
137 				});
138 			}
139 			return result;
140 		}
141 	}
142 
143 	private class CancellingKeyPairIterator implements Iterator<KeyPair> {
144 
145 		private final SessionContext context;
146 
147 		private final Iterator<Path> paths;
148 
149 		private KeyPair nextItem;
150 
151 		private boolean nextSet;
152 
153 		public CancellingKeyPairIterator(SessionContext session,
154 				Collection<? extends Path> resources) {
155 			List<Path> copy = new ArrayList<>(resources.size());
156 			copy.addAll(resources);
157 			paths = copy.iterator();
158 			context = session;
159 		}
160 
161 		@Override
162 		public boolean hasNext() {
163 			if (nextSet) {
164 				return nextItem != null;
165 			}
166 			nextSet = true;
167 			while (nextItem == null && paths.hasNext()) {
168 				try {
169 					nextItem = loadKey(context, paths.next());
170 				} catch (CancellationException cancelled) {
171 					throw cancelled;
172 				} catch (Exception other) {
173 					log.warn(other.getMessage(), other);
174 				}
175 			}
176 			return nextItem != null;
177 		}
178 
179 		@Override
180 		public KeyPair next() {
181 			if (!nextSet && !hasNext()) {
182 				throw new NoSuchElementException();
183 			}
184 			KeyPair result = nextItem;
185 			nextItem = null;
186 			nextSet = false;
187 			if (result == null) {
188 				throw new NoSuchElementException();
189 			}
190 			return result;
191 		}
192 
193 	}
194 }