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.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";
78
79 static final String JETS3T_CRYPTO_ALG = "jets3t-crypto-alg";
80
81
82 abstract OutputStream encrypt(OutputStream output) throws IOException;
83
84
85 abstract void request(HttpURLConnection conn, String prefix) throws IOException;
86
87
88 abstract void validate(HttpURLConnection conn, String prefix) throws IOException;
89
90
91 abstract InputStream decrypt(InputStream input) throws IOException;
92
93
94
95
96
97
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 = "";
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 = "";
111
112
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
128 }
129
130 @Override
131 void validate(final HttpURLConnection u, final String prefix)
132 throws IOException {
133 validateImpl(u, prefix, "", "");
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
149
150
151
152
153
154
155
156
157
158
159
160
161 static class JetS3tV2 extends WalkEncryption {
162
163 static final String VERSION = "2";
164
165 static final String ALGORITHM = "PBEWithMD5AndDES";
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
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
192 Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
193
194
195
196 String cryptoName = cryptoAlg.toUpperCase(Locale.ROOT);
197
198 if (!cryptoName.startsWith("PBE"))
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
205 boolean useIV = cryptoName.contains("AES");
206
207
208 if (useIV) {
209
210
211
212
213
214
215
216
217 IvParameterSpec paramIV = new IvParameterSpec(ZERO_AES_IV);
218 paramSpec = new PBEParameterSpec(SALT, ITERATIONS, paramIV);
219 } else {
220
221 paramSpec = new PBEParameterSpec(SALT, ITERATIONS);
222 }
223
224
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
265 interface Keys {
266
267 String JGIT_PROFILE = "jgit-crypto-profile";
268
269
270 String JGIT_VERSION = "jgit-crypto-version";
271
272
273 String JGIT_CONTEXT = "jgit-crypto-context";
274
275
276 String X_ALGO = ".algo";
277 String X_KEY_ALGO = ".key.algo";
278 String X_KEY_SIZE = ".key.size";
279 String X_KEY_ITER = ".key.iter";
280 String X_KEY_SALT = ".key.salt";
281 }
282
283
284 interface Vals {
285
286 String DEFAULT_VERS = "0";
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 = "";
294
295
296 String REGEX_WS = "\\s+";
297
298
299 String REGEX_PBE = "(PBE).*(WITH).+(AND).+";
300
301
302 String REGEX_TRANS = "(.+)/(.+)/(.+)";
303 }
304
305 static GeneralSecurityException securityError(String message) {
306 return new GeneralSecurityException(
307 MessageFormat.format(JGitText.get().encryptionError, message));
308 }
309
310
311
312
313
314 static abstract class SymmetricEncryption extends WalkEncryption
315 implements Keys, Vals {
316
317
318 final String profile;
319
320
321 final String version;
322
323
324 final String cipherAlgo;
325
326
327 final String paramsAlgo;
328
329
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
346 Cipher cipher = InsecureCipherFactory.create(cipherAlgo);
347
348
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
393 cipher.init(Cipher.ENCRYPT_MODE, secretKey);
394 cipher.doFinal();
395
396 }
397
398
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
424
425
426
427
428 }
429
430
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;
481 }
482 }
483 }
484
485
486
487
488
489 static class JGitV1 extends SymmetricEncryption {
490
491 static final String VERSION = "1";
492
493
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
521
522
523 static class JGitV2 extends SymmetricEncryption {
524
525 static final String VERSION = "2";
526
527 JGitV2(Properties props)
528 throws GeneralSecurityException {
529 super(props);
530 }
531 }
532
533
534
535
536
537
538
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)
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 }