1
2
3
4
5
6
7
8
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";
45
46 static final String JETS3T_CRYPTO_ALG = "jets3t-crypto-alg";
47
48
49 abstract OutputStream encrypt(OutputStream output) throws IOException;
50
51
52 abstract void request(HttpURLConnection conn, String prefix) throws IOException;
53
54
55 abstract void validate(HttpURLConnection conn, String prefix) throws IOException;
56
57
58 abstract InputStream decrypt(InputStream input) throws IOException;
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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 = "";
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 = "";
92
93
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
108 }
109
110 @Override
111 void validate(HttpURLConnection u, String prefix)
112 throws IOException {
113 validateImpl(u, prefix, "", "");
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
129
130
131
132
133
134
135
136
137
138
139
140
141 static class JetS3tV2 extends WalkEncryption {
142
143 static final String VERSION = "2";
144
145 static final String ALGORITHM = "PBEWithMD5AndDES";
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
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
172 Cipher cipher = InsecureCipherFactory.create(cryptoAlg);
173
174
175
176 String cryptoName = cryptoAlg.toUpperCase(Locale.ROOT);
177
178 if (!cryptoName.startsWith("PBE"))
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
185 boolean useIV = cryptoName.contains("AES");
186
187
188 if (useIV) {
189
190
191
192
193
194
195
196
197 IvParameterSpec paramIV = new IvParameterSpec(ZERO_AES_IV);
198 paramSpec = new PBEParameterSpec(SALT, ITERATIONS, paramIV);
199 } else {
200
201 paramSpec = new PBEParameterSpec(SALT, ITERATIONS);
202 }
203
204
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
245 interface Keys {
246
247 String JGIT_PROFILE = "jgit-crypto-profile";
248
249
250 String JGIT_VERSION = "jgit-crypto-version";
251
252
253 String JGIT_CONTEXT = "jgit-crypto-context";
254
255
256 String X_ALGO = ".algo";
257 String X_KEY_ALGO = ".key.algo";
258 String X_KEY_SIZE = ".key.size";
259 String X_KEY_ITER = ".key.iter";
260 String X_KEY_SALT = ".key.salt";
261 }
262
263
264 interface Vals {
265
266 String DEFAULT_VERS = "0";
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 = "";
274
275
276 String REGEX_WS = "\\s+";
277
278
279 String REGEX_PBE = "(PBE).*(WITH).+(AND).+";
280
281
282 String REGEX_TRANS = "(.+)/(.+)/(.+)";
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
295
296
297 abstract static class SymmetricEncryption extends WalkEncryption
298 implements Keys, Vals {
299
300
301 final String profile;
302
303
304 final String version;
305
306
307 final String cipherAlgo;
308
309
310 final String paramsAlgo;
311
312
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
329 Cipher cipher = InsecureCipherFactory.create(cipherAlgo);
330
331
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
375 cipher.init(Cipher.ENCRYPT_MODE, secretKey);
376 cipher.doFinal();
377
378 }
379
380
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
406
407
408
409
410 }
411
412
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;
463 }
464 }
465 }
466
467
468
469
470
471 static class JGitV1 extends SymmetricEncryption {
472
473 static final String VERSION = "1";
474
475
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
503
504
505 static class JGitV2 extends SymmetricEncryption {
506
507 static final String VERSION = "2";
508
509 JGitV2(Properties props)
510 throws GeneralSecurityException {
511 super(props);
512 }
513 }
514
515
516
517
518
519
520
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)
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 }