001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.secrets;
019
020
021
022import com.fasterxml.jackson.core.type.TypeReference;
023import io.stallion.exceptions.UsageException;
024import io.stallion.settings.childSections.SecretsSettings;
025import io.stallion.utils.Encrypter;
026import io.stallion.utils.json.JSON;
027import org.apache.commons.io.FileUtils;
028import org.apache.commons.lang3.text.WordUtils;
029import org.springframework.security.crypto.keygen.KeyGenerators;
030
031import java.io.File;
032import java.io.IOException;
033import java.util.*;
034
035import static io.stallion.utils.Literals.*;
036import static io.stallion.Context.*;
037
038
039public class SecretsVault {
040    private String passPhrase;
041    private String secretsPath = "";
042    private static SecretsSettings secretsSettings;
043
044    private HashMap<String, String> secrets = new HashMap<>();
045
046    private static String appPath = "";
047    private static Map<String, String> appSecrets;
048
049    public static Map<String, String> getAppSecrets() {
050        if (empty(appPath)) {
051            throw new UsageException("You cannot call getAppSecrets() before init() is called");
052        }
053        if (appSecrets == null) {
054            appSecrets = loadIfExists(appPath);
055            if (appSecrets == null) {
056                appSecrets = new HashMap<>();
057            }
058        }
059        return appSecrets;
060    }
061
062    public static void init(String theAppPath, SecretsSettings theSecretsSettings) {
063        // We have to pass in the application path, since we cannot rely on Settings.instance() being available.
064        // However, we do not want to actually load the secrets vault, since that adds complexity and overhead
065        // in circumstances when it may not even being necessary (such as when running locally when there are no
066        // production keys). So we lazy-load the actual vault when it is first requested.
067        appPath = theAppPath;
068        secretsSettings = theSecretsSettings;
069    }
070
071
072    public static Map<String, String> loadIfExists(String appPath) {
073        String rawPath = appPath + "/conf/secrets.json";
074        String encryptedPath = appPath + "/conf/secrets.json.aes";
075        File rawFile = new File(rawPath);
076        if (rawFile.isFile()) {
077            TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() {};
078            String json = null;
079            try {
080                json = FileUtils.readFileToString(rawFile, UTF8);
081            } catch (IOException e) {
082                throw new RuntimeException(e);
083            }
084            return JSON.parse(json, typeRef);
085        }
086        if (new File(encryptedPath).isFile()) {
087            // Get passphrase
088            SecretsVault vault = new SecretsCommandLineManager().loadVault(appPath, secretsSettings);
089            if (vault != null) {
090                return vault.getSecrets();
091            }
092
093        }
094        return null;
095    }
096
097    public SecretsVault(String appPath, String passPhrase) {
098        this.passPhrase = passPhrase;
099        if (passPhrase.length() < 16) {
100            throw new UsageException("Your passPhrase is not long enough!");
101        }
102        secretsPath = appPath + "/conf/secrets.json.aes";
103        File file = new File(secretsPath);
104        if (!file.isFile()) {
105            secrets = new HashMap<>();
106            save();
107        } else {
108            try {
109                String encrypted = FileUtils.readFileToString(file, UTF8);
110                encrypted = encrypted.replace("\n", "");
111                secrets = decryptAndParse(encrypted);
112            } catch (IOException e) {
113                throw new RuntimeException(e);
114            }
115        }
116
117    }
118
119    public String secretsToJson() {
120        return JSON.stringify(getSecrets());
121    }
122
123    public HashMap<String, String> getSecrets() {
124        return secrets;
125    }
126
127    public List<String> getSecretNames() {
128        List<String> secretNames = new ArrayList<String>(secrets.keySet());
129        secretNames.sort(new Comparator<String>() {
130            @Override
131            public int compare(String o1, String o2) {
132                return o1.compareTo(o2);
133            }
134        });
135        return secretNames;
136    }
137
138    public String getSecret(String name) {
139        return secrets.get(name);
140    }
141
142    public SecretsVault add(String name, String value) {
143        if (empty(name)) {
144            throw new UsageException("Name is empty.");
145        }
146        if (secrets.containsKey(name)) {
147            throw new UsageException("Secrets vault already contains secret with name " + name);
148        }
149        secrets.put(name, value);
150        return this;
151    }
152
153    public SecretsVault update(String name, String value) {
154        if (!secrets.containsKey(name)) {
155            throw new UsageException("No secret with name '" + name + "' exists");
156        }
157        secrets.put(name, value);
158        return this;
159    }
160
161    public HashMap<String,String> decryptAndParse(String encrypted) {
162        TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() {};
163        String json = Encrypter.decryptString(passPhrase, encrypted);
164        secrets = JSON.parse(json, typeRef);
165        return secrets;
166    }
167
168    public String dumpAndEncrypt() {
169        return WordUtils.wrap(Encrypter.encryptString(passPhrase, JSON.stringify(secrets)), 80, "\n", true);
170    }
171
172    public void save() {
173        Long version = Long.parseLong(secrets.getOrDefault("version", "1")) + 1L;
174        secrets.put("version", version.toString());
175        String encrypted = dumpAndEncrypt();
176        File file = new File(secretsPath);
177        try {
178            FileUtils.write(file, encrypted, UTF8);
179        } catch (IOException e) {
180            throw new RuntimeException(e);
181        }
182    }
183}