/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.deltaspike.core.impl.crypto; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Properties; /** * handle Encryption */ public class DefaultCipherService { private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final String HASH_ALGORITHM = "SHA-256"; private static final String CIPHER_ALGORITHM = "AES"; public String setMasterHash(String masterPassword, String masterSalt, boolean overwrite) throws IOException { File masterFile = getMasterFile(); if (!masterFile.getParentFile().exists()) { if (!masterFile.getParentFile().mkdirs()) { throw new IOException("Can not create directory " + masterFile.getParent()); } } String saltHash = byteToHex(secureHash(masterSalt)); String saltKey = byteToHex(secureHash(saltHash)); String encrypted = byteToHex(aesEncrypt(byteToHex(secureHash(masterPassword)), saltHash)); Properties keys = new Properties(); if (masterFile.exists()) { keys = loadProperties(masterFile.toURI().toURL()); } if (keys.get(saltKey) != null && !overwrite) { throw new IllegalStateException("MasterKey for hash " + saltHash + " already exists. Forced overwrite option needed"); } keys.put(saltKey, encrypted); keys.store(new FileOutputStream(masterFile), null); return saltHash; } protected String getMasterKey(String masterSalt) { File masterFile = getMasterFile(); if (!masterFile.exists()) { throw new IllegalStateException("Could not find master.hash file. Create a master password first!"); } try { String saltHash = byteToHex(secureHash(masterSalt)); String saltKey = byteToHex(secureHash(saltHash)); Properties keys = loadProperties(masterFile.toURI().toURL()); String encryptedMasterKey = (String) keys.get(saltKey); if (encryptedMasterKey == null) { throw new IllegalStateException("Could not find master key for hash " + saltKey + ". Create a master password first!"); } return aesDecrypt(hexToByte(encryptedMasterKey), saltHash); } catch (MalformedURLException e) { throw new RuntimeException(e); } } public String encrypt(String cleartext, String masterSalt) { return byteToHex(aesEncrypt(cleartext, getMasterKey(masterSalt))); } public String decrypt(String encryptedValue, String masterSalt) { return aesDecrypt(hexToByte(encryptedValue), getMasterKey(masterSalt)); } protected File getMasterFile() { String userHome = System.getProperty("user.home"); if (userHome == null || userHome.isEmpty()) { throw new IllegalStateException("Can not determine user home directory"); } return new File(userHome, ".deltaspike/master.hash"); } protected byte[] secureHash(String value) { try { MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM); return md.digest(value.getBytes(UTF_8)); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } /** * performs an AES encryption of the given text with the given password key */ public byte[] aesEncrypt(String valueToEncrypt, String key) { try { SecretKeySpec secretKeySpec = getSecretKeySpec(key); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); return cipher.doFinal(valueToEncrypt.getBytes(UTF_8)); } catch (Exception e) { throw new RuntimeException(e); } } /** * performs an AES decryption of the given text with the given key key */ public String aesDecrypt(byte[] encryptedValue, String key) { try { SecretKeySpec secretKeySpec = getSecretKeySpec(key); Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); return new String(cipher.doFinal(encryptedValue), UTF_8); } catch (Exception e) { throw new RuntimeException(e); } } private SecretKeySpec getSecretKeySpec(String password) { byte[] pwdHash = secureHash(password); byte[] key = Arrays.copyOf(pwdHash, 16); // use only first 128 bit // Note: using 128 bit AES avoids requirement for "Unlimited Crypto" patch return new SecretKeySpec(key, "AES"); } protected String byteToHex(final byte[] hash) { StringBuilder sb = new StringBuilder(hash.length * 2); for (byte b : hash) { sb.append(Character.forDigit(b >> 4 & 0x0f, 16)); sb.append(Character.forDigit(b & 0x0f, 16)); } return sb.toString(); } protected byte[] hexToByte(String hexString) { if (hexString == null || hexString.length() == 0) { return new byte[0]; } hexString = hexString.trim(); if (hexString.length() % 2 != 0) { throw new IllegalArgumentException("not a valid hex string " + hexString); } byte[] bytes = new byte[hexString.length() / 2]; for (int i = 0; i < hexString.length() / 2; i++) { int val = (Character.digit(hexString.charAt(i * 2), 16) << 4) + (Character.digit(hexString.charAt( (i * 2) + 1), 16)); bytes[i] = (byte) val; } return bytes; } /** * Copied over from PropertyFileUtils to avoid the need for having the api * on the classpath when using the password encode CLI */ private Properties loadProperties(URL url) { Properties props = new Properties(); InputStream inputStream = null; try { inputStream = url.openStream(); if (inputStream != null) { props.load(inputStream); } } catch (IOException e) { throw new IllegalStateException(e); } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { // no worries, means that the file is already closed } } return props; } }