package it.geosolutions.geostore.core.security.password;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
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 javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.BeanNameAware;
import static it.geosolutions.geostore.core.security.password.SecurityUtils.toBytes;
/**
* Class for GeoStore specific key management
*
* <strong>requires a password</strong> form configuration
*
* The type of the keystore is JCEKS and can be used/modified
* with java tools like "keytool" from the command line.
* *
*
* @author Lorenzo Natali
*
*/
public class KeyStoreProviderImpl implements BeanNameAware, KeyStoreProvider{
private static final Logger LOGGER = Logger.getLogger(KeyStoreProviderImpl.class);
public final static String DEFAULT_BEAN_NAME="DefaultKeyStoreProvider";
public final static String DEFAULT_FILE_NAME="geostore.jceks";
public final static String PREPARED_FILE_NAME="geostore.jceks.new";
public final static String CONFIGPASSWORDKEY = "ug:geostore:key";
public final static String USERGROUP_PREFIX = "ug:";
public final static String USERGROUP_POSTFIX = ":key";
private String keyStoreFilePath = null;
protected String name;
protected File keyStoreFile;
protected KeyStore ks;
private char[] masterPassword;
private String keyName;
private MasterPasswordProvider masterPasswordProvider;
public MasterPasswordProvider getMasterPasswordProvider() {
return masterPasswordProvider;
}
public void setMasterPasswordProvider(
MasterPasswordProvider masterPasswordProvider) {
this.masterPasswordProvider = masterPasswordProvider;
}
public String getKeyName() {
return keyName;
}
public void setKeyName(String keyName) {
this.keyName = keyName;
}
public void setMasterPassword(char[] masterPassword) {
this.masterPassword = masterPassword;
}
public final static String KEYSTORETYPE = "JCEKS";
public KeyStoreProviderImpl() {
}
@Override
public void setBeanName(String name) {
this.name = name;
}
/* (non-Javadoc)
* @see org.geoserver.security.password.KeystoreProvider#getKeyStoreProvderFile()
*/
@Override
public File getFile() {
//retrieve the file on first access
if (keyStoreFile == null) {
//if the keyStoreFilePath is configured create it
if(getKeyStoreFilePath()!=null){
keyStoreFile = new File(getKeyStoreFilePath());
if(keyStoreFile!=null){
if(!keyStoreFile.exists()){
if(keyStoreFile.isDirectory()){
keyStoreFile = new File(getKeyStoreFilePath()+ "geostore.jceks");
}
LOGGER.warn("the keyStore file doesn't exist. confiure a new one");
}
}
//if the file doesn't exist create a new one
//TODO add a key
//otherwise get the default one
}else{
URL defaultKeyStrore = KeyStoreProviderImpl.class.getClassLoader().getResource("geostore.jceks");
try {
if(defaultKeyStrore!= null){
keyStoreFile = new File(defaultKeyStrore.toURI());
}
} catch (URISyntaxException e) {
LOGGER.error("UNABLE TO GET THE DEFAULT KEY STORE");
}
}
}
return keyStoreFile;
}
public String getKeyStoreFilePath() {
return keyStoreFilePath;
}
public void setKeyStoreFilePath(String keyStoreFilePath ){
this.keyStoreFilePath = keyStoreFilePath;
}
/* (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 = getMasterPassword();
try {
return ks.getKey(alias, passwd);
}
finally {
disposePassword(passwd);
}
} catch (Exception e) {
throw new IOException(e);
}
}
private char[] getMasterPassword() {
//TODO cifrate this password somehow
if( masterPassword !=null){
return masterPassword;
}else{
if(masterPasswordProvider != null){
try {
masterPassword = masterPasswordProvider.doGetMasterPassword();
} catch (Exception e) {
LOGGER.error("unable to read the master password\n:" + e.getStackTrace());
}
}
}
return masterPassword;
}
public Enumeration<String> aliases(){
if(ks!=null)
try {
return ks.aliases();
} catch (KeyStoreException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
return null;
}
/* (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 = getMasterPassword();
try {
ks = KeyStore.getInstance(KEYSTORETYPE);
if (getFile().exists()==false) { // create an empy one
ks.load(null, passwd);
addInitialKeys();
FileOutputStream fos = new FileOutputStream(getFile());
ks.store(fos, passwd);
fos.close();
} else {
FileInputStream fis =
new FileInputStream(getFile());
ks.load(fis, passwd);
fis.close();
}
} catch (Exception ex) {
if (ex instanceof IOException) // avoid useless wrapping
throw (IOException) ex;
throw new IOException (ex);
}
finally {
disposePassword(passwd);
}
}
private void disposePassword(char[] passwd) {
// TODO implement it when security improved.
}
/* (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);
}
FileInputStream fis =
new FileInputStream(getFile());
try {
testStore.load(fis, password);
} catch (IOException e2) {
// indicates invalid password
return false;
} catch (Exception e) {
// should not happen, see assertActivatedKeyStore
throw new RuntimeException(e);
}
fis.close();
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 = getMasterPassword();
try {
ks.setEntry(alias, skEntry, new KeyStore.PasswordProtection(passwd));
} catch (KeyStoreException e) {
throw new IOException(e);
}
finally {
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();
FileOutputStream fos = new FileOutputStream(getFile());
char[] passwd = getMasterPassword();
try {
ks.store(fos, passwd);
} catch (Exception e) {
throw new IOException(e);
}
finally {
disposePassword(passwd);
}
fos.close();
}
/**
* Creates initial key entries
* auto generated keys
* {@link #CONFIGPASSWORDKEY}
*
* @throws IOException
*/
protected void addInitialKeys() throws IOException {
RandomPasswordProvider randPasswdProvider =
getRandomPassworddProvider();
char[] configKey = randPasswdProvider.getRandomPasswordWithDefaultLength();
setSecretKey( CONFIGPASSWORDKEY, configKey);
}
private RandomPasswordProvider getRandomPassworddProvider() {
return new RandomPasswordProvider();
}
/* (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{
File dir = getFile().getParentFile();
File newKSFile = new File(dir,PREPARED_FILE_NAME);
if (newKSFile.exists())
newKSFile.delete();
try {
KeyStore oldKS=KeyStore.getInstance(KEYSTORETYPE);
FileInputStream fin = new FileInputStream(getFile());
oldKS.load(fin, oldPassword);
fin.close();
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.warn("Unknown key in store, alias: "+alias+
" class: "+ key.getClass().getName());
else
newKS.setEntry(alias, entry, protectionparam);
}
FileOutputStream fos = new FileOutputStream(newKSFile);
newKS.store(fos, newPassword);
fos.close();
} catch (Exception ex) {
throw new IOException(ex);
}
}
/* (non-Javadoc)
* @see org.geoserver.security.password.KeystoreProvider#abortMasterPasswordChange()
*/
@Override
public void abortMasterPasswordChange() {
File dir = getFile().getParentFile();
File newKSFile = new File(dir,PREPARED_FILE_NAME);
if (newKSFile.exists()) {
//newKSFile.delete();
}
}
/* (non-Javadoc)
* @see org.geoserver.security.password.KeystoreProvider#commitMasterPasswordChange()
*/
@Override
public void commitMasterPasswordChange() throws IOException {
File dir = getFile().getParentFile();
File newKSFile = new File(dir,PREPARED_FILE_NAME);
File oldKSFile = new File(dir,DEFAULT_FILE_NAME);
if (newKSFile.exists()==false)
return; //nothing to do
if (oldKSFile.exists()==false)
return; //not initialized
// Try to open with new password
FileInputStream fin = new FileInputStream(newKSFile);
char[] passwd = 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.error("cannot delete " +getFile().getCanonicalPath());
return;
}
if (newKSFile.renameTo(oldKSFile)==false) {
String msg = "cannot rename "+ newKSFile.getCanonicalPath();
msg += "to " + oldKSFile.getCanonicalPath();
msg += "Try to rename manually and restart";
LOGGER.error(msg);
return;
}
reloadKeyStore();
LOGGER.info("Successfully changed master password");
} catch (IOException e) {
String msg = "Error creating new keystore: " + newKSFile.getCanonicalPath();
LOGGER.warn( msg, e);
throw e;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
finally {
disposePassword(passwd);
if (fin != null) {
try{
fin.close();
}
catch (IOException ex) {
// give up
}
}
}
}
}