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}