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 = configuration.getChild("filename").getValue();
061    }
062    
063    /**
064     * Decrypt a string (base64) using the selected key
065     * @param encryptedValue the encrypted value
066     * @param key The key used to decrypt value
067     * @return the decrypted String
068     * @throws WrongKeyException If the key is not the right one
069     */
070    public String decrypt(String encryptedValue, String key) throws WrongKeyException
071    {
072        if (encryptedValue == null)
073        {
074            return null;
075        }
076        
077        Cipher cipher;
078        try
079        {
080            cipher = _buildCipher(key, Cipher.DECRYPT_MODE);
081        }
082        catch (Exception e)
083        {
084            throw new RuntimeException(e);
085        }
086        
087        byte[] encryptedData = Base64.getDecoder().decode(encryptedValue);
088        byte[] data;
089        
090        try
091        {
092            data = cipher.doFinal(encryptedData);
093        }
094        catch (Exception e)
095        {
096            throw new WrongKeyException(e);
097        }
098        
099        return new String(data, StandardCharsets.UTF_8);
100    }
101
102    /**
103     * Decrypt a string (base64) using the generated key
104     * @param encryptedValue the encrypted value
105     * @return the decrypted String
106     */
107    public String decrypt(String encryptedValue)
108    {
109        String key = getCryptoKey();
110        return decrypt(encryptedValue, key);
111    }
112
113    /**
114     * Encrypt (base64) a string using the selected key
115     * @param data input data
116     * @param key the key used to encrypt
117     * @return the base64 value of the encrypted data
118     */
119    public String encrypt(String data, String key)
120    {
121        if (data == null)
122        {
123            return null;
124        }
125        try
126        {
127            Cipher cipher = _buildCipher(key, Cipher.ENCRYPT_MODE);
128            byte[] dataToSend = data.getBytes(StandardCharsets.UTF_8);
129            byte[] encryptedData = cipher.doFinal(dataToSend);
130            return Base64.getEncoder().encodeToString(encryptedData);
131
132        }
133        catch (Exception e)
134        {
135            throw new RuntimeException(e);
136        }
137    }
138
139    /**
140     * Encrypt (base64) a string using the generated key
141     * @param data input data
142     * @return the base64 value of the encrypted data
143     */
144    public String encrypt(String data)
145    {
146        String key = getCryptoKey();
147        return encrypt(data, key);
148    }
149
150    /**
151     * Get the generated crypto key
152     * @return the generated crypto key
153     */
154    public String getCryptoKey()
155    {
156        if (_cryptoKey == null)
157        {
158            File cryptoFile = new File(AmetysHomeHelper.getAmetysHomeConfig(), _filename);
159            if (cryptoFile.exists())
160            {
161                if (cryptoFile.canRead())
162                {
163                    try (BufferedReader reader = new BufferedReader(new FileReader(cryptoFile)))
164                    {
165                        String line = reader.readLine();
166                        if (!StringUtils.isEmpty(line))
167                        {
168                            _cryptoKey = line.trim();
169                        }
170                        else
171                        {
172                            _cryptoKey = _writeKeyInFile(cryptoFile);
173                        }
174                    }
175                    catch (IOException e)
176                    {
177                        getLogger().error("Unable to read the crypto key from file {}", cryptoFile.getAbsolutePath(), e);
178                    }
179                }
180                else
181                {
182                    getLogger().error("Unable to read the crypto key from file {}", cryptoFile.getAbsolutePath());
183                }
184            }
185            else
186            {
187                try
188                {
189                    _cryptoKey = _writeKeyInFile(cryptoFile);
190                }
191                catch (IOException e)
192                {
193                    getLogger().error("Unable to write the crypto key in file {}", cryptoFile.getAbsolutePath(), e);
194                }
195            }
196        }
197        return _cryptoKey;
198    }
199
200    /**
201     * Will create a new random key based on a sha256 hash of the current time, and write it to a file
202     * @param file file where to write
203     * @return the generated password
204     * @throws IOException if something went wrong when writting the file
205     */
206    private String _writeKeyInFile(File file) throws IOException
207    {
208        try (FileWriter fw = new FileWriter(file))
209        {
210            String basePassword = ZonedDateTime.now().toString();
211            String key = Sha2Crypt.sha256Crypt(basePassword.getBytes());
212            fw.write(key);
213            return key;
214        }
215    }
216
217    private Cipher _buildCipher(String password, int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException
218    {
219        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
220        Key key = _buildKey(password);
221        cipher.init(mode, key);
222        return cipher;
223    }
224
225    private Key _buildKey(String password) throws NoSuchAlgorithmException
226    {
227        MessageDigest digester = MessageDigest.getInstance(PASS_HASH_ALGORITHM);
228        digester.update(String.valueOf(password).getBytes(StandardCharsets.UTF_8));
229        byte[] key = digester.digest();
230        return new SecretKeySpec(key, KEY_ALGORITHM);
231    }
232
233    /**
234     * Exception when the decrypt key has not worked
235     */
236    public static class WrongKeyException extends RuntimeException
237    {
238        WrongKeyException(Exception e)
239        {
240            super(e);
241        }
242    }
243}