You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

WalkEncryption.java 17KB

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