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