1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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.Properties;
56 import java.util.regex.Matcher;
57 import java.util.regex.Pattern;
58
59 import javax.crypto.Cipher;
60 import javax.crypto.CipherInputStream;
61 import javax.crypto.CipherOutputStream;
62 import javax.crypto.SecretKey;
63 import javax.crypto.SecretKeyFactory;
64 import javax.crypto.spec.IvParameterSpec;
65 import javax.crypto.spec.PBEKeySpec;
66 import javax.crypto.spec.PBEParameterSpec;
67 import javax.crypto.spec.SecretKeySpec;
68 import javax.xml.bind.DatatypeConverter;
69
70 import org.eclipse.jgit.internal.JGitText;
71 import org.eclipse.jgit.util.Base64;
72
73 abstract class WalkEncryption {
74 static final WalkEncryption NONE = new NoEncryption();
75
76 static final String JETS3T_CRYPTO_VER = "jets3t-crypto-ver";
77
78 static final String JETS3T_CRYPTO_ALG = "jets3t-crypto-alg";
79
80
81 abstract OutputStream encrypt(OutputStream output) throws IOException;
82
83
84 abstract void request(HttpURLConnection conn, String prefix) throws IOException;
85
86
87 abstract void validate(HttpURLConnection conn, String prefix) throws IOException;
88
89
90 abstract InputStream decrypt(InputStream input) throws IOException;
91
92
93
94
95
96
97 protected void validateImpl(final HttpURLConnection u, final String prefix,
98 final String version, final String name) throws IOException {
99 String v;
100
101 v = u.getHeaderField(prefix + JETS3T_CRYPTO_VER);
102 if (v == null)
103 v = "";
104 if (!version.equals(v))
105 throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionVersion, v));
106
107 v = u.getHeaderField(prefix + JETS3T_CRYPTO_ALG);
108 if (v == null)
109 v = "";
110
111
112 if (!name.equalsIgnoreCase(v))
113 throw new IOException(MessageFormat.format(JGitText.get().unsupportedEncryptionAlgorithm, v));
114 }
115
116 IOException error(final Throwable why) {
117 final IOException e;
118 e = new IOException(MessageFormat.format(JGitText.get().encryptionError, why.getMessage()));
119 e.initCause(why);
120 return e;
121 }
122
123 private static class NoEncryption extends WalkEncryption {
124 @Override
125 void request(HttpURLConnection u, String prefix) {
126
127 }
128
129 @Override
130 void validate(final HttpURLConnection u, final String prefix)
131 throws IOException {
132 validateImpl(u, prefix, "", "");
133 }
134
135 @Override
136 InputStream decrypt(InputStream in) {
137 return in;
138 }
139
140 @Override
141 OutputStream encrypt(OutputStream os) {
142 return os;
143 }
144 }
145
146
147
148 static PBEParameterSpec java7PBEParameterSpec(byte[] salt,
149 int iterationCount) {
150 return new PBEParameterSpec(salt, iterationCount);
151 }
152
153
154
155 static PBEParameterSpec java8PBEParameterSpec(byte[] salt,
156 int iterationCount, AlgorithmParameterSpec paramSpec) {
157 try {
158 @SuppressWarnings("boxing")
159 PBEParameterSpec instance = PBEParameterSpec.class
160 .getConstructor(byte[].class, int.class,
161 AlgorithmParameterSpec.class)
162 .newInstance(salt, iterationCount, paramSpec);
163 return instance;
164 } catch (Exception e) {
165 throw new RuntimeException(e);
166 }
167 }
168
169
170
171 static double javaVersion() {
172 return Double.parseDouble(System.getProperty("java.specification.version"));
173 }
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189 static class JetS3tV2 extends WalkEncryption {
190
191 static final String VERSION = "2";
192
193 static final String ALGORITHM = "PBEWithMD5AndDES";
194
195 static final int ITERATIONS = 5000;
196
197 static final int KEY_SIZE = 32;
198
199 static final byte[] SALT = {
200 (byte) 0xA4, (byte) 0x0B, (byte) 0xC8, (byte) 0x34,
201 (byte) 0xD6, (byte) 0x95, (byte) 0xF3, (byte) 0x13
202 };
203
204
205 static final byte[] ZERO_AES_IV = new byte[16];
206
207 private static final String cryptoVer = VERSION;
208
209 private final String cryptoAlg;
210
211 private final SecretKey secretKey;
212
213 private final AlgorithmParameterSpec paramSpec;
214
215 JetS3tV2(final String algo, final String key)
216 throws GeneralSecurityException {
217 cryptoAlg = algo;
218
219
220 Cipher cipher = Cipher.getInstance(cryptoAlg);
221
222
223
224 String cryptoName = cryptoAlg.toUpperCase();
225
226 if (!cryptoName.startsWith("PBE"))
227 throw new GeneralSecurityException(JGitText.get().encryptionOnlyPBE);
228
229 PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray(), SALT, ITERATIONS, KEY_SIZE);
230 secretKey = SecretKeyFactory.getInstance(algo).generateSecret(keySpec);
231
232
233 boolean useIV = cryptoName.contains("AES");
234
235
236 boolean isJava8 = javaVersion() >= 1.8;
237
238 if (useIV && isJava8) {
239
240
241
242
243
244
245
246
247 IvParameterSpec paramIV = new IvParameterSpec(ZERO_AES_IV);
248 paramSpec = java8PBEParameterSpec(SALT, ITERATIONS, paramIV);
249 } else {
250
251 paramSpec = java7PBEParameterSpec(SALT, ITERATIONS);
252 }
253
254
255 cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
256 cipher.doFinal();
257
258 }
259
260 @Override
261 void request(final HttpURLConnection u, final String prefix) {
262 u.setRequestProperty(prefix + JETS3T_CRYPTO_VER, cryptoVer);
263 u.setRequestProperty(prefix + JETS3T_CRYPTO_ALG, cryptoAlg);
264 }
265
266 @Override
267 void validate(final HttpURLConnection u, final String prefix)
268 throws IOException {
269 validateImpl(u, prefix, cryptoVer, cryptoAlg);
270 }
271
272 @Override
273 OutputStream encrypt(final OutputStream os) throws IOException {
274 try {
275 final Cipher cipher = Cipher.getInstance(cryptoAlg);
276 cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
277 return new CipherOutputStream(os, cipher);
278 } catch (GeneralSecurityException e) {
279 throw error(e);
280 }
281 }
282
283 @Override
284 InputStream decrypt(final InputStream in) throws IOException {
285 try {
286 final Cipher cipher = Cipher.getInstance(cryptoAlg);
287 cipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec);
288 return new CipherInputStream(in, cipher);
289 } catch (GeneralSecurityException e) {
290 throw error(e);
291 }
292 }
293 }
294
295
296 interface Keys {
297
298 String JGIT_PROFILE = "jgit-crypto-profile";
299
300
301 String JGIT_VERSION = "jgit-crypto-version";
302
303
304 String JGIT_CONTEXT = "jgit-crypto-context";
305
306
307 String X_ALGO = ".algo";
308 String X_KEY_ALGO = ".key.algo";
309 String X_KEY_SIZE = ".key.size";
310 String X_KEY_ITER = ".key.iter";
311 String X_KEY_SALT = ".key.salt";
312 }
313
314
315 interface Vals {
316
317 String DEFAULT_VERS = "0";
318 String DEFAULT_ALGO = JetS3tV2.ALGORITHM;
319 String DEFAULT_KEY_ALGO = JetS3tV2.ALGORITHM;
320 String DEFAULT_KEY_SIZE = Integer.toString(JetS3tV2.KEY_SIZE);
321 String DEFAULT_KEY_ITER = Integer.toString(JetS3tV2.ITERATIONS);
322 String DEFAULT_KEY_SALT = DatatypeConverter.printHexBinary(JetS3tV2.SALT);
323
324 String EMPTY = "";
325
326
327 String REGEX_WS = "\\s+";
328
329
330 String REGEX_PBE = "(PBE).*(WITH).+(AND).+";
331
332
333 String REGEX_TRANS = "(.+)/(.+)/(.+)";
334 }
335
336 static GeneralSecurityException securityError(String message) {
337 return new GeneralSecurityException(
338 MessageFormat.format(JGitText.get().encryptionError, message));
339 }
340
341
342
343
344
345 static abstract class SymmetricEncryption extends WalkEncryption
346 implements Keys, Vals {
347
348
349 final String profile;
350
351
352 final String version;
353
354
355 final String cipherAlgo;
356
357
358 final String paramsAlgo;
359
360
361 final SecretKey secretKey;
362
363 SymmetricEncryption(Properties props) throws GeneralSecurityException {
364
365 profile = props.getProperty(AmazonS3.Keys.CRYPTO_ALG);
366 version = props.getProperty(AmazonS3.Keys.CRYPTO_VER);
367 String pass = props.getProperty(AmazonS3.Keys.PASSWORD);
368
369 cipherAlgo = props.getProperty(profile + X_ALGO, DEFAULT_ALGO);
370
371 String keyAlgo = props.getProperty(profile + X_KEY_ALGO, DEFAULT_KEY_ALGO);
372 String keySize = props.getProperty(profile + X_KEY_SIZE, DEFAULT_KEY_SIZE);
373 String keyIter = props.getProperty(profile + X_KEY_ITER, DEFAULT_KEY_ITER);
374 String keySalt = props.getProperty(profile + X_KEY_SALT, DEFAULT_KEY_SALT);
375
376
377 Cipher cipher = Cipher.getInstance(cipherAlgo);
378
379
380 SecretKeyFactory factory = SecretKeyFactory.getInstance(keyAlgo);
381
382 final int size;
383 try {
384 size = Integer.parseInt(keySize);
385 } catch (Exception e) {
386 throw securityError(X_KEY_SIZE + EMPTY + keySize);
387 }
388
389 final int iter;
390 try {
391 iter = Integer.parseInt(keyIter);
392 } catch (Exception e) {
393 throw securityError(X_KEY_ITER + EMPTY + keyIter);
394 }
395
396 final byte[] salt;
397 try {
398 salt = DatatypeConverter
399 .parseHexBinary(keySalt.replaceAll(REGEX_WS, EMPTY));
400 } catch (Exception e) {
401 throw securityError(X_KEY_SALT + EMPTY + keySalt);
402 }
403
404 KeySpec keySpec = new PBEKeySpec(pass.toCharArray(), salt, iter, size);
405
406 SecretKey keyBase = factory.generateSecret(keySpec);
407
408 String name = cipherAlgo.toUpperCase();
409 Matcher matcherPBE = Pattern.compile(REGEX_PBE).matcher(name);
410 Matcher matcherTrans = Pattern.compile(REGEX_TRANS).matcher(name);
411 if (matcherPBE.matches()) {
412 paramsAlgo = cipherAlgo;
413 secretKey = keyBase;
414 } else if (matcherTrans.find()) {
415 paramsAlgo = matcherTrans.group(1);
416 secretKey = new SecretKeySpec(keyBase.getEncoded(), paramsAlgo);
417 } else {
418 throw new GeneralSecurityException(MessageFormat.format(
419 JGitText.get().unsupportedEncryptionAlgorithm,
420 cipherAlgo));
421 }
422
423
424 cipher.init(Cipher.ENCRYPT_MODE, secretKey);
425 cipher.doFinal();
426
427 }
428
429
430 volatile String context;
431
432 @Override
433 OutputStream encrypt(OutputStream output) throws IOException {
434 try {
435 Cipher cipher = Cipher.getInstance(cipherAlgo);
436 cipher.init(Cipher.ENCRYPT_MODE, secretKey);
437 AlgorithmParameters params = cipher.getParameters();
438 if (params == null) {
439 context = EMPTY;
440 } else {
441 context = Base64.encodeBytes(params.getEncoded());
442 }
443 return new CipherOutputStream(output, cipher);
444 } catch (Exception e) {
445 throw error(e);
446 }
447 }
448
449 @Override
450 void request(HttpURLConnection conn, String prefix) throws IOException {
451 conn.setRequestProperty(prefix + JGIT_PROFILE, profile);
452 conn.setRequestProperty(prefix + JGIT_VERSION, version);
453 conn.setRequestProperty(prefix + JGIT_CONTEXT, context);
454
455
456
457
458
459 }
460
461
462 volatile Cipher decryptCipher;
463
464 @Override
465 void validate(HttpURLConnection conn, String prefix)
466 throws IOException {
467 String prof = conn.getHeaderField(prefix + JGIT_PROFILE);
468 String vers = conn.getHeaderField(prefix + JGIT_VERSION);
469 String cont = conn.getHeaderField(prefix + JGIT_CONTEXT);
470
471 if (prof == null) {
472 throw new IOException(MessageFormat
473 .format(JGitText.get().encryptionError, JGIT_PROFILE));
474 }
475 if (vers == null) {
476 throw new IOException(MessageFormat
477 .format(JGitText.get().encryptionError, JGIT_VERSION));
478 }
479 if (cont == null) {
480 throw new IOException(MessageFormat
481 .format(JGitText.get().encryptionError, JGIT_CONTEXT));
482 }
483 if (!profile.equals(prof)) {
484 throw new IOException(MessageFormat.format(
485 JGitText.get().unsupportedEncryptionAlgorithm, prof));
486 }
487 if (!version.equals(vers)) {
488 throw new IOException(MessageFormat.format(
489 JGitText.get().unsupportedEncryptionVersion, vers));
490 }
491 try {
492 decryptCipher = Cipher.getInstance(cipherAlgo);
493 if (cont.isEmpty()) {
494 decryptCipher.init(Cipher.DECRYPT_MODE, secretKey);
495 } else {
496 AlgorithmParameters params = AlgorithmParameters
497 .getInstance(paramsAlgo);
498 params.init(Base64.decode(cont));
499 decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, params);
500 }
501 } catch (Exception e) {
502 throw error(e);
503 }
504 }
505
506 @Override
507 InputStream decrypt(InputStream input) throws IOException {
508 try {
509 return new CipherInputStream(input, decryptCipher);
510 } finally {
511 decryptCipher = null;
512 }
513 }
514 }
515
516
517
518
519
520 static class JGitV1 extends SymmetricEncryption {
521
522 static final String VERSION = "1";
523
524
525 static Properties wrap(String algo, String pass) {
526 Properties props = new Properties();
527 props.put(AmazonS3.Keys.CRYPTO_ALG, algo);
528 props.put(AmazonS3.Keys.CRYPTO_VER, VERSION);
529 props.put(AmazonS3.Keys.PASSWORD, pass);
530 props.put(algo + Keys.X_ALGO, algo);
531 props.put(algo + Keys.X_KEY_ALGO, algo);
532 props.put(algo + Keys.X_KEY_ITER, DEFAULT_KEY_ITER);
533 props.put(algo + Keys.X_KEY_SIZE, DEFAULT_KEY_SIZE);
534 props.put(algo + Keys.X_KEY_SALT, DEFAULT_KEY_SALT);
535 return props;
536 }
537
538 JGitV1(String algo, String pass)
539 throws GeneralSecurityException {
540 super(wrap(algo, pass));
541 String name = cipherAlgo.toUpperCase();
542 Matcher matcherPBE = Pattern.compile(REGEX_PBE).matcher(name);
543 if (!matcherPBE.matches())
544 throw new GeneralSecurityException(
545 JGitText.get().encryptionOnlyPBE);
546 }
547
548 }
549
550
551
552
553
554 static class JGitV2 extends SymmetricEncryption {
555
556 static final String VERSION = "2";
557
558 JGitV2(Properties props)
559 throws GeneralSecurityException {
560 super(props);
561 }
562 }
563
564
565
566
567
568
569
570
571 static WalkEncryption instance(Properties props)
572 throws GeneralSecurityException {
573
574 String algo = props.getProperty(AmazonS3.Keys.CRYPTO_ALG, Vals.DEFAULT_ALGO);
575 String vers = props.getProperty(AmazonS3.Keys.CRYPTO_VER, Vals.DEFAULT_VERS);
576 String pass = props.getProperty(AmazonS3.Keys.PASSWORD);
577
578 if (pass == null)
579 return WalkEncryption.NONE;
580
581 switch (vers) {
582 case Vals.DEFAULT_VERS:
583 return new JetS3tV2(algo, pass);
584 case JGitV1.VERSION:
585 return new JGitV1(algo, pass);
586 case JGitV2.VERSION:
587 return new JGitV2(props);
588 default:
589 throw new GeneralSecurityException(MessageFormat.format(
590 JGitText.get().unsupportedEncryptionVersion, vers));
591 }
592 }
593 }