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    }