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