001package org.intellimate.izou.security.storage;
002
003import org.apache.logging.log4j.LogManager;
004import org.apache.logging.log4j.Logger;
005import org.intellimate.izou.main.Main;
006import org.intellimate.izou.security.SecurityFunctions;
007import org.intellimate.izou.util.IzouModule;
008import ro.fortsoft.pf4j.PluginDescriptor;
009
010import javax.crypto.SecretKey;
011import java.io.*;
012import java.security.KeyStore;
013import java.security.KeyStoreException;
014import java.security.NoSuchAlgorithmException;
015import java.security.UnrecoverableEntryException;
016import java.security.cert.CertificateException;
017import java.util.HashMap;
018
019/**
020 * The SecureStorage class offers a way for addOns to store data so that other addOns cannot access it. For example if
021 * addOn A wants to store the users username and password to some service, it can do so using this class without any
022 * other addOn having access to that information.
023 * <p>
024 *     While the stored information is encrypted, it is not safe from the user. In theory and with a lot of effort, the
025 *     user could extract the keys and decrypt the stored information since the keys are not hidden from the user (only
026 *     from addOns).
027 * </p>
028 */
029public final class SecureStorage extends IzouModule {
030    private static boolean exists = false;
031    private static SecureStorage secureStorage;
032
033    private HashMap<SecretKey, SecureContainer> containers;
034    private final Logger logger = LogManager.getLogger(this.getClass());
035
036    /**
037     * Creates an SecureStorage. There can only be one single SecureStorage, so calling this method twice
038     * will cause an illegal access exception.
039     *
040     * @param main the main instance of izou
041     * @return a SecureAccess object
042     * @throws IllegalAccessException thrown if this method is called more than once
043     */
044    public static SecureStorage createSecureStorage(Main main) throws IllegalAccessException {
045        if (!exists) {
046            SecureStorage secureStorage = new SecureStorage(main);
047            exists = true;
048            SecureStorage.secureStorage = secureStorage;
049            return secureStorage;
050        }
051
052        throw new IllegalAccessException("Cannot create more than one instance of IzouSecurityManager");
053    }
054
055    /**
056     * Gets the instance of the secure storage object or null if it has not been created yet
057     *
058     * @return the instance of the secure storage object or null if it has not been created yet
059     */
060    public synchronized static SecureStorage getInstance() {
061        if (exists) {
062            return SecureStorage.secureStorage;
063        }
064
065        return null;
066    }
067
068    /**
069     * Creates a new SecureStorage instance if and only if none has been created yet
070     *
071     * @param main the main instance of izou
072     * @throws IllegalAccessException thrown if this method is called more than once
073     */
074    public SecureStorage(Main main) throws IllegalAccessException, NullPointerException {
075        super(main);
076        if (exists) {
077            throw new IllegalAccessException("Cannot create more than one instance of IzouSecurityManager");
078        }
079
080        SecretKey key = retrieveKey();
081        if (key == null) {
082            SecurityFunctions securityFunctions = new SecurityFunctions();
083            key = securityFunctions.generateKey();
084
085            if (key != null) {
086                storeKey(key);
087            } else {
088                throw new NullPointerException("Unable to create security key");
089            }
090        }
091
092        containers = retrieveContainers();
093        if (containers == null) {
094            containers = new HashMap<>();
095        }
096    }
097
098    /**
099     * Stores a {@link SecureContainer} with the given secure ID of the plugin descriptor. Each addOn can only have 1
100     * secure container, so in order to update it, retrieve it and store it again.
101     *
102     * @param descriptor The plugin descriptor belonging to an addOn
103     * @param container The secure container to be stored with an addOn
104     */
105    public void store(PluginDescriptor descriptor, SecureContainer container) {
106        HashMap<String, String> clearTextData = container.getClearTextData();
107        HashMap<byte[], byte[]> cryptData = container.getCryptData();
108
109        SecretKey secretKey = retrieveKey();
110        SecurityFunctions module = new SecurityFunctions();
111        for (byte[] key : cryptData.keySet()) {
112            clearTextData.put(module.decryptAES(key, secretKey), module.decryptAES(cryptData.get(key), secretKey));
113            cryptData.remove(key);
114        }
115
116        container.setCryptData(cryptData);
117        containers.put(descriptor.getSecureID(), container);
118        saveContainers();
119    }
120
121    /**
122     * Retrieves a {@link SecureContainer} with the given secure ID of the plugin descriptor
123     *
124     * @param descriptor The plugin descriptor belonging to an addOn
125     * @return  container The secure container that was retrieved
126     */
127    public SecureContainer retrieve(PluginDescriptor descriptor) {
128        SecureContainer container = containers.get(descriptor.getSecureID());
129        HashMap<byte[], byte[]> cryptData = container.getCryptData();
130        HashMap<String, String> clearTextData = container.getClearTextData();
131
132        SecretKey secretKey = retrieveKey();
133        SecurityFunctions module = new SecurityFunctions();
134        for (byte[] key : cryptData.keySet()) {
135            clearTextData.put(module.decryptAES(key, secretKey), module.decryptAES(cryptData.get(key), secretKey));
136            cryptData.remove(key);
137        }
138
139        container.setClearTextData(clearTextData);
140        return container;
141    }
142
143    /**
144     * Saves the containers to ./system/data/containers.ser
145     */
146    private void saveContainers() {
147        String workingDir = getMain().getFileSystemManager().getSystemDataLocation().getAbsolutePath();
148        final String containerFile = workingDir + File.separator + "containers.ser";
149        try {
150            FileOutputStream fileOut = new FileOutputStream(containerFile);
151            ObjectOutputStream out = new ObjectOutputStream(fileOut);
152            out.writeObject(containers);
153            out.close();
154            fileOut.close();
155        } catch(IOException e) {
156            logger.error("Unable to save containers to file", e);
157        }
158    }
159
160    /**
161     * Retrieves the containers from ./system/data/containers.ser if the file is found, else returns null
162     *
163     * @return the containers from ./system/data/containers.ser if the file is found, else null
164     */
165    private HashMap<SecretKey, SecureContainer> retrieveContainers() {
166        HashMap<SecretKey, SecureContainer> containers = null;
167        String workingDir = getMain().getFileSystemManager().getSystemDataLocation().getAbsolutePath();
168        final String containerFile = workingDir + File.separator
169                + "containers.ser";
170        try {
171            FileInputStream fileIn = new FileInputStream(containerFile);
172            ObjectInputStream in = new ObjectInputStream(fileIn);
173            Object o = in.readObject();
174            if (o instanceof HashMap) {
175                containers = (HashMap) o;
176            }
177            in.close();
178            fileIn.close();
179        } catch (FileNotFoundException e) {
180            return null;
181        } catch(IOException | ClassNotFoundException e) {
182            logger.error("Unable to retrieve containers from file", e);
183        }
184
185        return containers;
186    }
187
188    /**
189     * Retrieves the izou aes key stored in a keystore
190     *
191     * @return the izou aes key stored in a keystore
192     */
193    private SecretKey retrieveKey() {
194        SecretKey key = null;
195        try {
196            String workingDir = getMain().getFileSystemManager().getSystemLocation().getAbsolutePath();
197            final String keyStoreFile = workingDir + File.separator + "izou.keystore";
198            KeyStore keyStore = createKeyStore(keyStoreFile, "4b[X:+H4CS&avY<)");
199
200            KeyStore.PasswordProtection keyPassword = new KeyStore.PasswordProtection("Ev45j>eP}QTR?K9_".toCharArray());
201            KeyStore.Entry entry = keyStore.getEntry("izou_key", keyPassword);
202            key = ((KeyStore.SecretKeyEntry) entry).getSecretKey();
203        } catch(NullPointerException e) {
204            return null;
205        } catch (UnrecoverableEntryException | NoSuchAlgorithmException | KeyStoreException e) {
206            logger.error("Unable to retrieve key", e);
207        }
208
209        return key;
210    }
211
212    /**
213     * Stores the izou aes key in a keystore
214     *
215     * @param key the key to store
216     */
217    private void storeKey(SecretKey key) {
218        final String keyStoreFile = getMain().getFileSystemManager().getSystemLocation() + File.separator + "izou.keystore";
219        KeyStore keyStore = createKeyStore(keyStoreFile, "4b[X:+H4CS&avY<)");
220
221        try {
222            KeyStore.SecretKeyEntry keyStoreEntry = new KeyStore.SecretKeyEntry(key);
223            KeyStore.PasswordProtection keyPassword = new KeyStore.PasswordProtection("Ev45j>eP}QTR?K9_".toCharArray());
224            keyStore.setEntry("izou_key", keyStoreEntry, keyPassword);
225            keyStore.store(new FileOutputStream(keyStoreFile), "4b[X:+H4CS&avY<)".toCharArray());
226        } catch (NoSuchAlgorithmException | KeyStoreException
227                | CertificateException | IOException e) {
228            logger.error("Unable to store key", e);
229        }
230    }
231
232    /**
233     * Creates a new keystore for the izou aes key
234     *
235     * @param fileName the path to the keystore
236     * @param password the password to use with the keystore
237     * @return the newly created keystore
238     */
239    private KeyStore createKeyStore(String fileName, String password)  {
240        File file = new File(fileName);
241        KeyStore keyStore = null;
242        try {
243            keyStore = KeyStore.getInstance("JCEKS");
244            if (file.exists()) {
245                keyStore.load(new FileInputStream(file), password.toCharArray());
246            } else {
247                keyStore.load(null, null);
248                keyStore.store(new FileOutputStream(fileName), password.toCharArray());
249            }
250        } catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException e) {
251            logger.error("Unable to create key store", e);
252        }
253
254        return keyStore;
255    }
256}