/* * JBoss, Home of Professional Open Source * Copyright 2015 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.wildfly.security.credential.store; import java.io.File; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.Security; import java.security.spec.InvalidKeySpecException; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.wildfly.security.WildFlyElytronProvider; import org.wildfly.security.auth.server.IdentityCredentials; import org.wildfly.security.credential.PasswordCredential; import org.wildfly.security.credential.store.impl.KeyStoreCredentialStore; import org.wildfly.security.password.PasswordFactory; import org.wildfly.security.password.interfaces.ClearPassword; import org.wildfly.security.password.spec.ClearPasswordSpec; /** * {@code KeyStoreCredentialStore} tests * * @author <a href="mailto:pskopek@redhat.com">Peter Skopek</a>, * <a href="mailto:hsvabek@redhat.com">Hynek Svabek</a>. */ public class KeystorePasswordStoreTest { private static final Provider provider = new WildFlyElytronProvider(); private static Map<String, String> stores = new HashMap<>(); private static String BASE_STORE_DIRECTORY = "target/ks-cred-stores"; static { stores.put("ONE", BASE_STORE_DIRECTORY + "/keystore1.jceks"); stores.put("TWO", BASE_STORE_DIRECTORY + "/keystore2.jceks"); stores.put("THREE", BASE_STORE_DIRECTORY + "/keystore3.jceks"); stores.put("TO_DELETE", BASE_STORE_DIRECTORY + "/keystore4.jceks"); } /** * Clean all vaults. */ public static void cleanCredentialStores() { File dir = new File(BASE_STORE_DIRECTORY); dir.mkdirs(); for (String f: stores.values()) { File file = new File(f); file.delete(); } } static CredentialStore newCredentialStoreInstance() throws NoSuchAlgorithmException { return CredentialStore.getInstance(KeyStoreCredentialStore.KEY_STORE_CREDENTIAL_STORE); } /** * Convert {@code char[]} password to {@code PasswordCredential} * @param password to convert * @return new {@code PasswordCredential} * @throws UnsupportedCredentialTypeException should never happen as we have only supported types and algorithms */ PasswordCredential createCredentialFromPassword(char[] password) throws UnsupportedCredentialTypeException { try { PasswordFactory passwordFactory = PasswordFactory.getInstance(ClearPassword.ALGORITHM_CLEAR); return new PasswordCredential(passwordFactory.generatePassword(new ClearPasswordSpec(password))); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new UnsupportedCredentialTypeException(e); } } /** * Converts {@code PasswordCredential} to {@code char[]} password * @param passwordCredential to convert * @return plain text password as {@code char[]} */ char[] getPasswordFromCredential(PasswordCredential passwordCredential) { Assert.assertNotNull("passwordCredential parameter", passwordCredential); return passwordCredential.getPassword().castAndApply(ClearPassword.class, ClearPassword::getPassword); } /** * Register security provider containing {@link org.wildfly.security.credential.store.CredentialStoreSpi} implementation. */ @BeforeClass public static void setup() throws Exception { Security.addProvider(provider); cleanCredentialStores(); // setup vaults that need to be complete before a test starts CredentialStoreBuilder.get().setKeyStoreFile(stores.get("TWO")) .setKeyStoreType("JCEKS") .setKeyStorePassword("secret_store_TWO") .addPassword("alias1", "secret-password-1") .addPassword("alias2", "secret-password-2") .build(); CredentialStoreBuilder.get().setKeyStoreFile(stores.get("THREE")) .setKeyStoreType("JCEKS") .setKeyStorePassword("secret_store_THREE") .addPassword("db-pass-1", "1-secret-info") .addPassword("db-pass-2", "2-secret-info") .addPassword("db-pass-3", "3-secret-info") .addPassword("db-pass-4", "4-secret-info") .addPassword("db-pass-5", "5-secret-info") .build(); CredentialStoreBuilder.get().setKeyStoreFile(stores.get("TO_DELETE")) .setKeyStorePassword("secret_store_DELETE") .addPassword("alias1", "secret-password-1") .addPassword("alias2", "secret-password-2") .build(); } /** * Remove security provider. */ @AfterClass public static void remove() { Security.removeProvider(provider.getName()); } /** * After initialize Credential Store is removed backend CS file. This file must be created again when there is added new * entry to store. * * @throws NoSuchAlgorithmException * @throws CredentialStoreException * @throws UnsupportedCredentialTypeException */ @Test public void testRecreatingKSTest() throws NoSuchAlgorithmException, CredentialStoreException, UnsupportedCredentialTypeException { File ks = new File(stores.get("TO_DELETE")); if (!ks.exists()) { Assert.fail("KeyStore must exists!"); } char[] password1 = "secret-password1".toCharArray(); char[] password2 = "secret-password2".toCharArray(); HashMap<String, String> csAttributes = new HashMap<>(); csAttributes.put("location", stores.get("TO_DELETE")); csAttributes.put("keyStoreType", "JCEKS"); String passwordAlias1 = "passAlias1"; String passwordAlias2 = "passAlias2"; CredentialStore cs = newCredentialStoreInstance(); cs.initialize(csAttributes, new CredentialStore.CredentialSourceProtectionParameter( IdentityCredentials.NONE.withCredential(new PasswordCredential( ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, "secret_store_DELETE".toCharArray()))))); cs.store(passwordAlias1, createCredentialFromPassword(password1)); cs.store(passwordAlias2, createCredentialFromPassword(password2)); Assert.assertArrayEquals(password1, getPasswordFromCredential(cs.retrieve(passwordAlias1, PasswordCredential.class))); if (!ks.delete()) { Assert.fail("KeyStore [" + ks.getAbsolutePath() + "] delete fail"); } Assert.assertArrayEquals(password1, getPasswordFromCredential(cs.retrieve(passwordAlias1, PasswordCredential.class))); // load new entry (in memory) Assert.assertArrayEquals(password2, getPasswordFromCredential(cs.retrieve(passwordAlias2, PasswordCredential.class))); cs.store("abc", createCredentialFromPassword(password1)); cs.flush(); if (!ks.exists()) { Assert.fail("KeyStore [" + ks.getAbsolutePath() + "] must exist yet."); } } /** * Credential Store is set to read-only. * * @throws NoSuchAlgorithmException * @throws CredentialStoreException * @throws UnsupportedCredentialTypeException */ @Test public void testReadOnly() throws NoSuchAlgorithmException, CredentialStoreException, UnsupportedCredentialTypeException { char[] password1 = "secret-password1".toCharArray(); HashMap<String, String> csAttributes = new HashMap<>(); csAttributes.put("location", stores.get("TWO")); csAttributes.put("keyStoreType", "JCEKS"); csAttributes.put("modifiable", "false"); String passwordAlias1 = "passAlias_readonly"; CredentialStore cs = newCredentialStoreInstance(); cs.initialize(csAttributes, new CredentialStore.CredentialSourceProtectionParameter( IdentityCredentials.NONE.withCredential(new PasswordCredential( ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, "secret_store_TWO".toCharArray()))))); try { cs.store(passwordAlias1, createCredentialFromPassword(password1)); Assert.fail("This Credential Store should be read-only."); } catch (CredentialStoreException e) { } Assert.assertNull("'" + passwordAlias1 + "' must not be in this Credential Store because is read-only.", cs.retrieve(passwordAlias1, PasswordCredential.class)); } /** * Credential Store entries must be case insensitive. * * @throws NoSuchAlgorithmException * @throws CredentialStoreException * @throws UnsupportedCredentialTypeException */ @Test public void testCaseInsensitiveAlias() throws NoSuchAlgorithmException, CredentialStoreException, UnsupportedCredentialTypeException { HashMap<String, String> csAttributes = new HashMap<>(); csAttributes.put("location", stores.get("TWO")); csAttributes.put("keyStoreType", "JCEKS"); CredentialStore cs = newCredentialStoreInstance(); cs.initialize(csAttributes, new CredentialStore.CredentialSourceProtectionParameter( IdentityCredentials.NONE.withCredential(new PasswordCredential( ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, "secret_store_TWO".toCharArray()))))); // store test String caseSensitive1 = "caseSensitiveName"; String caseSensitive2 = caseSensitive1.toUpperCase(); char[] newPassword1 = "new-secret-passONE".toCharArray(); char[] newPassword2 = "new-secret-passTWO".toCharArray(); cs.store(caseSensitive1, createCredentialFromPassword(newPassword1)); if (!cs.exists(caseSensitive1, PasswordCredential.class)) { Assert.fail("'" + caseSensitive1 + "'" + " must exist"); } if (!cs.exists(caseSensitive1.toLowerCase(), PasswordCredential.class)) { Assert.fail("'" + caseSensitive1.toLowerCase() + "'" + " in lowercase must exist"); } cs.remove(caseSensitive1, PasswordCredential.class); if (cs.exists(caseSensitive1, PasswordCredential.class)) { Assert.fail(caseSensitive1 + " has been removed from the vault, but it exists"); } // this is actually alias update cs.store(caseSensitive2, createCredentialFromPassword(newPassword2)); Assert.assertArrayEquals(newPassword2, getPasswordFromCredential(cs.retrieve(caseSensitive1, PasswordCredential.class))); Assert.assertArrayEquals(newPassword2, getPasswordFromCredential(cs.retrieve(caseSensitive2, PasswordCredential.class))); Assert.assertArrayEquals(newPassword2, getPasswordFromCredential(cs.retrieve(caseSensitive1.toLowerCase(), PasswordCredential.class))); // Reaload CS keystore from filesystem csAttributes.put("location", stores.get("TWO")); csAttributes.put("keyStoreType", "JCEKS"); csAttributes.put("modifiable", "false"); CredentialStore csReloaded = newCredentialStoreInstance(); csReloaded.initialize(csAttributes, new CredentialStore.CredentialSourceProtectionParameter( IdentityCredentials.NONE.withCredential(new PasswordCredential( ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, "secret_store_TWO".toCharArray()))))); Assert.assertArrayEquals(newPassword2, getPasswordFromCredential(cs.retrieve(caseSensitive1, PasswordCredential.class))); Assert.assertArrayEquals(newPassword2, getPasswordFromCredential(cs.retrieve(caseSensitive2, PasswordCredential.class))); Assert.assertArrayEquals(newPassword2, getPasswordFromCredential(cs.retrieve(caseSensitive1.toLowerCase(), PasswordCredential.class))); } /** * Basic {@code CredentialStore} test. * @throws Exception when problem occurs */ @Test public void basicKeystorePasswordStoreTest() throws Exception { char[] password1 = "db-secret-pass1".toCharArray(); char[] password2 = "PangmaŠišatá".toCharArray(); char[] password3 = "Červenavý střizlíček a žľúva ďobali ve šťavnatých ocúnech".toCharArray(); HashMap<String, String> csAttributes = new HashMap<>(); csAttributes.put("location", stores.get("ONE")); csAttributes.put("keyStoreType", "JCEKS"); csAttributes.put("create", Boolean.TRUE.toString()); String passwordAlias1 = "db1-password1"; String passwordAlias2 = "db1-password2"; String passwordAlias3 = "db1-password3"; CredentialStore cs = newCredentialStoreInstance(); cs.initialize(csAttributes, new CredentialStore.CredentialSourceProtectionParameter( IdentityCredentials.NONE.withCredential(createCredentialFromPassword("test".toCharArray())) )); cs.store(passwordAlias1, createCredentialFromPassword(password1)); cs.store(passwordAlias2, createCredentialFromPassword(password2)); cs.store(passwordAlias3, createCredentialFromPassword(password3)); cs.flush(); Assert.assertArrayEquals(password2, getPasswordFromCredential(cs.retrieve(passwordAlias2, PasswordCredential.class))); Assert.assertArrayEquals(password1, getPasswordFromCredential(cs.retrieve(passwordAlias1, PasswordCredential.class))); Assert.assertArrayEquals(password3, getPasswordFromCredential(cs.retrieve(passwordAlias3, PasswordCredential.class))); char[] newPassword1 = "new-secret-pass1".toCharArray(); // update test cs.store(passwordAlias1, createCredentialFromPassword(newPassword1)); Assert.assertArrayEquals(newPassword1, getPasswordFromCredential(cs.retrieve(passwordAlias1, PasswordCredential.class))); // remove test cs.remove(passwordAlias2, PasswordCredential.class); if (cs.exists(passwordAlias2, PasswordCredential.class)) { Assert.fail(passwordAlias2 + " has been removed from the vault, but it exists"); } } /** * Basic {@code CredentialStore} test on already existing store. * @throws Exception when problem occurs */ @Test public void basicTestOnAlreadyCreatedKeystorePasswordStore() throws Exception { HashMap<String, String> csAttributes = new HashMap<>(); csAttributes.put("location", stores.get("TWO")); csAttributes.put("keyStoreType", "JCEKS"); // testing if KeystorePasswordStore.MODIFIABLE default value is "true", so not setting anything String passwordAlias1 = "alias1"; String passwordAlias2 = "alias2"; CredentialStore cs = newCredentialStoreInstance(); cs.initialize(csAttributes, new CredentialStore.CredentialSourceProtectionParameter( IdentityCredentials.NONE.withCredential( new PasswordCredential(ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, "secret_store_TWO".toCharArray())) ) )); // expected entries there Assert.assertArrayEquals("secret-password-1".toCharArray(), getPasswordFromCredential(cs.retrieve(passwordAlias1, PasswordCredential.class))); Assert.assertArrayEquals("secret-password-2".toCharArray(), getPasswordFromCredential(cs.retrieve(passwordAlias2, PasswordCredential.class))); // retrieve non-existent entry Assert.assertNull(cs.retrieve("wrong_alias", PasswordCredential.class)); // store test cs.store("db-password", createCredentialFromPassword("supersecretdbpass".toCharArray())); // remove test cs.remove(passwordAlias2, PasswordCredential.class); Set<String> aliases = cs.getAliases(); Assert.assertFalse("Alias \"" + passwordAlias2 + "\" should be removed.", aliases.contains(passwordAlias2)); if (!cs.exists("db-password", PasswordCredential.class)) { Assert.fail("'db-password'" + " has to exist"); } if (cs.exists(passwordAlias2, PasswordCredential.class)) { Assert.fail(passwordAlias2 + " has been removed from the vault, but it exists"); } char[] newPassword1 = "new-secret-pass1".toCharArray(); // update test cs.store(passwordAlias1, createCredentialFromPassword(newPassword1)); Assert.assertArrayEquals(newPassword1, getPasswordFromCredential(cs.retrieve(passwordAlias1, PasswordCredential.class))); } }