/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.security; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Enumeration; import java.util.logging.Level; import java.util.logging.Logger; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import org.geoserver.platform.resource.Resource; import org.geoserver.platform.resource.Resource.Type; import org.geoserver.security.password.RandomPasswordProvider; import org.geotools.util.logging.Logging; import org.springframework.beans.factory.BeanNameAware; import static org.geoserver.security.SecurityUtils.toBytes; /** * Class for Geoserver specific key management * * <strong>requires a master password</strong> form * {@link MasterPasswordProviderImpl} * * The type of the keystore is JCEKS and can be used/modified * with java tools like "keytool" from the command line. * * * * @author christian * */ public class KeyStoreProviderImpl implements BeanNameAware, KeyStoreProvider{ public final static String DEFAULT_BEAN_NAME="DefaultKeyStoreProvider"; public final static String DEFAULT_FILE_NAME="geoserver.jceks"; public final static String PREPARED_FILE_NAME="geoserver.jceks.new"; public final static String CONFIGPASSWORDKEY = "config:password:key"; public final static String URLPARAMKEY = "url:param:key"; public final static String USERGROUP_PREFIX = "ug:"; public final static String USERGROUP_POSTFIX = ":key"; static protected Logger LOGGER = Logging.getLogger("org.geoserver.security"); protected String name; protected Resource keyStoreResource; protected KeyStore ks; public final static String KEYSTORETYPE = "JCEKS"; GeoServerSecurityManager securityManager; public KeyStoreProviderImpl() { } @Override public void setBeanName(String name) { this.name = name; } public String getName() { return name; } public void setSecurityManager(GeoServerSecurityManager securityManager) { this.securityManager = securityManager; } public GeoServerSecurityManager getSecurityManager() { return securityManager; } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#getKeyStoreProvderFile() */ @Override public Resource getResource() { if (keyStoreResource == null) { keyStoreResource = securityManager.security().get(DEFAULT_FILE_NAME); } return keyStoreResource; } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#reloadKeyStore() */ @Override public void reloadKeyStore() throws IOException{ ks=null; assertActivatedKeyStore(); } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#getKey(java.lang.String) */ @Override public Key getKey(String alias) throws IOException{ assertActivatedKeyStore(); try { char[] passwd = securityManager.getMasterPassword(); try { return ks.getKey(alias, passwd); } finally { securityManager.disposePassword(passwd); } } catch (Exception e) { throw new IOException(e); } } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#getConfigPasswordKey() */ @Override public byte[] getConfigPasswordKey() throws IOException{ SecretKey key = getSecretKey(CONFIGPASSWORDKEY); if (key==null) return null; return key.getEncoded(); } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#hasConfigPasswordKey() */ @Override public boolean hasConfigPasswordKey() throws IOException { return containsAlias(CONFIGPASSWORDKEY); } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#containsAlias(java.lang.String) */ @Override public boolean containsAlias(String alias) throws IOException{ assertActivatedKeyStore(); try { return ks.containsAlias(alias); } catch (KeyStoreException e) { throw new IOException(e); } } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#getUserGRoupKey(java.lang.String) */ @Override public byte[] getUserGroupKey(String serviceName) throws IOException{ SecretKey key = getSecretKey(aliasForGroupService(serviceName)); if (key==null) return null; return key.getEncoded(); } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#hasUserGRoupKey(java.lang.String) */ @Override public boolean hasUserGroupKey(String serviceName) throws IOException { return containsAlias(aliasForGroupService(serviceName)); } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#getSecretKey(java.lang.String) */ @Override public SecretKey getSecretKey(String name) throws IOException{ Key key = getKey(name); if (key==null) return null; if ((key instanceof SecretKey) == false) throw new IOException("Invalid key type for: "+name); return (SecretKey) key; } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#getPublicKey(java.lang.String) */ @Override public PublicKey getPublicKey(String name) throws IOException{ Key key = getKey(name); if (key==null) return null; if ((key instanceof PublicKey) == false) throw new IOException("Invalid key type for: "+name); return (PublicKey) key; } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#getPrivateKey(java.lang.String) */ @Override public PrivateKey getPrivateKey(String name) throws IOException{ Key key = getKey(name); if (key==null) return null; if ((key instanceof PrivateKey) == false) throw new IOException("Invalid key type for: "+name); return (PrivateKey) key; } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#aliasForGroupService(java.lang.String) */ @Override public String aliasForGroupService(String serviceName) { StringBuffer buff = new StringBuffer(USERGROUP_PREFIX); buff.append(serviceName); buff.append(USERGROUP_POSTFIX); return buff.toString(); } /** * Opens or creates a {@link KeyStore} using the file * {@link #DEFAULT_FILE_NAME} * * Throws an exception for an invalid master key * * @throws IOException */ protected void assertActivatedKeyStore() throws IOException { if (ks != null) return; char[] passwd = securityManager.getMasterPassword(); try { ks = KeyStore.getInstance(KEYSTORETYPE); if (getResource().getType() == Type.UNDEFINED) { // create an empy one ks.load(null, passwd); addInitialKeys(); try (OutputStream fos = getResource().out()) { ks.store(fos, passwd); } } else { try (InputStream fis = getResource().in()) { ks.load(fis, passwd); } } } catch (Exception ex) { if (ex instanceof IOException) // avoid useless wrapping throw (IOException) ex; throw new IOException (ex); } finally { securityManager.disposePassword(passwd); } } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#isKeystorePassword(java.lang.String) */ @Override public boolean isKeyStorePassword(char[] password) throws IOException{ if (password==null) return false; assertActivatedKeyStore(); KeyStore testStore=null; try { testStore = KeyStore.getInstance(KEYSTORETYPE); } catch (KeyStoreException e1) { // should not happen, see assertActivatedKeyStore throw new RuntimeException(e1); } try (InputStream fis = getResource().in()) { testStore.load(fis, password); } catch (IOException e2) { // indicates invalid password return false; } catch (Exception e) { // should not happen, see assertActivatedKeyStore throw new RuntimeException(e); } return true; } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#setSecretKey(java.lang.String, java.lang.String) */ @Override public void setSecretKey(String alias, char[] key) throws IOException { assertActivatedKeyStore(); SecretKey mySecretKey=new SecretKeySpec(toBytes(key),"PBE"); KeyStore.SecretKeyEntry skEntry = new KeyStore.SecretKeyEntry(mySecretKey); char[] passwd = securityManager.getMasterPassword(); try { ks.setEntry(alias, skEntry, new KeyStore.PasswordProtection(passwd)); } catch (KeyStoreException e) { throw new IOException(e); } finally { securityManager.disposePassword(passwd); } } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#setUserGroupKey(java.lang.String, java.lang.String) */ @Override public void setUserGroupKey(String serviceName,char[] password) throws IOException{ String alias = aliasForGroupService(serviceName); setSecretKey(alias, password); } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#removeKey(java.lang.String) */ @Override public void removeKey(String alias ) throws IOException { assertActivatedKeyStore(); try { ks.deleteEntry(alias); } catch (KeyStoreException e) { throw new IOException(e); } } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#storeKeyStore() */ @Override public void storeKeyStore() throws IOException{ // store away the keystore assertActivatedKeyStore(); try (OutputStream fos = getResource().out()) { char[] passwd = securityManager.getMasterPassword(); try { ks.store(fos, passwd); } catch (Exception e) { throw new IOException(e); } finally { securityManager.disposePassword(passwd); } } } /** * Creates initial key entries * auto generated keys * {@link #CONFIGPASSWORDKEY} * * @throws IOException */ protected void addInitialKeys() throws IOException { //TODO:scramble RandomPasswordProvider randPasswdProvider = getSecurityManager().getRandomPassworddProvider(); char[] configKey = randPasswdProvider.getRandomPasswordWithDefaultLength(); setSecretKey( CONFIGPASSWORDKEY, configKey); } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#prepareForMasterPasswordChange(java.lang.String, java.lang.String) */ @Override public void prepareForMasterPasswordChange(char[] oldPassword, char[] newPassword) throws IOException{ Resource dir = getResource().parent(); Resource newKSFile = dir.get(PREPARED_FILE_NAME); if (newKSFile.getType() != Type.UNDEFINED) { newKSFile.delete(); } try { KeyStore oldKS=KeyStore.getInstance(KEYSTORETYPE); try (InputStream fin = getResource().in()) { oldKS.load(fin, oldPassword); } KeyStore newKS = KeyStore.getInstance(KEYSTORETYPE); newKS.load(null, newPassword); KeyStore.PasswordProtection protectionparam = new KeyStore.PasswordProtection(newPassword); Enumeration<String> enumeration = oldKS.aliases(); while (enumeration.hasMoreElements()) { String alias =enumeration.nextElement(); Key key = oldKS.getKey(alias, oldPassword); KeyStore.Entry entry =null; if (key instanceof SecretKey) entry = new KeyStore.SecretKeyEntry((SecretKey)key); if (key instanceof PrivateKey) entry = new KeyStore.PrivateKeyEntry((PrivateKey)key, oldKS.getCertificateChain(alias)); if (key instanceof PublicKey) entry = new KeyStore.TrustedCertificateEntry(oldKS.getCertificate(alias)); if (entry == null) LOGGER.warning("Unknown key in store, alias: "+alias+ " class: "+ key.getClass().getName()); else newKS.setEntry(alias, entry, protectionparam); } try (OutputStream fos = newKSFile.out()) { newKS.store(fos, newPassword); } } catch (Exception ex) { throw new IOException(ex); } } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#abortMasterPasswordChange() */ @Override public void abortMasterPasswordChange() { Resource dir = getResource().parent(); Resource newKSFile = dir.get(PREPARED_FILE_NAME); if (newKSFile.getType() != Type.UNDEFINED) { //newKSFile.delete(); } } /* (non-Javadoc) * @see org.geoserver.security.password.KeystoreProvider#commitMasterPasswordChange() */ @Override public void commitMasterPasswordChange() throws IOException { Resource dir = getResource().parent(); Resource newKSFile = dir.get(PREPARED_FILE_NAME); Resource oldKSFile = dir.get(DEFAULT_FILE_NAME); if (newKSFile.getType() == Type.UNDEFINED) { return; //nothing to do } if (oldKSFile.getType() == Type.UNDEFINED) { return; //not initialized } // Try to open with new password InputStream fin = newKSFile.in(); char[] passwd = securityManager.getMasterPassword(); try { KeyStore newKS = KeyStore.getInstance(KEYSTORETYPE); newKS.load(fin, passwd); // to be sure, decrypt all keys Enumeration<String> enumeration = newKS.aliases(); while (enumeration.hasMoreElements()) { newKS.getKey(enumeration.nextElement(), passwd); } fin.close(); fin=null; if (oldKSFile.delete()==false) { LOGGER.severe("cannot delete " + oldKSFile.path()); return; } if (newKSFile.renameTo(oldKSFile)==false) { String msg = "cannot rename "+ newKSFile.path(); msg += "to " + oldKSFile.path(); msg += "Try to rename manually and restart"; LOGGER.severe(msg); return; } reloadKeyStore(); LOGGER.info("Successfully changed master password"); } catch (IOException e) { String msg = "Error creating new keystore: " + newKSFile.path(); LOGGER.log(Level.WARNING, msg, e); throw e; } catch (Exception ex) { throw new RuntimeException(ex); } finally { securityManager.disposePassword(passwd); if (fin != null) { try{ fin.close(); } catch (IOException ex) { // give up } } } } }