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}