001/* 002 * Copyright 2022 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.util; 017 018import java.io.BufferedReader; 019import java.io.File; 020import java.io.FileReader; 021import java.io.FileWriter; 022import java.io.IOException; 023import java.nio.charset.StandardCharsets; 024import java.security.InvalidKeyException; 025import java.security.Key; 026import java.security.MessageDigest; 027import java.security.NoSuchAlgorithmException; 028import java.time.ZonedDateTime; 029import java.util.Base64; 030 031import javax.crypto.Cipher; 032import javax.crypto.NoSuchPaddingException; 033import javax.crypto.spec.SecretKeySpec; 034 035import org.apache.avalon.framework.component.Component; 036import org.apache.avalon.framework.configuration.Configurable; 037import org.apache.avalon.framework.configuration.Configuration; 038import org.apache.avalon.framework.configuration.ConfigurationException; 039import org.apache.commons.codec.digest.Sha2Crypt; 040import org.apache.commons.lang3.StringUtils; 041 042import org.ametys.runtime.plugin.component.AbstractLogEnabled; 043import org.ametys.runtime.util.AmetysHomeHelper; 044 045/** 046 * Helper to encrypt/decrypt some text 047 */ 048public class CryptoHelper extends AbstractLogEnabled implements Component, Configurable 049{ 050 private static final String CIPHER_ALGORITHM = "AES"; 051 private static final String KEY_ALGORITHM = "AES"; 052 private static final String PASS_HASH_ALGORITHM = "SHA-256"; 053 054 private String _cryptoKey; 055 056 private String _filename; 057 058 public void configure(Configuration configuration) throws ConfigurationException 059 { 060 _filename = _configureFilename(configuration); 061 } 062 063 /** 064 * Configure the file name containing the key 065 * @param configuration the configuration 066 * @return the file name 067 * @throws ConfigurationException if an error occurs while configuring the file name 068 */ 069 protected String _configureFilename(Configuration configuration) throws ConfigurationException 070 { 071 return configuration.getChild("filename").getValue(); 072 } 073 074 /** 075 * Retrieves the file name 076 * @return the file name 077 */ 078 protected String _getFilename() 079 { 080 return _filename; 081 } 082 083 /** 084 * Decrypt a string (base64) using the selected key 085 * @param encryptedValue the encrypted value 086 * @param key The key used to decrypt value 087 * @return the decrypted String 088 * @throws WrongKeyException If the key is not the right one 089 */ 090 public String decrypt(String encryptedValue, String key) throws WrongKeyException 091 { 092 if (encryptedValue == null) 093 { 094 return null; 095 } 096 097 Cipher cipher; 098 try 099 { 100 cipher = _buildCipher(key, Cipher.DECRYPT_MODE); 101 } 102 catch (Exception e) 103 { 104 throw new RuntimeException(e); 105 } 106 107 byte[] encryptedData = Base64.getDecoder().decode(encryptedValue); 108 byte[] data; 109 110 try 111 { 112 data = cipher.doFinal(encryptedData); 113 } 114 catch (Exception e) 115 { 116 throw new WrongKeyException(e); 117 } 118 119 return new String(data, StandardCharsets.UTF_8); 120 } 121 122 /** 123 * Decrypt a string (base64) using the generated key 124 * @param encryptedValue the encrypted value 125 * @return the decrypted String 126 */ 127 public String decrypt(String encryptedValue) 128 { 129 String key = getCryptoKey(); 130 return decrypt(encryptedValue, key); 131 } 132 133 /** 134 * Encrypt (base64) a string using the selected key 135 * @param data input data 136 * @param key the key used to encrypt 137 * @return the base64 value of the encrypted data 138 */ 139 public String encrypt(String data, String key) 140 { 141 if (data == null) 142 { 143 return null; 144 } 145 try 146 { 147 Cipher cipher = _buildCipher(key, Cipher.ENCRYPT_MODE); 148 byte[] dataToSend = data.getBytes(StandardCharsets.UTF_8); 149 byte[] encryptedData = cipher.doFinal(dataToSend); 150 return Base64.getEncoder().encodeToString(encryptedData); 151 152 } 153 catch (Exception e) 154 { 155 throw new RuntimeException(e); 156 } 157 } 158 159 /** 160 * Encrypt (base64) a string using the generated key 161 * @param data input data 162 * @return the base64 value of the encrypted data 163 */ 164 public String encrypt(String data) 165 { 166 String key = getCryptoKey(); 167 return encrypt(data, key); 168 } 169 170 /** 171 * Get the generated crypto key 172 * @return the generated crypto key 173 */ 174 public String getCryptoKey() 175 { 176 if (_cryptoKey == null) 177 { 178 File cryptoFile = new File(AmetysHomeHelper.getAmetysHomeConfig(), _filename); 179 if (cryptoFile.exists()) 180 { 181 if (cryptoFile.canRead()) 182 { 183 try (BufferedReader reader = new BufferedReader(new FileReader(cryptoFile))) 184 { 185 String line = reader.readLine(); 186 if (!StringUtils.isEmpty(line)) 187 { 188 _cryptoKey = line.trim(); 189 } 190 else 191 { 192 _cryptoKey = _writeKeyInFile(cryptoFile); 193 } 194 } 195 catch (IOException e) 196 { 197 getLogger().error("Unable to read the crypto key from file {}", cryptoFile.getAbsolutePath(), e); 198 } 199 } 200 else 201 { 202 getLogger().error("Unable to read the crypto key from file {}", cryptoFile.getAbsolutePath()); 203 } 204 } 205 else 206 { 207 try 208 { 209 _cryptoKey = _writeKeyInFile(cryptoFile); 210 } 211 catch (IOException e) 212 { 213 getLogger().error("Unable to write the crypto key in file {}", cryptoFile.getAbsolutePath(), e); 214 } 215 } 216 } 217 return _cryptoKey; 218 } 219 220 /** 221 * Will create a new random key based on a sha256 hash of the current time, and write it to a file 222 * @param file file where to write 223 * @return the generated password 224 * @throws IOException if something went wrong when writting the file 225 */ 226 protected String _writeKeyInFile(File file) throws IOException 227 { 228 try (FileWriter fw = new FileWriter(file)) 229 { 230 String basePassword = ZonedDateTime.now().toString(); 231 String key = Sha2Crypt.sha256Crypt(basePassword.getBytes()); 232 fw.write(key); 233 return key; 234 } 235 } 236 237 private Cipher _buildCipher(String password, int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException 238 { 239 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); 240 Key key = _buildKey(password); 241 cipher.init(mode, key); 242 return cipher; 243 } 244 245 private Key _buildKey(String password) throws NoSuchAlgorithmException 246 { 247 MessageDigest digester = MessageDigest.getInstance(PASS_HASH_ALGORITHM); 248 digester.update(String.valueOf(password).getBytes(StandardCharsets.UTF_8)); 249 byte[] key = digester.digest(); 250 return new SecretKeySpec(key, KEY_ALGORITHM); 251 } 252 253 /** 254 * Exception when the decrypt key has not worked 255 */ 256 public static class WrongKeyException extends RuntimeException 257 { 258 WrongKeyException(Exception e) 259 { 260 super(e); 261 } 262 } 263}