static void Main(string[] args) { var argv = args; int argc = args.Length; string path = null; string password = null; string keyfile = null; bool delete = false; string key = null; string value = null; bool decryptAll = false; bool generate = false; DecryptFormat format = DecryptFormat.None; OptionSet globalOptions; var createOptions = new OptionSet { { "s|store=", "Path to the secrets store to be created", s => path = s }, { "p|password:"******"Secure the store with a key derived from a password (optionally provided in the command line)", p => password = p ?? "" }, { "f|keyfile=", "Secure the store with a randomly generated key, created at the provided path", f => keyfile = f }, { "g|generate", "Generate a new key and save it to the path specified by --keyfile", g => generate = g != null }, }; var updateOptions = new OptionSet { { "s|store=", "Path to the secrets store to be loaded", s => path = s }, { "p|password:"******"Decrypt the store with a key derived from a password (optionally provided in the command line)", p => password = p ?? "" }, { "f|keyfile=", "Decrypt the store with a keyfile, located at the provided path", f => keyfile = f }, { "k|key=", "The (new or existing) key of the (key, value) tuple to create or update", k => key = k }, { "v|value=", "The (new) value of the (key, value) tuple to create or update", v => value = v }, { "d|delete=", "The existing key of the (key, value) tuple to delete", k => { key = k; delete = true; } }, }; var decryptOptions = new OptionSet { { "s|store=", "Path to the secrets store to be loaded", s => path = s }, { "p|password:"******"Decrypt the store with a key derived from a password (optionally provided in the command line)", p => password = p ?? "" }, { "f|keyfile=", "Decrypt the store with a keyfile, located at the provided path", f => keyfile = f }, { "k|key=", "The (new or existing) key of the (key, value) tuple to create or update", k => key = k }, { "a|all", "Decrypt the entire contents of the store and print to stdout", a => decryptAll = a != null }, { "t|format=", "Specify the output format: json (default), text", t => { if (!Parse(t, out format)) { throw new ExitCodeException(1, "Unsupported format specified!"); } } } }; void printUsage() { Console.WriteLine($"ssclient [OPTIONS | [create|update|decrypt] OPTIONS]"); PrintOptions(null, globalOptions); Console.WriteLine(); PrintOptions("create", createOptions); PrintOptions("update", updateOptions); PrintOptions("decrypt", decryptOptions); } bool help = false; bool version = false; globalOptions = new OptionSet { { "h|help|?", "Show this help message and exit", h => help = true }, { "v|version", "Print version information and exit", h => version = true }, }; if (args.Length == 0) { Console.Write("Usage: "); printUsage(); Environment.Exit(1); } //only try to parse --help and --version globalOptions.Parse(new[] { args[0] }); void printVersion() { Console.WriteLine($"ssclient {AssemblyVersion} - SecureStore secrets manager client"); Console.WriteLine("Copyright NeoSmart Technologies 2017 - https://github.com/neosmart/SecureStore/"); } if (help) { printVersion(); Console.WriteLine(); Console.Write("Usage: "); printUsage(); Environment.Exit(0); } if (version) { printVersion(); Environment.Exit(0); } string command = args[0]; OptionSet options = null; switch (command) { case "create": options = createOptions; break; case "update": options = updateOptions; break; case "decrypt": options = decryptOptions; break; default: Console.WriteLine($"{command}: unsupported command!"); Console.WriteLine(); printUsage(); Environment.Exit(1); break; } int exitCode = 0; try { //we have no trailing parameters, but Mono.Options is dumb and does not treat --password PASSWORD //as an option:value tuple when password is defined as taking an optional value. //It instead requires --password=PASSWORD or --password:PASSWORD or -pPASSWORD var bareArguments = options.Parse(args.Skip(1)); bool standalonePassword = false; if (bareArguments.Count == 1 && password == "") { //check if this was the standalone password var possibles = new[] { "-p", "--password", "/p", "/password" }; for (int i = 1; i < argc; ++i) { if (!possibles.Contains(argv[i])) { continue; } //this was the password index if (argc > i + 1 && argv[i + 1] == bareArguments[0]) { password = bareArguments[0]; standalonePassword = true; } break; } } //Handle common parameters if (!standalonePassword && bareArguments.Count > 0) { Help("Invalid arguments!", command, options); } if (string.IsNullOrWhiteSpace(path)) { Help("A path to the secrets store is required!", command, options); } if (keyfile != null && password != null) { Help("Cannot specify both --password and --keyfile", command, options); } if (keyfile == null && password == null) { Help("Must specify either --password or --keyfile!", command, options); } if (password == "") { Console.Write("Password: "******"create") { if (generate && keyfile == null) { Help("--generate requires a path specified by --keyfile to export the newly created key to!", command, options); } sman = SecretsManager.CreateStore(); } else { if (command == "update") { if (delete && (key != null || value != null)) { Help("Cannot specify both --delete and --key or --value!", command, options); } else if (!delete && (key == null || value == null)) { Help("Must specify both --key and --value to update!", command, options); } } else if (command == "decrypt") { if (!decryptAll && string.IsNullOrWhiteSpace(key)) { Help("Either --all or --key KEY must be specified as an argument to decrypt!", command, options); } } sman = SecretsManager.LoadStore(path); } using (sman) { if (generate) { sman.GenerateKey(); sman.ExportKey(keyfile); } else if (keyfile != null) { sman.LoadKeyFromFile(keyfile); } else { sman.LoadKeyFromPassword(password); } var client = new Client(sman); switch (command) { case "create": client.Create(); break; case "update": if (delete) { client.Delete(key); } else { client.Update(key, value); } break; case "decrypt": if (!decryptAll) { if (format != DecryptFormat.None) { Help($"--format can only be used in conjunction with --all!", command, options); } client.Decrypt(key); } else { client.DecryptAll(format); } break; default: throw new NotImplementedException($"Case {command} not handled!"); } sman.SaveStore(path); } } catch (OptionException ex) { Console.WriteLine(ex.Message); exitCode = 1; } catch (ExitCodeException ex) { if (!string.IsNullOrWhiteSpace(ex.Message)) { Console.WriteLine(ex.Message); } exitCode = ex.ExitCode; } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); exitCode = 1; } Environment.Exit(exitCode); }
static void Main(string[] mainArgs) { var args = new List <string>(mainArgs); bool help = false; bool version = false; string path = null; string password = null; string keyfile = null; string secretName = null; string secretValue = null; bool decryptAll = false; DecryptFormat format = DecryptFormat.None; var globalOptions = new OptionSet { { "h|help|?", "Show this help message and exit", _ => help = true }, { "v|version", "Print version information and exit", _ => version = true }, { "s|store=", "Load secrets store from provided path", s => path = s }, { "p|password:"******"Prompt for decryption password", p => password = p ?? "" }, { "k|keyfile=", "Load decryption key from path", k => keyfile = k }, }; var options = new Dictionary <string, OptionSet>(); options["create"] = new OptionSet { { "h|help", "Show help for create command options", _ => help = true }, // password is : because automated systems may use the command line to specify passwords. This may change! { "p|password:"******"Secure store with a key derived from a password", p => password = p ?? "" }, { "k|keyfile=", "Path to load or save secure key from/to", k => keyfile = k }, }; options["delete"] = new OptionSet { { "h|help", "Show help for delete command options", _ => help = true }, { "s|store=", "Load secrets store from provided path", s => path = s }, // password is : because automated systems may use the command line to specify passwords. This may change! { "p|password:"******"Decrypt store with key derived from password", p => password = p ?? "" }, { "k|keyfile=", "Path to load secure key from", k => keyfile = k }, }; options["get"] = new OptionSet { { "h|help", "Show help for decryption command options", _ => help = true }, { "s|store=", "Load secrets store from provided path", s => path = s }, // password is : because automated systems may use the command line to specify passwords. This may change! { "p|password:"******"Decrypt store with key derived from password", p => password = p ?? "" }, { "k|keyfile=", "Path to load secure key from", k => keyfile = k }, { "a|all", "Decrypt the entire contents of the store and print to stdout", _ => decryptAll = true }, { "t|output-format=", "Specify the output format: json (default), text", t => { if (!Parse(t, out format)) { throw new ExitCodeException(1, "Unsupported format specified!"); } } } }; options["set"] = new OptionSet { { "h|help", "Show help for encryption command options", _ => help = true }, { "s|store=", "Load secrets store from provided path", s => path = s }, // password is : because automated systems may use the command line to specify passwords. This may change! { "p|password:"******"Decrypt store with key derived from password", p => password = p ?? "" }, { "k|keyfile=", "Path to load secure key from", k => keyfile = k }, }; void printUsage(TextWriter output) { output.WriteLine($"ssclient [FLAGS] [create|set|get|delete] OPTIONS"); PrintOptions(output, null, globalOptions); output.WriteLine(); foreach (var kv in options) { PrintOptions(output, kv.Key, kv.Value); } } if (args.Count == 0) { Console.Out.Write("Usage: "); printUsage(Console.Out); Environment.Exit(1); } // Only try to parse --help and --version globalOptions.Parse(args); void printVersion() { Console.Out.WriteLine($"ssclient {AssemblyVersion} - SecureStore secrets manager client"); Console.Out.WriteLine("Copyright NeoSmart Technologies 2017-2020 - https://github.com/neosmart/SecureStore/"); } if (help) { printVersion(); Console.Out.WriteLine(); Console.Out.Write("Usage: "); printUsage(Console.Out); Environment.Exit(0); } if (version) { printVersion(); Environment.Exit(0); } int commandIndex = args.FindIndex(arg => options.ContainsKey(arg)); if (commandIndex < 0) { printUsage(Console.Error); Environment.Exit(1); } string command = args[commandIndex]; args.RemoveAt(commandIndex); OptionSet parseOptions = options[command]; int exitCode = 0; try { // Mono.Options is dumb and does not treat --password PASSWORD // as an option:value tuple when password is defined as taking an optional value. // It instead requires --password=PASSWORD or --password:PASSWORD or -pPASSWORD var bareArguments = parseOptions.Parse(args); if (bareArguments.Count > 0 && password == string.Empty) { // Check if this was the standalone password var possibles = new[] { "-p", "--password", "/p", "/password" }; for (int i = 0; i < args.Count - 1; ++i) { if (!possibles.Contains(args[i])) { continue; } // This was the password index var bareIndex = bareArguments.FindIndex(arg => arg == args[i + 1]); if (bareIndex >= 0) { password = bareArguments[bareIndex]; bareArguments.RemoveAt(bareIndex); break; } } } // Consume remaining bare parameters before carrying out any actions so we can validate // there are no unexpected bare parameters. if (command == "create") { if (bareArguments.Count > 0 && string.IsNullOrEmpty(path)) { path = bareArguments[0]; bareArguments.RemoveAt(0); } } else if (command == "get" || command == "delete") { if (!decryptAll && bareArguments.Count != 1) { Console.Error.WriteLine("Expected the name of a single secret to look up or delete!"); Environment.Exit(1); } else if (!decryptAll) { secretName = bareArguments[0]; bareArguments.RemoveAt(0); } } else if (command == "set") { if (bareArguments.Count == 1 && bareArguments[0].Contains('=')) { var parts = bareArguments[0].Split('=', 2); secretName = parts[0]; secretValue = parts[1]; bareArguments.RemoveAt(0); } else if (bareArguments.Count == 2 && !bareArguments[1].Contains('=')) { secretName = bareArguments[0]; secretValue = bareArguments[1]; bareArguments.RemoveRange(0, 2); } else { Console.Error.WriteLine("Expected a single \"key=value\" or \"key\" \"value\" to set!"); Environment.Exit(1); } } // Handle common parameters // if (bareArguments.Count > 0) // { // Console.Error.Write($"BareArguments[0]: {bareArguments[0]}"); // Help(Console.Error, "Invalid arguments!", command, parseOptions); // } if (path is null) { // Help(Console.Error, "A path to the secrets store is required!", command, parseOptions); // Default to secrets.json rather than error out path = "secrets.json"; } if (string.IsNullOrWhiteSpace(path)) { Help(Console.Error, "A path to the secrets store is required!", command, parseOptions); } if (keyfile == null && password == null) { // Help(Console.Error, "Must specify either --password or --keyfile!", command, parseOptions); // Default to password mode instead of erroring out password = string.Empty; } // We need to differentiate between null (not set) and empty (empty) if (password == string.Empty) { if (command == "create") { while (string.IsNullOrWhiteSpace(password)) { Console.Write("New password: "******"Confirm password: "******"Password: "******"create") { if (password is null && string.IsNullOrWhiteSpace(keyfile)) { Console.Error.WriteLine("A newly created store must have one or both of --password and --keyfile specified"); Environment.Exit(1); } if (!string.IsNullOrWhiteSpace(password) && File.Exists(keyfile) && new FileInfo(keyfile).Length > 0) { Confirm($"Overwrite the existing contents of the key file at {keyfile} " + "with a key derived from the provided password? [yes/no]: "); } sman = SecretsManager.CreateStore(); } else { if (command == "get") { if (decryptAll && !string.IsNullOrWhiteSpace(secretName)) { Help(Console.Error, "Either --all or KEY must be specified as an argument to decrypt (not both)!", command, parseOptions); } } sman = SecretsManager.LoadStore(path); } using (sman) { if (!string.IsNullOrEmpty(password)) { sman.LoadKeyFromPassword(password); } if (command == "create") { if (File.Exists(path) && new FileInfo(path).Length > 0) { Confirm($"Overwrite existing store at {path}? [yes/no]: "); } if (password == null) { if (File.Exists(keyfile) && new FileInfo(keyfile).Length > 0) { sman.LoadKeyFromFile(keyfile); } else { sman.GenerateKey(); sman.ExportKey(keyfile); } } else if (!string.IsNullOrEmpty(keyfile)) { sman.ExportKey(keyfile); } } else if (password == null && keyfile != null) { sman.LoadKeyFromFile(keyfile); } var client = new Client(sman); switch (command) { case "create": client.Create(); break; case "delete": client.Delete(secretName); break; case "set": client.Update(secretName, secretValue); break; case "get": if (!decryptAll) { if (format != DecryptFormat.None) { Help(Console.Error, "--format can only be used in conjunction with --all!", command, parseOptions); } client.Decrypt(secretName); } else { client.DecryptAll(format); } break; default: throw new NotImplementedException($"Case {command} not handled!"); } sman.SaveStore(path); } } catch (OptionException ex) { Console.WriteLine(ex.Message); exitCode = 1; } catch (ExitCodeException ex) { if (!string.IsNullOrWhiteSpace(ex.Message)) { Console.WriteLine(ex.Message); } exitCode = ex.ExitCode; } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); exitCode = 1; } Environment.Exit(exitCode); }