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
019 package org.apache.hadoop.crypto.key;
020
021 import java.io.IOException;
022 import java.io.PrintStream;
023 import java.security.InvalidParameterException;
024 import java.security.NoSuchAlgorithmException;
025 import java.util.HashMap;
026 import java.util.List;
027 import java.util.Map;
028
029 import org.apache.hadoop.conf.Configuration;
030 import org.apache.hadoop.conf.Configured;
031 import org.apache.hadoop.crypto.key.KeyProvider.Metadata;
032 import org.apache.hadoop.crypto.key.KeyProvider.Options;
033 import org.apache.hadoop.util.Tool;
034 import org.apache.hadoop.util.ToolRunner;
035
036 /**
037 * This program is the CLI utility for the KeyProvider facilities in Hadoop.
038 */
039 public 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 = false;
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 ("-i".equals(args[i]) || ("-interactive".equals(args[i]))) {
173 interactive = true;
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.getMessage());
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);
354 throw e;
355 }
356 } catch (IOException e1) {
357 out.println("Cannot roll key: " + keyName + " within KeyProvider: "
358 + provider);
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 = "delete <keyname> [-provider <provider>] [-help]";
371 public static final String DESC =
372 "The delete subcommand deletes all versions of the key\n" +
373 "specified by the <keyname> argument from within the\n" +
374 "provider specified -provider.";
375
376 String keyName = null;
377 boolean cont = true;
378
379 public DeleteCommand(String keyName) {
380 this.keyName = keyName;
381 }
382
383 @Override
384 public boolean validate() {
385 provider = getKeyProvider();
386 if (provider == null) {
387 out.println("There are no valid KeyProviders configured. Nothing\n"
388 + "was deleted. Use the -provider option to specify a provider.");
389 return false;
390 }
391 if (keyName == null) {
392 out.println("There is no keyName specified. Please specify a " +
393 "<keyname>. See the usage description with -help.");
394 return false;
395 }
396 if (interactive) {
397 try {
398 cont = ToolRunner
399 .confirmPrompt("You are about to DELETE all versions of "
400 + " key: " + keyName + " from KeyProvider "
401 + provider + ". Continue?:");
402 if (!cont) {
403 out.println("Nothing has been be deleted.");
404 }
405 return cont;
406 } catch (IOException e) {
407 out.println(keyName + " will not be deleted.");
408 e.printStackTrace(err);
409 }
410 }
411 return true;
412 }
413
414 public void execute() throws IOException {
415 warnIfTransientProvider();
416 out.println("Deleting key: " + keyName + " from KeyProvider: "
417 + provider);
418 if (cont) {
419 try {
420 provider.deleteKey(keyName);
421 provider.flush();
422 out.println(keyName + " has been successfully deleted.");
423 printProviderWritten();
424 } catch (IOException e) {
425 out.println(keyName + " has not been deleted.");
426 throw e;
427 }
428 }
429 }
430
431 @Override
432 public String getUsage() {
433 return USAGE + ":\n\n" + DESC;
434 }
435 }
436
437 private class CreateCommand extends Command {
438 public static final String USAGE =
439 "create <keyname> [-cipher <cipher>] [-size <size>]\n" +
440 " [-description <description>]\n" +
441 " [-attr <attribute=value>]\n" +
442 " [-provider <provider>] [-help]";
443 public static final String DESC =
444 "The create subcommand creates a new key for the name specified\n" +
445 "by the <keyname> argument within the provider specified by the\n" +
446 "-provider argument. You may specify a cipher with the -cipher\n" +
447 "argument. The default cipher is currently \"AES/CTR/NoPadding\".\n" +
448 "The default keysize is 128. You may specify the requested key\n" +
449 "length using the -size argument. Arbitrary attribute=value\n" +
450 "style attributes may be specified using the -attr argument.\n" +
451 "-attr may be specified multiple times, once per attribute.\n";
452
453 final String keyName;
454 final Options options;
455
456 public CreateCommand(String keyName, Options options) {
457 this.keyName = keyName;
458 this.options = options;
459 }
460
461 public boolean validate() {
462 boolean rc = true;
463 provider = getKeyProvider();
464 if (provider == null) {
465 out.println("There are no valid KeyProviders configured. No key\n" +
466 " was created. You can use the -provider option to specify\n" +
467 " a provider to use.");
468 rc = false;
469 }
470 if (keyName == null) {
471 out.println("Please provide a <keyname>. See the usage description" +
472 " with -help.");
473 rc = false;
474 }
475 return rc;
476 }
477
478 public void execute() throws IOException, NoSuchAlgorithmException {
479 warnIfTransientProvider();
480 try {
481 provider.createKey(keyName, options);
482 provider.flush();
483 out.println(keyName + " has been successfully created with options "
484 + options.toString() + ".");
485 printProviderWritten();
486 } catch (InvalidParameterException e) {
487 out.println(keyName + " has not been created. " + e.getMessage());
488 throw e;
489 } catch (IOException e) {
490 out.println(keyName + " has not been created. " + e.getMessage());
491 throw e;
492 } catch (NoSuchAlgorithmException e) {
493 out.println(keyName + " has not been created. " + e.getMessage());
494 throw e;
495 }
496 }
497
498 @Override
499 public String getUsage() {
500 return USAGE + ":\n\n" + DESC;
501 }
502 }
503
504 /**
505 * main() entry point for the KeyShell. While strictly speaking the
506 * return is void, it will System.exit() with a return code: 0 is for
507 * success and 1 for failure.
508 *
509 * @param args Command line arguments.
510 * @throws Exception
511 */
512 public static void main(String[] args) throws Exception {
513 int res = ToolRunner.run(new Configuration(), new KeyShell(), args);
514 System.exit(res);
515 }
516 }