| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.hadoop.crypto.key; |
| |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.security.InvalidParameterException; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.conf.Configured; |
| import org.apache.hadoop.crypto.key.KeyProvider.Metadata; |
| import org.apache.hadoop.crypto.key.KeyProvider.Options; |
| import org.apache.hadoop.util.Tool; |
| import org.apache.hadoop.util.ToolRunner; |
| |
| /** |
| * This program is the CLI utility for the KeyProvider facilities in Hadoop. |
| */ |
| public class KeyShell extends Configured implements Tool { |
| final static private String USAGE_PREFIX = "Usage: hadoop key " + |
| "[generic options]\n"; |
| final static private String COMMANDS = |
| " [-help]\n" + |
| " [" + CreateCommand.USAGE + "]\n" + |
| " [" + RollCommand.USAGE + "]\n" + |
| " [" + DeleteCommand.USAGE + "]\n" + |
| " [" + ListCommand.USAGE + "]\n"; |
| private static final String LIST_METADATA = "keyShell.list.metadata"; |
| @VisibleForTesting |
| public static final String NO_VALID_PROVIDERS = |
| "There are no valid (non-transient) providers configured.\n" + |
| "No action has been taken. Use the -provider option to specify\n" + |
| "a provider. If you want to use a transient provider then you\n" + |
| "MUST use the -provider argument."; |
| |
| private boolean interactive = true; |
| private Command command = null; |
| |
| /** If true, fail if the provider requires a password and none is given. */ |
| private boolean strict = false; |
| |
| /** allows stdout to be captured if necessary. */ |
| @VisibleForTesting |
| public PrintStream out = System.out; |
| /** allows stderr to be captured if necessary. */ |
| @VisibleForTesting |
| public PrintStream err = System.err; |
| |
| private boolean userSuppliedProvider = false; |
| |
| /** |
| * Primary entry point for the KeyShell; called via main(). |
| * |
| * @param args Command line arguments. |
| * @return 0 on success and 1 on failure. This value is passed back to |
| * the unix shell, so we must follow shell return code conventions: |
| * the return code is an unsigned character, and 0 means success, and |
| * small positive integers mean failure. |
| * @throws Exception |
| */ |
| @Override |
| public int run(String[] args) throws Exception { |
| int exitCode = 0; |
| try { |
| exitCode = init(args); |
| if (exitCode != 0) { |
| return exitCode; |
| } |
| if (command.validate()) { |
| command.execute(); |
| } else { |
| exitCode = 1; |
| } |
| } catch (Exception e) { |
| e.printStackTrace(err); |
| return 1; |
| } |
| return exitCode; |
| } |
| |
| /** |
| * Parse the command line arguments and initialize the data. |
| * <pre> |
| * % hadoop key create keyName [-size size] [-cipher algorithm] |
| * [-provider providerPath] |
| * % hadoop key roll keyName [-provider providerPath] |
| * % hadoop key list [-provider providerPath] |
| * % hadoop key delete keyName [-provider providerPath] [-i] |
| * </pre> |
| * @param args Command line arguments. |
| * @return 0 on success, 1 on failure. |
| * @throws IOException |
| */ |
| private int init(String[] args) throws IOException { |
| final Options options = KeyProvider.options(getConf()); |
| final Map<String, String> attributes = new HashMap<String, String>(); |
| |
| for (int i = 0; i < args.length; i++) { // parse command line |
| boolean moreTokens = (i < args.length - 1); |
| if (args[i].equals("create")) { |
| String keyName = "-help"; |
| if (moreTokens) { |
| keyName = args[++i]; |
| } |
| |
| command = new CreateCommand(keyName, options); |
| if ("-help".equals(keyName)) { |
| printKeyShellUsage(); |
| return 1; |
| } |
| } else if (args[i].equals("delete")) { |
| String keyName = "-help"; |
| if (moreTokens) { |
| keyName = args[++i]; |
| } |
| |
| command = new DeleteCommand(keyName); |
| if ("-help".equals(keyName)) { |
| printKeyShellUsage(); |
| return 1; |
| } |
| } else if (args[i].equals("roll")) { |
| String keyName = "-help"; |
| if (moreTokens) { |
| keyName = args[++i]; |
| } |
| |
| command = new RollCommand(keyName); |
| if ("-help".equals(keyName)) { |
| printKeyShellUsage(); |
| return 1; |
| } |
| } else if ("list".equals(args[i])) { |
| command = new ListCommand(); |
| } else if ("-size".equals(args[i]) && moreTokens) { |
| options.setBitLength(Integer.parseInt(args[++i])); |
| } else if ("-cipher".equals(args[i]) && moreTokens) { |
| options.setCipher(args[++i]); |
| } else if ("-description".equals(args[i]) && moreTokens) { |
| options.setDescription(args[++i]); |
| } else if ("-attr".equals(args[i]) && moreTokens) { |
| final String attrval[] = args[++i].split("=", 2); |
| final String attr = attrval[0].trim(); |
| final String val = attrval[1].trim(); |
| if (attr.isEmpty() || val.isEmpty()) { |
| out.println("\nAttributes must be in attribute=value form, " + |
| "or quoted\nlike \"attribute = value\"\n"); |
| printKeyShellUsage(); |
| return 1; |
| } |
| if (attributes.containsKey(attr)) { |
| out.println("\nEach attribute must correspond to only one value:\n" + |
| "atttribute \"" + attr + "\" was repeated\n" ); |
| printKeyShellUsage(); |
| return 1; |
| } |
| attributes.put(attr, val); |
| } else if ("-provider".equals(args[i]) && moreTokens) { |
| userSuppliedProvider = true; |
| getConf().set(KeyProviderFactory.KEY_PROVIDER_PATH, args[++i]); |
| } else if ("-metadata".equals(args[i])) { |
| getConf().setBoolean(LIST_METADATA, true); |
| } else if ("-f".equals(args[i]) || ("-force".equals(args[i]))) { |
| interactive = false; |
| } else if (args[i].equals("-strict")) { |
| strict = true; |
| } else if ("-help".equals(args[i])) { |
| printKeyShellUsage(); |
| return 1; |
| } else { |
| printKeyShellUsage(); |
| ToolRunner.printGenericCommandUsage(System.err); |
| return 1; |
| } |
| } |
| |
| if (command == null) { |
| printKeyShellUsage(); |
| return 1; |
| } |
| |
| if (!attributes.isEmpty()) { |
| options.setAttributes(attributes); |
| } |
| |
| return 0; |
| } |
| |
| private void printKeyShellUsage() { |
| out.println(USAGE_PREFIX + COMMANDS); |
| if (command != null) { |
| out.println(command.getUsage()); |
| } else { |
| out.println("=========================================================" + |
| "======"); |
| out.println(CreateCommand.USAGE + ":\n\n" + CreateCommand.DESC); |
| out.println("=========================================================" + |
| "======"); |
| out.println(RollCommand.USAGE + ":\n\n" + RollCommand.DESC); |
| out.println("=========================================================" + |
| "======"); |
| out.println(DeleteCommand.USAGE + ":\n\n" + DeleteCommand.DESC); |
| out.println("=========================================================" + |
| "======"); |
| out.println(ListCommand.USAGE + ":\n\n" + ListCommand.DESC); |
| } |
| } |
| |
| private abstract class Command { |
| protected KeyProvider provider = null; |
| |
| public boolean validate() { |
| return true; |
| } |
| |
| protected KeyProvider getKeyProvider() { |
| KeyProvider prov = null; |
| List<KeyProvider> providers; |
| try { |
| providers = KeyProviderFactory.getProviders(getConf()); |
| if (userSuppliedProvider) { |
| prov = providers.get(0); |
| } else { |
| for (KeyProvider p : providers) { |
| if (!p.isTransient()) { |
| prov = p; |
| break; |
| } |
| } |
| } |
| } catch (IOException e) { |
| e.printStackTrace(err); |
| } |
| if (prov == null) { |
| out.println(NO_VALID_PROVIDERS); |
| } |
| return prov; |
| } |
| |
| protected void printProviderWritten() { |
| out.println(provider + " has been updated."); |
| } |
| |
| protected void warnIfTransientProvider() { |
| if (provider.isTransient()) { |
| out.println("WARNING: you are modifying a transient provider."); |
| } |
| } |
| |
| public abstract void execute() throws Exception; |
| |
| public abstract String getUsage(); |
| } |
| |
| private class ListCommand extends Command { |
| public static final String USAGE = |
| "list [-provider <provider>] [-strict] [-metadata] [-help]"; |
| public static final String DESC = |
| "The list subcommand displays the keynames contained within\n" + |
| "a particular provider as configured in core-site.xml or\n" + |
| "specified with the -provider argument. -metadata displays\n" + |
| "the metadata. If -strict is supplied, fail immediately if\n" + |
| "the provider requires a password and none is given."; |
| |
| private boolean metadata = false; |
| |
| public boolean validate() { |
| boolean rc = true; |
| provider = getKeyProvider(); |
| if (provider == null) { |
| rc = false; |
| } |
| metadata = getConf().getBoolean(LIST_METADATA, false); |
| return rc; |
| } |
| |
| public void execute() throws IOException { |
| try { |
| final List<String> keys = provider.getKeys(); |
| out.println("Listing keys for KeyProvider: " + provider); |
| if (metadata) { |
| final Metadata[] meta = |
| provider.getKeysMetadata(keys.toArray(new String[keys.size()])); |
| for (int i = 0; i < meta.length; ++i) { |
| out.println(keys.get(i) + " : " + meta[i]); |
| } |
| } else { |
| for (String keyName : keys) { |
| out.println(keyName); |
| } |
| } |
| } catch (IOException e) { |
| out.println("Cannot list keys for KeyProvider: " + provider |
| + ": " + e.toString()); |
| throw e; |
| } |
| } |
| |
| @Override |
| public String getUsage() { |
| return USAGE + ":\n\n" + DESC; |
| } |
| } |
| |
| private class RollCommand extends Command { |
| public static final String USAGE = |
| "roll <keyname> [-provider <provider>] [-strict] [-help]"; |
| public static final String DESC = |
| "The roll subcommand creates a new version for the specified key\n" + |
| "within the provider indicated using the -provider argument.\n" + |
| "If -strict is supplied, fail immediately if the provider requires\n" + |
| "a password and none is given."; |
| |
| private String keyName = null; |
| |
| public RollCommand(String keyName) { |
| this.keyName = keyName; |
| } |
| |
| public boolean validate() { |
| boolean rc = true; |
| provider = getKeyProvider(); |
| if (provider == null) { |
| rc = false; |
| } |
| if (keyName == null) { |
| out.println("Please provide a <keyname>.\n" + |
| "See the usage description by using -help."); |
| rc = false; |
| } |
| return rc; |
| } |
| |
| public void execute() throws NoSuchAlgorithmException, IOException { |
| try { |
| warnIfTransientProvider(); |
| out.println("Rolling key version from KeyProvider: " |
| + provider + "\n for key name: " + keyName); |
| try { |
| provider.rollNewVersion(keyName); |
| provider.flush(); |
| out.println(keyName + " has been successfully rolled."); |
| printProviderWritten(); |
| } catch (NoSuchAlgorithmException e) { |
| out.println("Cannot roll key: " + keyName + " within KeyProvider: " |
| + provider + ". " + e.toString()); |
| throw e; |
| } |
| } catch (IOException e1) { |
| out.println("Cannot roll key: " + keyName + " within KeyProvider: " |
| + provider + ". " + e1.toString()); |
| throw e1; |
| } |
| } |
| |
| @Override |
| public String getUsage() { |
| return USAGE + ":\n\n" + DESC; |
| } |
| } |
| |
| private class DeleteCommand extends Command { |
| public static final String USAGE = |
| "delete <keyname> [-provider <provider>] [-strict] [-f] [-help]"; |
| public static final String DESC = |
| "The delete subcommand deletes all versions of the key\n" + |
| "specified by the <keyname> argument from within the\n" + |
| "provider specified by -provider. The command asks for\n" + |
| "user confirmation unless -f is specified. If -strict is\n" + |
| "supplied, fail immediately if the provider requires a\n" + |
| "password and none is given."; |
| |
| private String keyName = null; |
| private boolean cont = true; |
| |
| public DeleteCommand(String keyName) { |
| this.keyName = keyName; |
| } |
| |
| @Override |
| public boolean validate() { |
| provider = getKeyProvider(); |
| if (provider == null) { |
| return false; |
| } |
| if (keyName == null) { |
| out.println("There is no keyName specified. Please specify a " + |
| "<keyname>. See the usage description with -help."); |
| return false; |
| } |
| if (interactive) { |
| try { |
| cont = ToolRunner |
| .confirmPrompt("You are about to DELETE all versions of " |
| + " key " + keyName + " from KeyProvider " |
| + provider + ". Continue? "); |
| if (!cont) { |
| out.println(keyName + " has not been deleted."); |
| } |
| return cont; |
| } catch (IOException e) { |
| out.println(keyName + " will not be deleted."); |
| e.printStackTrace(err); |
| } |
| } |
| return true; |
| } |
| |
| public void execute() throws IOException { |
| warnIfTransientProvider(); |
| out.println("Deleting key: " + keyName + " from KeyProvider: " |
| + provider); |
| if (cont) { |
| try { |
| provider.deleteKey(keyName); |
| provider.flush(); |
| out.println(keyName + " has been successfully deleted."); |
| printProviderWritten(); |
| } catch (IOException e) { |
| out.println(keyName + " has not been deleted. " + e.toString()); |
| throw e; |
| } |
| } |
| } |
| |
| @Override |
| public String getUsage() { |
| return USAGE + ":\n\n" + DESC; |
| } |
| } |
| |
| private class CreateCommand extends Command { |
| public static final String USAGE = |
| "create <keyname> [-cipher <cipher>] [-size <size>]\n" + |
| " [-description <description>]\n" + |
| " [-attr <attribute=value>]\n" + |
| " [-provider <provider>] [-strict]\n" + |
| " [-help]"; |
| public static final String DESC = |
| "The create subcommand creates a new key for the name specified\n" + |
| "by the <keyname> argument within the provider specified by the\n" + |
| "-provider argument. You may specify a cipher with the -cipher\n" + |
| "argument. The default cipher is currently \"AES/CTR/NoPadding\".\n" + |
| "The default keysize is 128. You may specify the requested key\n" + |
| "length using the -size argument. Arbitrary attribute=value\n" + |
| "style attributes may be specified using the -attr argument.\n" + |
| "-attr may be specified multiple times, once per attribute.\n"; |
| |
| private final String keyName; |
| private final Options options; |
| |
| public CreateCommand(String keyName, Options options) { |
| this.keyName = keyName; |
| this.options = options; |
| } |
| |
| public boolean validate() { |
| boolean rc = true; |
| try { |
| provider = getKeyProvider(); |
| if (provider == null) { |
| rc = false; |
| } else if (provider.needsPassword()) { |
| if (strict) { |
| out.println(provider.noPasswordError()); |
| rc = false; |
| } else { |
| out.println(provider.noPasswordWarning()); |
| } |
| } |
| } catch (IOException e) { |
| e.printStackTrace(err); |
| } |
| if (keyName == null) { |
| out.println("Please provide a <keyname>. See the usage description" + |
| " with -help."); |
| rc = false; |
| } |
| return rc; |
| } |
| |
| public void execute() throws IOException, NoSuchAlgorithmException { |
| warnIfTransientProvider(); |
| try { |
| provider.createKey(keyName, options); |
| provider.flush(); |
| out.println(keyName + " has been successfully created with options " |
| + options.toString() + "."); |
| printProviderWritten(); |
| } catch (InvalidParameterException e) { |
| out.println(keyName + " has not been created. " + e.toString()); |
| throw e; |
| } catch (IOException e) { |
| out.println(keyName + " has not been created. " + e.toString()); |
| throw e; |
| } catch (NoSuchAlgorithmException e) { |
| out.println(keyName + " has not been created. " + e.toString()); |
| throw e; |
| } |
| } |
| |
| @Override |
| public String getUsage() { |
| return USAGE + ":\n\n" + DESC; |
| } |
| } |
| |
| /** |
| * main() entry point for the KeyShell. While strictly speaking the |
| * return is void, it will System.exit() with a return code: 0 is for |
| * success and 1 for failure. |
| * |
| * @param args Command line arguments. |
| * @throws Exception |
| */ |
| public static void main(String[] args) throws Exception { |
| int res = ToolRunner.run(new Configuration(), new KeyShell(), args); |
| System.exit(res); |
| } |
| } |