View Javadoc
1   /*
2    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.io.OutputStream;
49  import java.net.HttpURLConnection;
50  import java.security.AlgorithmParameters;
51  import java.security.GeneralSecurityException;
52  import java.security.spec.AlgorithmParameterSpec;
53  import java.security.spec.KeySpec;
54  import java.text.MessageFormat;
55  import java.util.Locale;
56  import java.util.Properties;
57  import java.util.regex.Matcher;
58  import java.util.regex.Pattern;
59  
60  import javax.crypto.Cipher;
61  import javax.crypto.CipherInputStream;
62  import javax.crypto.CipherOutputStream;
63  import javax.crypto.SecretKey;
64  import javax.crypto.SecretKeyFactory;
65  import javax.crypto.spec.IvParameterSpec;
66  import javax.crypto.spec.PBEKeySpec;
67  import javax.crypto.spec.PBEParameterSpec;
68  import javax.crypto.spec.SecretKeySpec;
69  import javax.xml.bind.DatatypeConverter;
70  
71  import org.eclipse.jgit.internal.JGitText;
72  import org.eclipse.jgit.util.Base64;
73  
74  abstract class WalkEncryption {
75  	static final WalkEncryption NONE = new NoEncryption();
76  
77  	static final String JETS3T_CRYPTO_VER = "jets3t-crypto-ver"; //$NON-NLS-1$
78  
79  	static final String JETS3T_CRYPTO_ALG = "jets3t-crypto-alg"; //$NON-NLS-1$
80  
81  	// Note: encrypt -> request state machine, step 1.
82  	abstract OutputStream encrypt(OutputStream output) throws IOException;
83  
84  	// Note: encrypt -> request state machine, step 2.
85  	abstract void request(HttpURLConnection conn, String prefix) throws IOException;
86  
87  	// Note: validate -> decrypt state machine, step 1.
88  	abstract void validate(HttpURLConnection conn, String prefix) throws IOException;
89  
90  	// Note: validate -> decrypt state machine, step 2.
91  	abstract InputStream decrypt(InputStream input) throws IOException;
92  
93  
94  	// TODO mixed ciphers
95  	// consider permitting mixed ciphers to facilitate algorithm migration
96  	// i.e. user keeps the password, but changes the algorithm
97  	// then existing remote entries will still be readable
98  	protected void validateImpl(final HttpURLConnection u, final String prefix,
99  			final String version, final String name) throws IOException {
100 		String v;
101 
102 		v = u.getHeaderField(prefix + JETS3T_CRYPTO_VER);
103 		if (v == null)
104 			v = ""; //$NON-NLS-1$
105 		if (!version.equals(v))
106 			throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionVersion, v));
107 
108 		v = u.getHeaderField(prefix + JETS3T_CRYPTO_ALG);
109 		if (v == null)
110 			v = ""; //$NON-NLS-1$
111 		// Standard names are not case-sensitive.
112 		// http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
113 		if (!name.equalsIgnoreCase(v))
114 			throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionAlgorithm, v));
115 	}
116 
117 	IOException error(final Throwable why) {
118 		final IOException e;
119 		e = new IOException(MessageFormat.format(JGitText.get().encryptionError, why.getMessage()));
120 		e.initCause(why);
121 		return e;
122 	}
123 
124 	private static class NoEncryption extends WalkEncryption {
125 		@Override
126 		void request(HttpURLConnection u, String prefix) {
127 			// Don't store any request properties.
128 		}
129 
130 		@Override
131 		void validate(final HttpURLConnection u, final String prefix)
132 				throws IOException {
133 			validateImpl(u, prefix, "", ""); //$NON-NLS-1$ //$NON-NLS-2$
134 		}
135 
136 		@Override
137 		InputStream decrypt(InputStream in) {
138 			return in;
139 		}
140 
141 		@Override
142 		OutputStream encrypt(OutputStream os) {
143 			return os;
144 		}
145 	}
146 
147 	/**
148 	 * JetS3t compatibility reference: <a href=
149 	 * "https://bitbucket.org/jmurty/jets3t/src/156c00eb160598c2e9937fd6873f00d3190e28ca/src/org/jets3t/service/security/EncryptionUtil.java">
150 	 * EncryptionUtil.java</a>
151 	 * <p>
152 	 * Note: EncryptionUtil is inadequate:
153 	 * <li>EncryptionUtil.isCipherAvailableForUse checks encryption only which
154 	 * "always works", but in JetS3t both encryption and decryption use non-IV
155 	 * aware algorithm parameters for all PBE specs, which breaks in case of AES
156 	 * <li>that means that only non-IV algorithms will work round trip in
157 	 * JetS3t, such as PBEWithMD5AndDES and PBEWithSHAAndTwofish-CBC
158 	 * <li>any AES based algorithms such as "PBE...With...And...AES" will not
159 	 * work, since they need proper IV setup
160 	 */
161 	static class JetS3tV2 extends WalkEncryption {
162 
163 		static final String VERSION = "2"; //$NON-NLS-1$
164 
165 		static final String ALGORITHM = "PBEWithMD5AndDES"; //$NON-NLS-1$
166 
167 		static final int ITERATIONS = 5000;
168 
169 		static final int KEY_SIZE = 32;
170 
171 		static final byte[] SALT = { //
172 				(byte) 0xA4, (byte) 0x0B, (byte) 0xC8, (byte) 0x34, //
173 				(byte) 0xD6, (byte) 0x95, (byte) 0xF3, (byte) 0x13 //
174 		};
175 
176 		// Size 16, see com.sun.crypto.provider.AESConstants.AES_BLOCK_SIZE
177 		static final byte[] ZERO_AES_IV = new byte[16];
178 
179 		private static final String CRYPTO_VER = VERSION;
180 
181 		private final String cryptoAlg;
182 
183 		private final SecretKey secretKey;
184 
185 		private final AlgorithmParameterSpec paramSpec;
186 
187 		JetS3tV2(final String algo, final String key)
188 				throws GeneralSecurityException {
189 			cryptoAlg = algo;
190 
191 			// Verify if cipher is present.
192 			Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
193 
194 			// Standard names are not case-sensitive.
195 			// http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
196 			String cryptoName = cryptoAlg.toUpperCase(Locale.ROOT);
197 
198 			if (!cryptoName.startsWith("PBE")) //$NON-NLS-1$
199 				throw new GeneralSecurityException(JGitText.get().encryptionOnlyPBE);
200 
201 			PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray(), SALT, ITERATIONS, KEY_SIZE);
202 			secretKey = SecretKeyFactory.getInstance(algo).generateSecret(keySpec);
203 
204 			// Detect algorithms which require initialization vector.
205 			boolean useIV = cryptoName.contains("AES"); //$NON-NLS-1$
206 
207 			// PBEParameterSpec algorithm parameters are supported from Java 8.
208 			if (useIV) {
209 				// Support IV where possible:
210 				// * since JCE provider uses random IV for PBE/AES
211 				// * and there is no place to store dynamic IV in JetS3t V2
212 				// * we use static IV, and tolerate increased security risk
213 				// TODO back port this change to JetS3t V2
214 				// See:
215 				// https://bitbucket.org/jmurty/jets3t/raw/156c00eb160598c2e9937fd6873f00d3190e28ca/src/org/jets3t/service/security/EncryptionUtil.java
216 				// http://cr.openjdk.java.net/~mullan/webrevs/ascarpin/webrev.00/raw_files/new/src/share/classes/com/sun/crypto/provider/PBES2Core.java
217 				IvParameterSpec paramIV = new IvParameterSpec(ZERO_AES_IV);
218 				paramSpec = new PBEParameterSpec(SALT, ITERATIONS, paramIV);
219 			} else {
220 				// Strict legacy JetS3t V2 compatibility, with no IV support.
221 				paramSpec = new PBEParameterSpec(SALT, ITERATIONS);
222 			}
223 
224 			// Verify if cipher + key are allowed by policy.
225 			cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
226 			cipher.doFinal();
227 		}
228 
229 		@Override
230 		void request(final HttpURLConnection u, final String prefix) {
231 			u.setRequestProperty(prefix + JETS3T_CRYPTO_VER, CRYPTO_VER);
232 			u.setRequestProperty(prefix + JETS3T_CRYPTO_ALG, cryptoAlg);
233 		}
234 
235 		@Override
236 		void validate(final HttpURLConnection u, final String prefix)
237 				throws IOException {
238 			validateImpl(u, prefix, CRYPTO_VER, cryptoAlg);
239 		}
240 
241 		@Override
242 		OutputStream encrypt(final OutputStream os) throws IOException {
243 			try {
244 				final Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
245 				cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
246 				return new CipherOutputStream(os, cipher);
247 			} catch (GeneralSecurityException e) {
248 				throw error(e);
249 			}
250 		}
251 
252 		@Override
253 		InputStream decrypt(final InputStream in) throws IOException {
254 			try {
255 				final Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
256 				cipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec);
257 				return new CipherInputStream(in, cipher);
258 			} catch (GeneralSecurityException e) {
259 				throw error(e);
260 			}
261 		}
262 	}
263 
264 	/** Encryption property names. */
265 	interface Keys {
266 		// Remote S3 meta: V1 algorithm name or V2 profile name.
267 		String JGIT_PROFILE = "jgit-crypto-profile"; //$NON-NLS-1$
268 
269 		// Remote S3 meta: JGit encryption implementation version.
270 		String JGIT_VERSION = "jgit-crypto-version"; //$NON-NLS-1$
271 
272 		// Remote S3 meta: base-64 encoded cipher algorithm parameters.
273 		String JGIT_CONTEXT = "jgit-crypto-context"; //$NON-NLS-1$
274 
275 		// Amazon S3 connection configuration file profile property suffixes:
276 		String X_ALGO = ".algo"; //$NON-NLS-1$
277 		String X_KEY_ALGO = ".key.algo"; //$NON-NLS-1$
278 		String X_KEY_SIZE = ".key.size"; //$NON-NLS-1$
279 		String X_KEY_ITER = ".key.iter"; //$NON-NLS-1$
280 		String X_KEY_SALT = ".key.salt"; //$NON-NLS-1$
281 	}
282 
283 	/** Encryption constants and defaults. */
284 	interface Vals {
285 		// Compatibility defaults.
286 		String DEFAULT_VERS = "0"; //$NON-NLS-1$
287 		String DEFAULT_ALGO = JetS3tV2.ALGORITHM;
288 		String DEFAULT_KEY_ALGO = JetS3tV2.ALGORITHM;
289 		String DEFAULT_KEY_SIZE = Integer.toString(JetS3tV2.KEY_SIZE);
290 		String DEFAULT_KEY_ITER = Integer.toString(JetS3tV2.ITERATIONS);
291 		String DEFAULT_KEY_SALT = DatatypeConverter.printHexBinary(JetS3tV2.SALT);
292 
293 		String EMPTY = ""; //$NON-NLS-1$
294 
295 		// Match white space.
296 		String REGEX_WS = "\\s+"; //$NON-NLS-1$
297 
298 		// Match PBE ciphers, i.e: PBEWithMD5AndDES
299 		String REGEX_PBE = "(PBE).*(WITH).+(AND).+"; //$NON-NLS-1$
300 
301 		// Match transformation ciphers, i.e: AES/CBC/PKCS5Padding
302 		String REGEX_TRANS = "(.+)/(.+)/(.+)"; //$NON-NLS-1$
303 	}
304 
305 	static GeneralSecurityException securityError(String message) {
306 		return new GeneralSecurityException(
307 				MessageFormat.format(JGitText.get().encryptionError, message));
308 	}
309 
310 	/**
311 	 * Base implementation of JGit symmetric encryption. Supports V2 properties
312 	 * format.
313 	 */
314 	static abstract class SymmetricEncryption extends WalkEncryption
315 			implements Keys, Vals {
316 
317 		/** Encryption profile, root name of group of related properties. */
318 		final String profile;
319 
320 		/** Encryption version, reflects actual implementation class. */
321 		final String version;
322 
323 		/** Full cipher algorithm name. */
324 		final String cipherAlgo;
325 
326 		/** Cipher algorithm name for parameters lookup. */
327 		final String paramsAlgo;
328 
329 		/** Generated secret key. */
330 		final SecretKey secretKey;
331 
332 		SymmetricEncryption(Properties props) throws GeneralSecurityException {
333 
334 			profile = props.getProperty(AmazonS3.Keys.CRYPTO_ALG);
335 			version = props.getProperty(AmazonS3.Keys.CRYPTO_VER);
336 			String pass = props.getProperty(AmazonS3.Keys.PASSWORD);
337 
338 			cipherAlgo = props.getProperty(profile + X_ALGO, DEFAULT_ALGO);
339 
340 			String keyAlgo = props.getProperty(profile + X_KEY_ALGO, DEFAULT_KEY_ALGO);
341 			String keySize = props.getProperty(profile + X_KEY_SIZE, DEFAULT_KEY_SIZE);
342 			String keyIter = props.getProperty(profile + X_KEY_ITER, DEFAULT_KEY_ITER);
343 			String keySalt = props.getProperty(profile + X_KEY_SALT, DEFAULT_KEY_SALT);
344 
345 			// Verify if cipher is present.
346 			Cipher cipher = InsecureCipherFactory.create(cipherAlgo);
347 
348 			// Verify if key factory is present.
349 			SecretKeyFactory factory = SecretKeyFactory.getInstance(keyAlgo);
350 
351 			final int size;
352 			try {
353 				size = Integer.parseInt(keySize);
354 			} catch (Exception e) {
355 				throw securityError(X_KEY_SIZE + EMPTY + keySize);
356 			}
357 
358 			final int iter;
359 			try {
360 				iter = Integer.parseInt(keyIter);
361 			} catch (Exception e) {
362 				throw securityError(X_KEY_ITER + EMPTY + keyIter);
363 			}
364 
365 			final byte[] salt;
366 			try {
367 				salt = DatatypeConverter
368 						.parseHexBinary(keySalt.replaceAll(REGEX_WS, EMPTY));
369 			} catch (Exception e) {
370 				throw securityError(X_KEY_SALT + EMPTY + keySalt);
371 			}
372 
373 			KeySpec keySpec = new PBEKeySpec(pass.toCharArray(), salt, iter, size);
374 
375 			SecretKey keyBase = factory.generateSecret(keySpec);
376 
377 			String name = cipherAlgo.toUpperCase(Locale.ROOT);
378 			Matcher matcherPBE = Pattern.compile(REGEX_PBE).matcher(name);
379 			Matcher matcherTrans = Pattern.compile(REGEX_TRANS).matcher(name);
380 			if (matcherPBE.matches()) {
381 				paramsAlgo = cipherAlgo;
382 				secretKey = keyBase;
383 			} else if (matcherTrans.find()) {
384 				paramsAlgo = matcherTrans.group(1);
385 				secretKey = new SecretKeySpec(keyBase.getEncoded(), paramsAlgo);
386 			} else {
387 				throw new GeneralSecurityException(MessageFormat.format(
388 						JGitText.get().unsupportedEncryptionAlgorithm,
389 						cipherAlgo));
390 			}
391 
392 			// Verify if cipher + key are allowed by policy.
393 			cipher.init(Cipher.ENCRYPT_MODE, secretKey);
394 			cipher.doFinal();
395 
396 		}
397 
398 		// Shared state encrypt -> request.
399 		volatile String context;
400 
401 		@Override
402 		OutputStream encrypt(OutputStream output) throws IOException {
403 			try {
404 				Cipher cipher = InsecureCipherFactory.create(cipherAlgo);
405 				cipher.init(Cipher.ENCRYPT_MODE, secretKey);
406 				AlgorithmParameters params = cipher.getParameters();
407 				if (params == null) {
408 					context = EMPTY;
409 				} else {
410 					context = Base64.encodeBytes(params.getEncoded());
411 				}
412 				return new CipherOutputStream(output, cipher);
413 			} catch (Exception e) {
414 				throw error(e);
415 			}
416 		}
417 
418 		@Override
419 		void request(HttpURLConnection conn, String prefix) throws IOException {
420 			conn.setRequestProperty(prefix + JGIT_PROFILE, profile);
421 			conn.setRequestProperty(prefix + JGIT_VERSION, version);
422 			conn.setRequestProperty(prefix + JGIT_CONTEXT, context);
423 			// No cleanup:
424 			// single encrypt can be followed by several request
425 			// from the AmazonS3.putImpl() multiple retry attempts
426 			// context = null; // Cleanup encrypt -> request transition.
427 			// TODO re-factor AmazonS3.putImpl to be more transaction-like
428 		}
429 
430 		// Shared state validate -> decrypt.
431 		volatile Cipher decryptCipher;
432 
433 		@Override
434 		void validate(HttpURLConnection conn, String prefix)
435 				throws IOException {
436 			String prof = conn.getHeaderField(prefix + JGIT_PROFILE);
437 			String vers = conn.getHeaderField(prefix + JGIT_VERSION);
438 			String cont = conn.getHeaderField(prefix + JGIT_CONTEXT);
439 
440 			if (prof == null) {
441 				throw new IOException(MessageFormat
442 						.format(JGitText.get().encryptionError, JGIT_PROFILE));
443 			}
444 			if (vers == null) {
445 				throw new IOException(MessageFormat
446 						.format(JGitText.get().encryptionError, JGIT_VERSION));
447 			}
448 			if (cont == null) {
449 				throw new IOException(MessageFormat
450 						.format(JGitText.get().encryptionError, JGIT_CONTEXT));
451 			}
452 			if (!profile.equals(prof)) {
453 				throw new IOException(MessageFormat.format(
454 						JGitText.get().unsupportedEncryptionAlgorithm, prof));
455 			}
456 			if (!version.equals(vers)) {
457 				throw new IOException(MessageFormat.format(
458 						JGitText.get().unsupportedEncryptionVersion, vers));
459 			}
460 			try {
461 				decryptCipher = InsecureCipherFactory.create(cipherAlgo);
462 				if (cont.isEmpty()) {
463 					decryptCipher.init(Cipher.DECRYPT_MODE, secretKey);
464 				} else {
465 					AlgorithmParameters params = AlgorithmParameters
466 							.getInstance(paramsAlgo);
467 					params.init(Base64.decode(cont));
468 					decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, params);
469 				}
470 			} catch (Exception e) {
471 				throw error(e);
472 			}
473 		}
474 
475 		@Override
476 		InputStream decrypt(InputStream input) throws IOException {
477 			try {
478 				return new CipherInputStream(input, decryptCipher);
479 			} finally {
480 				decryptCipher = null; // Cleanup validate -> decrypt transition.
481 			}
482 		}
483 	}
484 
485 	/**
486 	 * Provides JetS3t-like encryption with AES support. Uses V1 connection file
487 	 * format. For reference, see: 'jgit-s3-connection-v-1.properties'.
488 	 */
489 	static class JGitV1 extends SymmetricEncryption {
490 
491 		static final String VERSION = "1"; //$NON-NLS-1$
492 
493 		// Re-map connection properties V1 -> V2.
494 		static Properties wrap(String algo, String pass) {
495 			Properties props = new Properties();
496 			props.put(AmazonS3.Keys.CRYPTO_ALG, algo);
497 			props.put(AmazonS3.Keys.CRYPTO_VER, VERSION);
498 			props.put(AmazonS3.Keys.PASSWORD, pass);
499 			props.put(algo + Keys.X_ALGO, algo);
500 			props.put(algo + Keys.X_KEY_ALGO, algo);
501 			props.put(algo + Keys.X_KEY_ITER, DEFAULT_KEY_ITER);
502 			props.put(algo + Keys.X_KEY_SIZE, DEFAULT_KEY_SIZE);
503 			props.put(algo + Keys.X_KEY_SALT, DEFAULT_KEY_SALT);
504 			return props;
505 		}
506 
507 		JGitV1(String algo, String pass)
508 				throws GeneralSecurityException {
509 			super(wrap(algo, pass));
510 			String name = cipherAlgo.toUpperCase(Locale.ROOT);
511 			Matcher matcherPBE = Pattern.compile(REGEX_PBE).matcher(name);
512 			if (!matcherPBE.matches())
513 				throw new GeneralSecurityException(
514 						JGitText.get().encryptionOnlyPBE);
515 		}
516 
517 	}
518 
519 	/**
520 	 * Supports both PBE and non-PBE algorithms. Uses V2 connection file format.
521 	 * For reference, see: 'jgit-s3-connection-v-2.properties'.
522 	 */
523 	static class JGitV2 extends SymmetricEncryption {
524 
525 		static final String VERSION = "2"; //$NON-NLS-1$
526 
527 		JGitV2(Properties props)
528 				throws GeneralSecurityException {
529 			super(props);
530 		}
531 	}
532 
533 	/**
534 	 * Encryption factory.
535 	 *
536 	 * @param props
537 	 * @return instance
538 	 * @throws GeneralSecurityException
539 	 */
540 	static WalkEncryption instance(Properties props)
541 			throws GeneralSecurityException {
542 
543 		String algo = props.getProperty(AmazonS3.Keys.CRYPTO_ALG, Vals.DEFAULT_ALGO);
544 		String vers = props.getProperty(AmazonS3.Keys.CRYPTO_VER, Vals.DEFAULT_VERS);
545 		String pass = props.getProperty(AmazonS3.Keys.PASSWORD);
546 
547 		if (pass == null) // Disable encryption.
548 			return WalkEncryption.NONE;
549 
550 		switch (vers) {
551 		case Vals.DEFAULT_VERS:
552 			return new JetS3tV2(algo, pass);
553 		case JGitV1.VERSION:
554 			return new JGitV1(algo, pass);
555 		case JGitV2.VERSION:
556 			return new JGitV2(props);
557 		default:
558 			throw new GeneralSecurityException(MessageFormat.format(
559 					JGitText.get().unsupportedEncryptionVersion, vers));
560 		}
561 	}
562 }