001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with this
004 * work for additional information regarding copyright ownership. The ASF
005 * licenses this file to you under the Apache License, Version 2.0 (the
006 * "License"); you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations under
015 * the License.
016 */
017package org.apache.hadoop.hbase.io.crypto;
018
019import java.io.BufferedInputStream;
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.net.URI;
025import java.net.URISyntaxException;
026import java.net.URLDecoder;
027import java.security.Key;
028import java.security.KeyStore;
029import java.security.KeyStoreException;
030import java.security.NoSuchAlgorithmException;
031import java.security.UnrecoverableKeyException;
032import java.security.cert.CertificateException;
033import java.util.Locale;
034import java.util.Properties;
035
036import org.apache.yetus.audience.InterfaceAudience;
037
038/**
039 * A basic KeyProvider that can resolve keys from a protected KeyStore file
040 * on the local filesystem. It is configured with a URI passed in as a String
041 * to init(). The URI should have the form:
042 * <p>
043 * <pre>    scheme://path?option1=value1&amp;option2=value2</pre>
044 * <p>
045 * <i>scheme</i> can be either "jks" or "jceks", specifying the file based
046 * providers shipped with every JRE. The latter is the certificate store for
047 * the SunJCE cryptography extension, or PKCS #12, and is capable of storing
048 * SecretKeys.
049 * <p>
050 * <i>path</i> is the location of the keystore in the filesystem namespace.
051 * <p>
052 * Options can be specified as query parameters.
053 * <p>
054 * If the store was created with a password, the password can be specified
055 * using the option 'password'.
056 * <p>
057 * For example:
058 * <p>
059 * <pre>    jceks:///var/tmp/example.ks?password=foobar</pre>
060 * <p>
061 * It is assumed that all keys in the store are protected with the same
062 * password.
063 * <p>
064 * Alternatively, a properties file can be specified containing passwords for
065 * keys in the keystore.
066 * <pre>    jceks:///var/tmp/example.ks?passwordFile=/var/tmp/example.pw</pre>
067 * <p>
068 * Subclasses for supporting KeyStores that are not file based can extend the
069 * protected methods of this class to specify the appropriate
070 * LoadStoreParameters.
071 */
072@InterfaceAudience.Public
073public class KeyStoreKeyProvider implements KeyProvider {
074
075  protected KeyStore store;
076  protected char[] password;         // can be null if no password
077  protected Properties passwordFile; // can be null if no file provided
078
079  protected void processParameter(String name, String value) throws IOException {
080    if (name.equalsIgnoreCase(KeyProvider.PASSWORD)) {
081      password = value.toCharArray();
082    }
083    if (name.equalsIgnoreCase(KeyProvider.PASSWORDFILE)) {
084      Properties p = new Properties();
085      InputStream in = new BufferedInputStream(new FileInputStream(new File(value)));
086      try {
087        p.load(in);
088        passwordFile = p;
089      } finally {
090        in.close();
091      }
092    }
093  }
094
095  protected void processParameters(URI uri) throws IOException {
096    String params = uri.getQuery();
097    if (params == null || params.isEmpty()) {
098      return;
099    }
100    do {
101      int nameStart = 0;
102      int nameEnd = params.indexOf('=');
103      if (nameEnd == -1) {
104        throw new RuntimeException("Invalid parameters: '" + params + "'");
105      }
106      int valueStart = nameEnd + 1;
107      int valueEnd = params.indexOf('&');
108      if (valueEnd == -1) {
109        valueEnd = params.length();
110      }
111      String name = URLDecoder.decode(params.substring(nameStart, nameEnd), "UTF-8");
112      String value = URLDecoder.decode(params.substring(valueStart, valueEnd), "UTF-8");
113      processParameter(name, value);
114      params = params.substring(valueEnd, params.length());
115    } while (!params.isEmpty());
116  }
117
118  protected void load(URI uri) throws IOException {
119    String path = uri.getPath();
120    if (path == null || path.isEmpty()) {
121      throw new RuntimeException("KeyProvider parameters should specify a path");
122    }
123    InputStream is = new FileInputStream(new File(path));
124    try {
125      store.load(is, password);
126    } catch (NoSuchAlgorithmException e) {
127      throw new RuntimeException(e);
128    } catch (CertificateException e) {
129      throw new RuntimeException(e);
130    } finally {
131      is.close();
132    }
133  }
134
135  @Override
136  public void init(String params) {
137    try {
138      URI uri = new URI(params);
139      String storeType = uri.getScheme();
140      if (storeType == null || storeType.isEmpty()) {
141        throw new RuntimeException("KeyProvider scheme should specify KeyStore type");
142      }
143      // KeyStore expects instance type specifications in uppercase
144      store = KeyStore.getInstance(storeType.toUpperCase(Locale.ROOT));
145      processParameters(uri);
146      load(uri);
147    } catch (URISyntaxException e) {
148      throw new RuntimeException(e);
149    } catch (KeyStoreException e) {
150      throw new RuntimeException(e);
151    } catch (IOException e) {
152      throw new RuntimeException(e);
153    }
154  }
155
156  protected char[] getAliasPassword(String alias) {
157    if (password != null) {
158      return password;
159    }
160    if (passwordFile != null) {
161      String p = passwordFile.getProperty(alias);
162      if (p != null) {
163        return p.toCharArray();
164      }
165    }
166    return null;
167  }
168
169  @Override
170  public Key getKey(String alias) {
171    try {
172      return store.getKey(alias, getAliasPassword(alias));
173    } catch (UnrecoverableKeyException e) {
174      throw new RuntimeException(e);
175    } catch (KeyStoreException e) {
176      throw new RuntimeException(e);
177    } catch (NoSuchAlgorithmException e) {
178      throw new RuntimeException(e);
179    }
180  }
181
182  @Override
183  public Key[] getKeys(String[] aliases) {
184    Key[] result = new Key[aliases.length];
185    for (int i = 0; i < aliases.length; i++) {
186      result[i] = getKey(aliases[i]);
187    }
188    return result;
189  }
190
191}