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