001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.hadoop.crypto.key; 020 021import java.io.IOException; 022import java.io.PrintStream; 023import java.security.InvalidParameterException; 024import java.security.NoSuchAlgorithmException; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.hadoop.conf.Configuration; 030import org.apache.hadoop.conf.Configured; 031import org.apache.hadoop.crypto.key.KeyProvider.Metadata; 032import org.apache.hadoop.crypto.key.KeyProvider.Options; 033import org.apache.hadoop.util.Tool; 034import org.apache.hadoop.util.ToolRunner; 035 036/** 037 * This program is the CLI utility for the KeyProvider facilities in Hadoop. 038 */ 039public class KeyShell extends Configured implements Tool { 040 final static private String USAGE_PREFIX = "Usage: hadoop key " + 041 "[generic options]\n"; 042 final static private String COMMANDS = 043 " [-help]\n" + 044 " [" + CreateCommand.USAGE + "]\n" + 045 " [" + RollCommand.USAGE + "]\n" + 046 " [" + DeleteCommand.USAGE + "]\n" + 047 " [" + ListCommand.USAGE + "]\n"; 048 private static final String LIST_METADATA = "keyShell.list.metadata"; 049 050 private boolean interactive = true; 051 private Command command = null; 052 053 /** allows stdout to be captured if necessary */ 054 public PrintStream out = System.out; 055 /** allows stderr to be captured if necessary */ 056 public PrintStream err = System.err; 057 058 private boolean userSuppliedProvider = false; 059 060 /** 061 * Primary entry point for the KeyShell; called via main(). 062 * 063 * @param args Command line arguments. 064 * @return 0 on success and 1 on failure. This value is passed back to 065 * the unix shell, so we must follow shell return code conventions: 066 * the return code is an unsigned character, and 0 means success, and 067 * small positive integers mean failure. 068 * @throws Exception 069 */ 070 @Override 071 public int run(String[] args) throws Exception { 072 int exitCode = 0; 073 try { 074 exitCode = init(args); 075 if (exitCode != 0) { 076 return exitCode; 077 } 078 if (command.validate()) { 079 command.execute(); 080 } else { 081 exitCode = 1; 082 } 083 } catch (Exception e) { 084 e.printStackTrace(err); 085 return 1; 086 } 087 return exitCode; 088 } 089 090 /** 091 * Parse the command line arguments and initialize the data 092 * <pre> 093 * % hadoop key create keyName [-size size] [-cipher algorithm] 094 * [-provider providerPath] 095 * % hadoop key roll keyName [-provider providerPath] 096 * % hadoop key list [-provider providerPath] 097 * % hadoop key delete keyName [-provider providerPath] [-i] 098 * </pre> 099 * @param args Command line arguments. 100 * @return 0 on success, 1 on failure. 101 * @throws IOException 102 */ 103 private int init(String[] args) throws IOException { 104 final Options options = KeyProvider.options(getConf()); 105 final Map<String, String> attributes = new HashMap<String, String>(); 106 107 for (int i = 0; i < args.length; i++) { // parse command line 108 boolean moreTokens = (i < args.length - 1); 109 if (args[i].equals("create")) { 110 String keyName = "-help"; 111 if (moreTokens) { 112 keyName = args[++i]; 113 } 114 115 command = new CreateCommand(keyName, options); 116 if ("-help".equals(keyName)) { 117 printKeyShellUsage(); 118 return 1; 119 } 120 } else if (args[i].equals("delete")) { 121 String keyName = "-help"; 122 if (moreTokens) { 123 keyName = args[++i]; 124 } 125 126 command = new DeleteCommand(keyName); 127 if ("-help".equals(keyName)) { 128 printKeyShellUsage(); 129 return 1; 130 } 131 } else if (args[i].equals("roll")) { 132 String keyName = "-help"; 133 if (moreTokens) { 134 keyName = args[++i]; 135 } 136 137 command = new RollCommand(keyName); 138 if ("-help".equals(keyName)) { 139 printKeyShellUsage(); 140 return 1; 141 } 142 } else if ("list".equals(args[i])) { 143 command = new ListCommand(); 144 } else if ("-size".equals(args[i]) && moreTokens) { 145 options.setBitLength(Integer.parseInt(args[++i])); 146 } else if ("-cipher".equals(args[i]) && moreTokens) { 147 options.setCipher(args[++i]); 148 } else if ("-description".equals(args[i]) && moreTokens) { 149 options.setDescription(args[++i]); 150 } else if ("-attr".equals(args[i]) && moreTokens) { 151 final String attrval[] = args[++i].split("=", 2); 152 final String attr = attrval[0].trim(); 153 final String val = attrval[1].trim(); 154 if (attr.isEmpty() || val.isEmpty()) { 155 out.println("\nAttributes must be in attribute=value form, " + 156 "or quoted\nlike \"attribute = value\"\n"); 157 printKeyShellUsage(); 158 return 1; 159 } 160 if (attributes.containsKey(attr)) { 161 out.println("\nEach attribute must correspond to only one value:\n" + 162 "atttribute \"" + attr + "\" was repeated\n" ); 163 printKeyShellUsage(); 164 return 1; 165 } 166 attributes.put(attr, val); 167 } else if ("-provider".equals(args[i]) && moreTokens) { 168 userSuppliedProvider = true; 169 getConf().set(KeyProviderFactory.KEY_PROVIDER_PATH, args[++i]); 170 } else if ("-metadata".equals(args[i])) { 171 getConf().setBoolean(LIST_METADATA, true); 172 } else if ("-f".equals(args[i]) || ("-force".equals(args[i]))) { 173 interactive = false; 174 } else if ("-help".equals(args[i])) { 175 printKeyShellUsage(); 176 return 1; 177 } else { 178 printKeyShellUsage(); 179 ToolRunner.printGenericCommandUsage(System.err); 180 return 1; 181 } 182 } 183 184 if (command == null) { 185 printKeyShellUsage(); 186 return 1; 187 } 188 189 if (!attributes.isEmpty()) { 190 options.setAttributes(attributes); 191 } 192 193 return 0; 194 } 195 196 private void printKeyShellUsage() { 197 out.println(USAGE_PREFIX + COMMANDS); 198 if (command != null) { 199 out.println(command.getUsage()); 200 } else { 201 out.println("=========================================================" + 202 "======"); 203 out.println(CreateCommand.USAGE + ":\n\n" + CreateCommand.DESC); 204 out.println("=========================================================" + 205 "======"); 206 out.println(RollCommand.USAGE + ":\n\n" + RollCommand.DESC); 207 out.println("=========================================================" + 208 "======"); 209 out.println(DeleteCommand.USAGE + ":\n\n" + DeleteCommand.DESC); 210 out.println("=========================================================" + 211 "======"); 212 out.println(ListCommand.USAGE + ":\n\n" + ListCommand.DESC); 213 } 214 } 215 216 private abstract class Command { 217 protected KeyProvider provider = null; 218 219 public boolean validate() { 220 return true; 221 } 222 223 protected KeyProvider getKeyProvider() { 224 KeyProvider provider = null; 225 List<KeyProvider> providers; 226 try { 227 providers = KeyProviderFactory.getProviders(getConf()); 228 if (userSuppliedProvider) { 229 provider = providers.get(0); 230 } else { 231 for (KeyProvider p : providers) { 232 if (!p.isTransient()) { 233 provider = p; 234 break; 235 } 236 } 237 } 238 } catch (IOException e) { 239 e.printStackTrace(err); 240 } 241 return provider; 242 } 243 244 protected void printProviderWritten() { 245 out.println(provider + " has been updated."); 246 } 247 248 protected void warnIfTransientProvider() { 249 if (provider.isTransient()) { 250 out.println("WARNING: you are modifying a transient provider."); 251 } 252 } 253 254 public abstract void execute() throws Exception; 255 256 public abstract String getUsage(); 257 } 258 259 private class ListCommand extends Command { 260 public static final String USAGE = 261 "list [-provider <provider>] [-metadata] [-help]"; 262 public static final String DESC = 263 "The list subcommand displays the keynames contained within\n" + 264 "a particular provider as configured in core-site.xml or\n" + 265 "specified with the -provider argument. -metadata displays\n" + 266 "the metadata."; 267 268 private boolean metadata = false; 269 270 public boolean validate() { 271 boolean rc = true; 272 provider = getKeyProvider(); 273 if (provider == null) { 274 out.println("There are no non-transient KeyProviders configured.\n" 275 + "Use the -provider option to specify a provider. If you\n" 276 + "want to list a transient provider then you must use the\n" 277 + "-provider argument."); 278 rc = false; 279 } 280 metadata = getConf().getBoolean(LIST_METADATA, false); 281 return rc; 282 } 283 284 public void execute() throws IOException { 285 try { 286 final List<String> keys = provider.getKeys(); 287 out.println("Listing keys for KeyProvider: " + provider); 288 if (metadata) { 289 final Metadata[] meta = 290 provider.getKeysMetadata(keys.toArray(new String[keys.size()])); 291 for (int i = 0; i < meta.length; ++i) { 292 out.println(keys.get(i) + " : " + meta[i]); 293 } 294 } else { 295 for (String keyName : keys) { 296 out.println(keyName); 297 } 298 } 299 } catch (IOException e) { 300 out.println("Cannot list keys for KeyProvider: " + provider 301 + ": " + e.toString()); 302 throw e; 303 } 304 } 305 306 @Override 307 public String getUsage() { 308 return USAGE + ":\n\n" + DESC; 309 } 310 } 311 312 private class RollCommand extends Command { 313 public static final String USAGE = "roll <keyname> [-provider <provider>] [-help]"; 314 public static final String DESC = 315 "The roll subcommand creates a new version for the specified key\n" + 316 "within the provider indicated using the -provider argument\n"; 317 318 String keyName = null; 319 320 public RollCommand(String keyName) { 321 this.keyName = keyName; 322 } 323 324 public boolean validate() { 325 boolean rc = true; 326 provider = getKeyProvider(); 327 if (provider == null) { 328 out.println("There are no valid KeyProviders configured. The key\n" + 329 "has not been rolled. Use the -provider option to specify\n" + 330 "a provider."); 331 rc = false; 332 } 333 if (keyName == null) { 334 out.println("Please provide a <keyname>.\n" + 335 "See the usage description by using -help."); 336 rc = false; 337 } 338 return rc; 339 } 340 341 public void execute() throws NoSuchAlgorithmException, IOException { 342 try { 343 warnIfTransientProvider(); 344 out.println("Rolling key version from KeyProvider: " 345 + provider + "\n for key name: " + keyName); 346 try { 347 provider.rollNewVersion(keyName); 348 provider.flush(); 349 out.println(keyName + " has been successfully rolled."); 350 printProviderWritten(); 351 } catch (NoSuchAlgorithmException e) { 352 out.println("Cannot roll key: " + keyName + " within KeyProvider: " 353 + provider + ". " + e.toString()); 354 throw e; 355 } 356 } catch (IOException e1) { 357 out.println("Cannot roll key: " + keyName + " within KeyProvider: " 358 + provider + ". " + e1.toString()); 359 throw e1; 360 } 361 } 362 363 @Override 364 public String getUsage() { 365 return USAGE + ":\n\n" + DESC; 366 } 367 } 368 369 private class DeleteCommand extends Command { 370 public static final String USAGE = 371 "delete <keyname> [-provider <provider>] [-f] [-help]"; 372 public static final String DESC = 373 "The delete subcommand deletes all versions of the key\n" + 374 "specified by the <keyname> argument from within the\n" + 375 "provider specified -provider. The command asks for\n" + 376 "user confirmation unless -f is specified."; 377 378 String keyName = null; 379 boolean cont = true; 380 381 public DeleteCommand(String keyName) { 382 this.keyName = keyName; 383 } 384 385 @Override 386 public boolean validate() { 387 provider = getKeyProvider(); 388 if (provider == null) { 389 out.println("There are no valid KeyProviders configured. Nothing\n" 390 + "was deleted. Use the -provider option to specify a provider."); 391 return false; 392 } 393 if (keyName == null) { 394 out.println("There is no keyName specified. Please specify a " + 395 "<keyname>. See the usage description with -help."); 396 return false; 397 } 398 if (interactive) { 399 try { 400 cont = ToolRunner 401 .confirmPrompt("You are about to DELETE all versions of " 402 + " key " + keyName + " from KeyProvider " 403 + provider + ". Continue? "); 404 if (!cont) { 405 out.println(keyName + " has not been deleted."); 406 } 407 return cont; 408 } catch (IOException e) { 409 out.println(keyName + " will not be deleted."); 410 e.printStackTrace(err); 411 } 412 } 413 return true; 414 } 415 416 public void execute() throws IOException { 417 warnIfTransientProvider(); 418 out.println("Deleting key: " + keyName + " from KeyProvider: " 419 + provider); 420 if (cont) { 421 try { 422 provider.deleteKey(keyName); 423 provider.flush(); 424 out.println(keyName + " has been successfully deleted."); 425 printProviderWritten(); 426 } catch (IOException e) { 427 out.println(keyName + " has not been deleted. " + e.toString()); 428 throw e; 429 } 430 } 431 } 432 433 @Override 434 public String getUsage() { 435 return USAGE + ":\n\n" + DESC; 436 } 437 } 438 439 private class CreateCommand extends Command { 440 public static final String USAGE = 441 "create <keyname> [-cipher <cipher>] [-size <size>]\n" + 442 " [-description <description>]\n" + 443 " [-attr <attribute=value>]\n" + 444 " [-provider <provider>] [-help]"; 445 public static final String DESC = 446 "The create subcommand creates a new key for the name specified\n" + 447 "by the <keyname> argument within the provider specified by the\n" + 448 "-provider argument. You may specify a cipher with the -cipher\n" + 449 "argument. The default cipher is currently \"AES/CTR/NoPadding\".\n" + 450 "The default keysize is 128. You may specify the requested key\n" + 451 "length using the -size argument. Arbitrary attribute=value\n" + 452 "style attributes may be specified using the -attr argument.\n" + 453 "-attr may be specified multiple times, once per attribute.\n"; 454 455 final String keyName; 456 final Options options; 457 458 public CreateCommand(String keyName, Options options) { 459 this.keyName = keyName; 460 this.options = options; 461 } 462 463 public boolean validate() { 464 boolean rc = true; 465 provider = getKeyProvider(); 466 if (provider == null) { 467 out.println("There are no valid KeyProviders configured. No key\n" + 468 " was created. You can use the -provider option to specify\n" + 469 " a provider to use."); 470 rc = false; 471 } 472 if (keyName == null) { 473 out.println("Please provide a <keyname>. See the usage description" + 474 " with -help."); 475 rc = false; 476 } 477 return rc; 478 } 479 480 public void execute() throws IOException, NoSuchAlgorithmException { 481 warnIfTransientProvider(); 482 try { 483 provider.createKey(keyName, options); 484 provider.flush(); 485 out.println(keyName + " has been successfully created with options " 486 + options.toString() + "."); 487 printProviderWritten(); 488 } catch (InvalidParameterException e) { 489 out.println(keyName + " has not been created. " + e.toString()); 490 throw e; 491 } catch (IOException e) { 492 out.println(keyName + " has not been created. " + e.toString()); 493 throw e; 494 } catch (NoSuchAlgorithmException e) { 495 out.println(keyName + " has not been created. " + e.toString()); 496 throw e; 497 } 498 } 499 500 @Override 501 public String getUsage() { 502 return USAGE + ":\n\n" + DESC; 503 } 504 } 505 506 /** 507 * main() entry point for the KeyShell. While strictly speaking the 508 * return is void, it will System.exit() with a return code: 0 is for 509 * success and 1 for failure. 510 * 511 * @param args Command line arguments. 512 * @throws Exception 513 */ 514 public static void main(String[] args) throws Exception { 515 int res = ToolRunner.run(new Configuration(), new KeyShell(), args); 516 System.exit(res); 517 } 518}