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