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 020import io.stallion.boot.CommandOptionsBase; 021import io.stallion.services.Log; 022import io.stallion.settings.childSections.SecretsSettings; 023import io.stallion.utils.Prompter; 024import net.east301.keyring.BackendNotSupportedException; 025import net.east301.keyring.Keyring; 026import net.east301.keyring.PasswordRetrievalException; 027import net.east301.keyring.PasswordSaveException; 028import net.east301.keyring.util.LockException; 029import org.apache.commons.io.FileUtils; 030 031import java.io.File; 032import java.io.IOException; 033import java.text.MessageFormat; 034 035import static io.stallion.utils.Literals.*; 036 037 038public class SecretsCommandLineManager { 039 040 private String password = ""; 041 private SecretsVault vault; 042 private String targetFolder = ""; 043 private boolean keyringAccesible = false; 044 private boolean inKeyChain = false; 045 private String keyringServiceName = ""; 046 private static final String keyringAccountName = "stallion"; 047 048 private static final String lock = " .-\"\"-.\n" + 049 " / .--. \\\n" + 050 " / / \\ \\\n" + 051 " | | | |\n" + 052 " | |.-\"\"-.|\n" + 053 " ///`.::::.`\\\n" + 054 " ||| ::/ \\:: ;\n" + 055 " ||; ::\\__/:: ;\n" + 056 " \\\\\\ '::::' /\n" + 057 " `=':-..-'`"; 058 public SecretsVault loadVault(String appPath, SecretsSettings secretsSettings) { 059 return loadVault(appPath, secretsSettings, ""); 060 } 061 062 public SecretsVault loadVault(String appPath, SecretsSettings secretsSettings, String password) { 063 064 targetFolder = appPath; 065 keyringServiceName = "Stallion Secrets Key: " + targetFolder; 066 067 if (secretsSettings == null) { 068 Log.info("secretsSettings is null"); 069 } else { 070 Log.info("Secrets passphrase file is {0}", secretsSettings.getPassPhraseFile()); 071 } 072 073 if (empty(password)) { 074 password = findPasswordFromEnv(); 075 } 076 if (empty(password)) { 077 password = findPasswordFromFile(secretsSettings); 078 } 079 080 081 if (empty(password)) { 082 password = findPasswordInKeyring(); 083 } 084 085 if (empty(password)) { 086 password = new Prompter("What is your encryption password? If you are creating a secrets file for the first time," + 087 "generate a new random string of at least 16 alpha-numeric characters, save it somewhere safe " + 088 "(like your password manager), and then paste it in here. This password is not recoverable. If you lost it, " + 089 "your secrets file will be locked forever.\nEnter your encryption password: ") { 090 @Override 091 public boolean validate(String line) { 092 if (line.length() < 15) { 093 this.lastErrorMessage = "Your password must be at least 20 characters."; 094 return false; 095 } 096 return true; 097 } 098 }.prompt(); 099 } 100 101 102 vault = new SecretsVault(targetFolder, password); 103 104 promptStorePassword(password); 105 return vault; 106 } 107 108 public void start(CommandOptionsBase options) { 109 System.out.print("\n\n" + lock + "\n\n"); 110 System.out.println("Welcome to the Stallion Secrets Manager.\n\n You can view your secrets, create new secrets, change secrets, delete secrets"); 111 112 targetFolder = options.getTargetPath(); 113 loadVault(options.getTargetPath(), null); 114 115 while(true) { 116 String line = new Prompter("What do you want to do? Options: new/edit/delete/list/quit ") 117 .setChoices("new", "edit", "delete", "list", "quit") 118 .prompt(); 119 switch (line) { 120 case "quit": 121 return; 122 case "new": 123 newSecret(); 124 break; 125 case "delete": 126 deleteSecrets(); 127 break; 128 case "list": 129 listSecrets(); 130 break; 131 case "edit": 132 editSecret(); 133 break; 134 default: 135 continue; 136 } 137 } 138 139 } 140 141 public String findPasswordFromEnv() { 142 return System.getenv("STALLION_SECRETS_PASSPHRASE"); 143 } 144 145 public String findPasswordFromFile(SecretsSettings secretsSettings) { 146 if (secretsSettings == null) { 147 return ""; 148 } 149 String path = or(secretsSettings.getPassPhraseFile(), "/usr/local/etc/stallion-secrets-passphrase"); 150 Log.info("Looking for secrets pass phrase in file {0}", path); 151 File passPhraseFile = new File(path); 152 153 if (!passPhraseFile.exists()) { 154 return ""; 155 } 156 try { 157 return FileUtils.readFileToString(passPhraseFile, UTF8).trim(); 158 } catch (IOException e) { 159 throw new RuntimeException(e); 160 } 161 } 162 163 164 public String findPasswordInKeyring() { 165 166 Keyring keyring; 167 try { 168 keyring = Keyring.create(); 169 } catch (BackendNotSupportedException ex) { 170 Log.info("Backend not supported for storing passphrase in Keyring"); 171 return ""; 172 } 173 174 keyringAccesible = true; 175 176 177 // some backend directory handles a file to store password to disks. 178 // in this case, we must set path to password store file by Keyring.setKeyStorePath 179 // before using Keyring.getPassword and Keyring.getPassword. 180 if (keyring.isKeyStorePathRequired()) { 181 try { 182 File keyStoreFile = File.createTempFile("keystore", ".keystore"); 183 keyring.setKeyStorePath(keyStoreFile.getPath()); 184 } catch (IOException ex) { 185 throw new RuntimeException(ex); 186 } 187 } 188 189 // 190 // Retrieve password from password store 191 // 192 193 // Password can be retrieved by using Keyring.getPassword method. 194 // PasswordRetrievalException is thrown when some error happened while getting password. 195 // LockException is thrown when keyring backend failed to lock password store file. 196 try { 197 String password = keyring.getPassword(keyringServiceName, keyringAccountName); 198 if (!empty(password)) { 199 inKeyChain = true; 200 } 201 return password; 202 } catch (LockException ex) { 203 throw new RuntimeException(ex); 204 } catch (PasswordRetrievalException ex) { 205 Log.info("Could not retrieve passphrase from the keychain"); 206 return ""; 207 } 208 } 209 210 public void promptStorePassword(String password) { 211 if (!keyringAccesible) { 212 return; 213 } 214 if (inKeyChain) { 215 return; 216 } 217 boolean shouldStore = new Prompter("Store this passphrase in your local keychain? (Y/n) ").yesNo(); 218 if (!shouldStore) { 219 Log.info("Not should store!"); 220 return; 221 } 222 223 224 Keyring keyring; 225 try { 226 keyring = Keyring.create(); 227 } catch (BackendNotSupportedException ex) { 228 Log.info("Backend not supported for storing passphrase in Keyring"); 229 return; 230 } 231 232 // some backend directory handles a file to store password to disks. 233 // in this case, we must set path to password store file by Keyring.setKeyStorePath 234 // before using Keyring.getPassword and Keyring.getPassword. 235 if (keyring.isKeyStorePathRequired()) { 236 try { 237 File keyStoreFile = File.createTempFile("keystore", ".keystore"); 238 keyring.setKeyStorePath(keyStoreFile.getPath()); 239 } catch (IOException ex) { 240 throw new RuntimeException(ex); 241 } 242 } 243 244 // Password can be stored to password store by using Keyring.setPassword method. 245 // PasswordSaveException is thrown when some error happened while saving password. 246 // LockException is thrown when keyring backend failed to lock password store file. 247 try { 248 keyring.setPassword(keyringServiceName, keyringAccountName, password); 249 } catch (LockException ex) { 250 throw new RuntimeException(ex); 251 } catch (PasswordSaveException ex) { 252 throw new RuntimeException(ex); 253 } 254 255 256 } 257 258 public void newSecret() { 259 String name = new Prompter("Secret name (You will use this name in your settings file to retrieve the secret): ") 260 .prompt(); 261 String value = new Prompter("Secret value: ") 262 .prompt(); 263 vault.add(name, value); 264 vault.save(); 265 System.out.println("Secret added."); 266 267 } 268 269 public void editSecret() { 270 String name = new Prompter("Secret name: ") 271 .setChoices(vault.getSecretNames()) 272 .prompt(); 273 String value = new Prompter("Secret value: ") 274 .prompt(); 275 vault.update(name, value); 276 vault.save(); 277 System.out.println("Secret updated."); 278 279 } 280 public void listSecrets() { 281 MessageFormat format = new MessageFormat("{0}: {1}"); 282 System.out.print("\n"); 283 if (vault.getSecretNames().size() == 0) { 284 System.out.println("No secrets defined."); 285 } 286 for(String name: vault.getSecretNames()) { 287 System.out.println(format.format("{0}: {1}", name, vault.getSecret(name))); 288 } 289 System.out.print("\n"); 290 } 291 public void deleteSecrets() { 292 String name = new Prompter("Secret name: ") 293 .setChoices(vault.getSecretNames()) 294 .prompt(); 295 vault.getSecrets().remove(name); 296 vault.save(); 297 System.out.println("Secret deleted."); 298 } 299 300 301}