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.ByteArrayInputStream;
022import java.io.ByteArrayOutputStream;
023import java.io.IOException;
024import java.io.InputStreamReader;
025import java.io.OutputStreamWriter;
026import java.nio.charset.StandardCharsets;
027import java.security.NoSuchAlgorithmException;
028import java.util.Collections;
029import java.util.Date;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033
034import com.google.gson.stream.JsonReader;
035import com.google.gson.stream.JsonWriter;
036import org.apache.hadoop.classification.InterfaceAudience;
037import org.apache.hadoop.classification.InterfaceStability;
038import org.apache.hadoop.conf.Configuration;
039
040import javax.crypto.KeyGenerator;
041
042/**
043 * A provider of secret key material for Hadoop applications. Provides an
044 * abstraction to separate key storage from users of encryption. It
045 * is intended to support getting or storing keys in a variety of ways,
046 * including third party bindings.
047 * <P/>
048 * <code>KeyProvider</code> implementations must be thread safe.
049 */
050@InterfaceAudience.Public
051@InterfaceStability.Unstable
052public abstract class KeyProvider {
053  public static final String DEFAULT_CIPHER_NAME =
054      "hadoop.security.key.default.cipher";
055  public static final String DEFAULT_CIPHER = "AES/CTR/NoPadding";
056  public static final String DEFAULT_BITLENGTH_NAME =
057      "hadoop.security.key.default.bitlength";
058  public static final int DEFAULT_BITLENGTH = 128;
059
060  private final Configuration conf;
061
062  /**
063   * The combination of both the key version name and the key material.
064   */
065  public static class KeyVersion {
066    private final String name;
067    private final String versionName;
068    private final byte[] material;
069
070    protected KeyVersion(String name, String versionName,
071                         byte[] material) {
072      this.name = name;
073      this.versionName = versionName;
074      this.material = material;
075    }
076
077    public String getName() {
078      return name;
079    }
080
081    public String getVersionName() {
082      return versionName;
083    }
084
085    public byte[] getMaterial() {
086      return material;
087    }
088
089    public String toString() {
090      StringBuilder buf = new StringBuilder();
091      buf.append("key(");
092      buf.append(versionName);
093      buf.append(")=");
094      if (material == null) {
095        buf.append("null");
096      } else {
097        for(byte b: material) {
098          buf.append(' ');
099          int right = b & 0xff;
100          if (right < 0x10) {
101            buf.append('0');
102          }
103          buf.append(Integer.toHexString(right));
104        }
105      }
106      return buf.toString();
107    }
108  }
109
110  /**
111   * Key metadata that is associated with the key.
112   */
113  public static class Metadata {
114    private final static String CIPHER_FIELD = "cipher";
115    private final static String BIT_LENGTH_FIELD = "bitLength";
116    private final static String CREATED_FIELD = "created";
117    private final static String DESCRIPTION_FIELD = "description";
118    private final static String VERSIONS_FIELD = "versions";
119    private final static String ATTRIBUTES_FIELD = "attributes";
120
121    private final String cipher;
122    private final int bitLength;
123    private final String description;
124    private final Date created;
125    private int versions;
126    private Map<String, String> attributes;
127
128    protected Metadata(String cipher, int bitLength, String description,
129        Map<String, String> attributes, Date created, int versions) {
130      this.cipher = cipher;
131      this.bitLength = bitLength;
132      this.description = description;
133      this.attributes = (attributes == null || attributes.isEmpty())
134                        ? null : attributes;
135      this.created = created;
136      this.versions = versions;
137    }
138
139    public String toString() {
140      final StringBuilder metaSB = new StringBuilder();
141      metaSB.append("cipher: ").append(cipher).append(", ");
142      metaSB.append("length: ").append(bitLength).append(", ");
143      metaSB.append("description: ").append(description).append(", ");
144      metaSB.append("created: ").append(created).append(", ");
145      metaSB.append("version: ").append(versions).append(", ");
146      metaSB.append("attributes: ");
147      if ((attributes != null) && !attributes.isEmpty()) {
148        for (Map.Entry<String, String> attribute : attributes.entrySet()) {
149          metaSB.append("[");
150          metaSB.append(attribute.getKey());
151          metaSB.append("=");
152          metaSB.append(attribute.getValue());
153          metaSB.append("], ");
154        }
155        metaSB.deleteCharAt(metaSB.length() - 2);  // remove last ', '
156      } else {
157        metaSB.append("null");
158      }
159      return metaSB.toString();
160    }
161
162    public String getDescription() {
163      return description;
164    }
165
166    public Date getCreated() {
167      return created;
168    }
169
170    public String getCipher() {
171      return cipher;
172    }
173
174    @SuppressWarnings("unchecked")
175    public Map<String, String> getAttributes() {
176      return (attributes == null) ? Collections.EMPTY_MAP : attributes;
177    }
178
179    /**
180     * Get the algorithm from the cipher.
181     * @return the algorithm name
182     */
183    public String getAlgorithm() {
184      int slash = cipher.indexOf('/');
185      if (slash == - 1) {
186        return cipher;
187      } else {
188        return cipher.substring(0, slash);
189      }
190    }
191
192    public int getBitLength() {
193      return bitLength;
194    }
195
196    public int getVersions() {
197      return versions;
198    }
199
200    protected int addVersion() {
201      return versions++;
202    }
203
204    /**
205     * Serialize the metadata to a set of bytes.
206     * @return the serialized bytes
207     * @throws IOException
208     */
209    protected byte[] serialize() throws IOException {
210      ByteArrayOutputStream buffer = new ByteArrayOutputStream();
211      JsonWriter writer = new JsonWriter(
212          new OutputStreamWriter(buffer, StandardCharsets.UTF_8));
213      try {
214        writer.beginObject();
215        if (cipher != null) {
216          writer.name(CIPHER_FIELD).value(cipher);
217        }
218        if (bitLength != 0) {
219          writer.name(BIT_LENGTH_FIELD).value(bitLength);
220        }
221        if (created != null) {
222          writer.name(CREATED_FIELD).value(created.getTime());
223        }
224        if (description != null) {
225          writer.name(DESCRIPTION_FIELD).value(description);
226        }
227        if (attributes != null && attributes.size() > 0) {
228          writer.name(ATTRIBUTES_FIELD).beginObject();
229          for (Map.Entry<String, String> attribute : attributes.entrySet()) {
230            writer.name(attribute.getKey()).value(attribute.getValue());
231          }
232          writer.endObject();
233        }
234        writer.name(VERSIONS_FIELD).value(versions);
235        writer.endObject();
236        writer.flush();
237      } finally {
238        writer.close();
239      }
240      return buffer.toByteArray();
241    }
242
243    /**
244     * Deserialize a new metadata object from a set of bytes.
245     * @param bytes the serialized metadata
246     * @throws IOException
247     */
248    protected Metadata(byte[] bytes) throws IOException {
249      String cipher = null;
250      int bitLength = 0;
251      Date created = null;
252      int versions = 0;
253      String description = null;
254      Map<String, String> attributes = null;
255      JsonReader reader =
256          new JsonReader(new InputStreamReader(new ByteArrayInputStream(bytes),
257              StandardCharsets.UTF_8));
258      try {
259        reader.beginObject();
260        while (reader.hasNext()) {
261          String field = reader.nextName();
262          if (CIPHER_FIELD.equals(field)) {
263            cipher = reader.nextString();
264          } else if (BIT_LENGTH_FIELD.equals(field)) {
265            bitLength = reader.nextInt();
266          } else if (CREATED_FIELD.equals(field)) {
267            created = new Date(reader.nextLong());
268          } else if (VERSIONS_FIELD.equals(field)) {
269            versions = reader.nextInt();
270          } else if (DESCRIPTION_FIELD.equals(field)) {
271            description = reader.nextString();
272          } else if (ATTRIBUTES_FIELD.equalsIgnoreCase(field)) {
273            reader.beginObject();
274            attributes = new HashMap<String, String>();
275            while (reader.hasNext()) {
276              attributes.put(reader.nextName(), reader.nextString());
277            }
278            reader.endObject();
279          }
280        }
281        reader.endObject();
282      } finally {
283        reader.close();
284      }
285      this.cipher = cipher;
286      this.bitLength = bitLength;
287      this.created = created;
288      this.description = description;
289      this.attributes = attributes;
290      this.versions = versions;
291    }
292  }
293
294  /**
295   * Options when creating key objects.
296   */
297  public static class Options {
298    private String cipher;
299    private int bitLength;
300    private String description;
301    private Map<String, String> attributes;
302
303    public Options(Configuration conf) {
304      cipher = conf.get(DEFAULT_CIPHER_NAME, DEFAULT_CIPHER);
305      bitLength = conf.getInt(DEFAULT_BITLENGTH_NAME, DEFAULT_BITLENGTH);
306    }
307
308    public Options setCipher(String cipher) {
309      this.cipher = cipher;
310      return this;
311    }
312
313    public Options setBitLength(int bitLength) {
314      this.bitLength = bitLength;
315      return this;
316    }
317
318    public Options setDescription(String description) {
319      this.description = description;
320      return this;
321    }
322
323    public Options setAttributes(Map<String, String> attributes) {
324      if (attributes != null) {
325        if (attributes.containsKey(null)) {
326          throw new IllegalArgumentException("attributes cannot have a NULL key");
327        }
328        this.attributes = new HashMap<String, String>(attributes);
329      }
330      return this;
331    }
332
333    public String getCipher() {
334      return cipher;
335    }
336
337    public int getBitLength() {
338      return bitLength;
339    }
340
341    public String getDescription() {
342      return description;
343    }
344
345    @SuppressWarnings("unchecked")
346    public Map<String, String> getAttributes() {
347      return (attributes == null) ? Collections.EMPTY_MAP : attributes;
348    }
349
350    @Override
351    public String toString() {
352      return "Options{" +
353          "cipher='" + cipher + '\'' +
354          ", bitLength=" + bitLength +
355          ", description='" + description + '\'' +
356          ", attributes=" + attributes +
357          '}';
358    }
359  }
360
361  /**
362   * Constructor.
363   * 
364   * @param conf configuration for the provider
365   */
366  public KeyProvider(Configuration conf) {
367    this.conf = new Configuration(conf);
368  }
369
370  /**
371   * Return the provider configuration.
372   * 
373   * @return the provider configuration
374   */
375  public Configuration getConf() {
376    return conf;
377  }
378  
379  /**
380   * A helper function to create an options object.
381   * @param conf the configuration to use
382   * @return a new options object
383   */
384  public static Options options(Configuration conf) {
385    return new Options(conf);
386  }
387
388  /**
389   * Indicates whether this provider represents a store
390   * that is intended for transient use - such as the UserProvider
391   * is. These providers are generally used to provide access to
392   * keying material rather than for long term storage.
393   * @return true if transient, false otherwise
394   */
395  public boolean isTransient() {
396    return false;
397  }
398
399  /**
400   * Get the key material for a specific version of the key. This method is used
401   * when decrypting data.
402   * @param versionName the name of a specific version of the key
403   * @return the key material
404   * @throws IOException
405   */
406  public abstract KeyVersion getKeyVersion(String versionName
407                                            ) throws IOException;
408
409  /**
410   * Get the key names for all keys.
411   * @return the list of key names
412   * @throws IOException
413   */
414  public abstract List<String> getKeys() throws IOException;
415
416  /**
417   * Get key metadata in bulk.
418   * @param names the names of the keys to get
419   * @throws IOException
420   */
421  public Metadata[] getKeysMetadata(String... names) throws IOException {
422    Metadata[] result = new Metadata[names.length];
423    for (int i=0; i < names.length; ++i) {
424      result[i] = getMetadata(names[i]);
425    }
426    return result;
427  }
428
429  /**
430   * Get the key material for all versions of a specific key name.
431   * @return the list of key material
432   * @throws IOException
433   */
434  public abstract List<KeyVersion> getKeyVersions(String name) throws IOException;
435
436  /**
437   * Get the current version of the key, which should be used for encrypting new
438   * data.
439   * @param name the base name of the key
440   * @return the version name of the current version of the key or null if the
441   *    key version doesn't exist
442   * @throws IOException
443   */
444  public KeyVersion getCurrentKey(String name) throws IOException {
445    Metadata meta = getMetadata(name);
446    if (meta == null) {
447      return null;
448    }
449    return getKeyVersion(buildVersionName(name, meta.getVersions() - 1));
450  }
451
452  /**
453   * Get metadata about the key.
454   * @param name the basename of the key
455   * @return the key's metadata or null if the key doesn't exist
456   * @throws IOException
457   */
458  public abstract Metadata getMetadata(String name) throws IOException;
459
460  /**
461   * Create a new key. The given key must not already exist.
462   * @param name the base name of the key
463   * @param material the key material for the first version of the key.
464   * @param options the options for the new key.
465   * @return the version name of the first version of the key.
466   * @throws IOException
467   */
468  public abstract KeyVersion createKey(String name, byte[] material,
469                                       Options options) throws IOException;
470
471  /**
472   * Get the algorithm from the cipher.
473   *
474   * @return the algorithm name
475   */
476  private String getAlgorithm(String cipher) {
477    int slash = cipher.indexOf('/');
478    if (slash == -1) {
479      return cipher;
480    } else {
481      return cipher.substring(0, slash);
482    }
483  }
484
485  /**
486   * Generates a key material.
487   *
488   * @param size length of the key.
489   * @param algorithm algorithm to use for generating the key.
490   * @return the generated key.
491   * @throws NoSuchAlgorithmException
492   */
493  protected byte[] generateKey(int size, String algorithm)
494      throws NoSuchAlgorithmException {
495    algorithm = getAlgorithm(algorithm);
496    KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
497    keyGenerator.init(size);
498    byte[] key = keyGenerator.generateKey().getEncoded();
499    return key;
500  }
501
502  /**
503   * Create a new key generating the material for it.
504   * The given key must not already exist.
505   * <p/>
506   * This implementation generates the key material and calls the
507   * {@link #createKey(String, byte[], Options)} method.
508   *
509   * @param name the base name of the key
510   * @param options the options for the new key.
511   * @return the version name of the first version of the key.
512   * @throws IOException
513   * @throws NoSuchAlgorithmException
514   */
515  public KeyVersion createKey(String name, Options options)
516      throws NoSuchAlgorithmException, IOException {
517    byte[] material = generateKey(options.getBitLength(), options.getCipher());
518    return createKey(name, material, options);
519  }
520
521  /**
522   * Delete the given key.
523   * @param name the name of the key to delete
524   * @throws IOException
525   */
526  public abstract void deleteKey(String name) throws IOException;
527
528  /**
529   * Roll a new version of the given key.
530   * @param name the basename of the key
531   * @param material the new key material
532   * @return the name of the new version of the key
533   * @throws IOException
534   */
535  public abstract KeyVersion rollNewVersion(String name,
536                                             byte[] material
537                                            ) throws IOException;
538
539  /**
540   * Can be used by implementing classes to close any resources
541   * that require closing
542   */
543  public void close() throws IOException {
544    // NOP
545  }
546
547  /**
548   * Roll a new version of the given key generating the material for it.
549   * <p/>
550   * This implementation generates the key material and calls the
551   * {@link #rollNewVersion(String, byte[])} method.
552   *
553   * @param name the basename of the key
554   * @return the name of the new version of the key
555   * @throws IOException
556   */
557  public KeyVersion rollNewVersion(String name) throws NoSuchAlgorithmException,
558                                                       IOException {
559    Metadata meta = getMetadata(name);
560    byte[] material = generateKey(meta.getBitLength(), meta.getCipher());
561    return rollNewVersion(name, material);
562  }
563
564  /**
565   * Ensures that any changes to the keys are written to persistent store.
566   * @throws IOException
567   */
568  public abstract void flush() throws IOException;
569
570  /**
571   * Split the versionName in to a base name. Converts "/aaa/bbb/3" to
572   * "/aaa/bbb".
573   * @param versionName the version name to split
574   * @return the base name of the key
575   * @throws IOException
576   */
577  public static String getBaseName(String versionName) throws IOException {
578    int div = versionName.lastIndexOf('@');
579    if (div == -1) {
580      throw new IOException("No version in key path " + versionName);
581    }
582    return versionName.substring(0, div);
583  }
584
585  /**
586   * Build a version string from a basename and version number. Converts
587   * "/aaa/bbb" and 3 to "/aaa/bbb@3".
588   * @param name the basename of the key
589   * @param version the version of the key
590   * @return the versionName of the key.
591   */
592  protected static String buildVersionName(String name, int version) {
593    return name + "@" + version;
594  }
595
596  /**
597   * Find the provider with the given key.
598   * @param providerList the list of providers
599   * @param keyName the key name we are looking for
600   * @return the KeyProvider that has the key
601   */
602  public static KeyProvider findProvider(List<KeyProvider> providerList,
603                                         String keyName) throws IOException {
604    for(KeyProvider provider: providerList) {
605      if (provider.getMetadata(keyName) != null) {
606        return provider;
607      }
608    }
609    throw new IOException("Can't find KeyProvider for key " + keyName);
610  }
611
612  /**
613   * Does this provider require a password? This means that a password is
614   * required for normal operation, and it has not been found through normal
615   * means. If true, the password should be provided by the caller using
616   * setPassword().
617   * @return Whether or not the provider requires a password
618   * @throws IOException
619   */
620  public boolean needsPassword() throws IOException {
621    return false;
622  }
623
624  /**
625   * If a password for the provider is needed, but is not provided, this will
626   * return a warning and instructions for supplying said password to the
627   * provider.
628   * @return A warning and instructions for supplying the password
629   */
630  public String noPasswordWarning() {
631    return null;
632  }
633
634  /**
635   * If a password for the provider is needed, but is not provided, this will
636   * return an error message and instructions for supplying said password to
637   * the provider.
638   * @return An error message and instructions for supplying the password
639   */
640  public String noPasswordError() {
641    return null;
642  }
643}