CertificateStoreManager.java 15 KB
Newer Older
1 2 3
package org.jivesoftware.openfire.keystore;

import org.jivesoftware.openfire.XMPPServer;
4
import org.jivesoftware.openfire.container.BasicModule;
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import org.jivesoftware.openfire.spi.ConnectionListener;
import org.jivesoftware.openfire.spi.ConnectionManagerImpl;
import org.jivesoftware.openfire.spi.ConnectionType;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * A manager of certificate stores.
 *
 */
// TODO Code duplication should be reduced.
// TODO Allow changing the store type.
23
public class CertificateStoreManager extends BasicModule
24 25 26 27 28 29 30 31
{
    private final static Logger Log = LoggerFactory.getLogger( CertificateStoreManager.class );

    private final ConcurrentMap<ConnectionType, CertificateStoreConfiguration> typeToTrustStore    = new ConcurrentHashMap<>();
    private final ConcurrentMap<ConnectionType, CertificateStoreConfiguration> typeToIdentityStore = new ConcurrentHashMap<>();
    private final ConcurrentMap<CertificateStoreConfiguration, IdentityStore>  identityStores      = new ConcurrentHashMap<>();
    private final ConcurrentMap<CertificateStoreConfiguration, TrustStore>     trustStores         = new ConcurrentHashMap<>();

32 33 34
    public CertificateStoreManager( )
    {
        super( "Certificate Store Manager" );
35 36
    }

37 38
    @Override
    public synchronized void initialize( XMPPServer server )
39
    {
40 41
        super.initialize( server );

42 43 44 45
        for ( ConnectionType type : ConnectionType.values() )
        {
            try
            {
46
                Log.debug( "(identity store for connection type '{}') Initializing store...", type );
47 48 49 50 51 52
                final CertificateStoreConfiguration identityStoreConfiguration = getIdentityStoreConfiguration( type );
                if ( !identityStores.containsKey( identityStoreConfiguration ) )
                {
                    final IdentityStore store = new IdentityStore( identityStoreConfiguration, false );
                    identityStores.put( identityStoreConfiguration, store );
                }
53
                typeToIdentityStore.put( type, identityStoreConfiguration );
54 55 56
            }
            catch ( CertificateStoreConfigException | IOException e )
            {
57
                Log.warn( "(identity store for connection type '{}') Unable to instantiate store ", type, e );
58 59 60 61
            }

            try
            {
62
                Log.debug( "(trust store for connection type '{}') Initializing store...", type );
63 64 65 66 67 68
                final CertificateStoreConfiguration trustStoreConfiguration = getTrustStoreConfiguration( type );
                if ( !trustStores.containsKey( trustStoreConfiguration ) )
                {
                    final TrustStore store = new TrustStore( trustStoreConfiguration, false );
                    trustStores.put( trustStoreConfiguration, store );
                }
69
                typeToTrustStore.put( type, trustStoreConfiguration );
70 71 72
            }
            catch ( CertificateStoreConfigException | IOException e )
            {
73
                Log.warn( "(trust store for connection type '{}') Unable to instantiate store ", type, e );
74 75 76 77
            }
        }
    }

78 79
    @Override
    public synchronized void destroy()
80
    {
81 82 83 84 85
        typeToIdentityStore.clear();
        typeToTrustStore.clear();
        identityStores.clear();
        trustStores.clear();
        super.destroy();
86 87
    }

88
    public IdentityStore getIdentityStore( ConnectionType type )
89
    {
90
        final CertificateStoreConfiguration configuration = typeToIdentityStore.get( type );
91 92 93
        if (configuration == null) {
            return null;
        }
94
        return identityStores.get( configuration );
95 96
    }

97 98 99
    public TrustStore getTrustStore( ConnectionType type )
    {
        final CertificateStoreConfiguration configuration = typeToTrustStore.get( type );
100 101 102
        if (configuration == null) {
            return null;
        }
103 104 105
        return trustStores.get( configuration );
    }

106
    public void replaceIdentityStore( ConnectionType type, CertificateStoreConfiguration configuration, boolean createIfAbsent ) throws CertificateStoreConfigException
107 108 109 110 111 112 113 114 115 116
    {
        if ( type == null)
        {
            throw new IllegalArgumentException( "Argument 'type' cannot be null." );
        }
        if ( configuration == null)
        {
            throw new IllegalArgumentException( "Argument 'configuration' cannot be null." );
        }

117
        final CertificateStoreConfiguration oldConfig = typeToIdentityStore.get( type ); // can be null if persisted properties are invalid
118 119 120 121

        if ( oldConfig == null || !oldConfig.equals( configuration ) )
        {
            // If the new store is not already being used by any other type, it'll need to be registered.
122
            if ( !identityStores.containsKey( configuration ) )
123 124
            {
                // This constructor can throw an exception. If it does, the state of the manager should not have already changed.
125
                final IdentityStore store = new IdentityStore( configuration, createIfAbsent );
126
                identityStores.put( configuration, store );
127 128
            }

129
            typeToIdentityStore.put( type, configuration );
130 131 132


            // If the old store is not used by any other type, it can be shut down.
133
            if ( oldConfig != null && !typeToIdentityStore.containsValue( oldConfig ) )
134
            {
135
                identityStores.remove( oldConfig );
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
            }

            // Update all connection listeners that were using the old configuration.
            final ConnectionManagerImpl connectionManager = ((ConnectionManagerImpl) XMPPServer.getInstance().getConnectionManager());
            for ( ConnectionListener connectionListener : connectionManager.getListeners( type ) ) {
                try {
                    connectionListener.setIdentityStoreConfiguration( configuration );
                } catch ( RuntimeException e ) {
                    Log.warn( "An exception occurred while trying to update the identity store configuration for connection type '" + type + "'", e );
                }
            }
        }

        // Always store the new configuration in properties, to make sure that we override a potential fallback.
        JiveGlobals.setProperty( type.getPrefix() + "keystore", configuration.getFile().getPath() ); // FIXME ensure that this is relative to Openfire home!
        JiveGlobals.setProperty( type.getPrefix() + "keypass", new String( configuration.getPassword() ) );
    }

154
    public void replaceTrustStore( ConnectionType type, CertificateStoreConfiguration configuration, boolean createIfAbsent ) throws CertificateStoreConfigException
155 156 157 158 159 160 161 162 163 164
    {
        if ( type == null)
        {
            throw new IllegalArgumentException( "Argument 'type' cannot be null." );
        }
        if ( configuration == null)
        {
            throw new IllegalArgumentException( "Argument 'configuration' cannot be null." );
        }

165
        final CertificateStoreConfiguration oldConfig = typeToTrustStore.get( type ); // can be null if persisted properties are invalid
166 167 168 169

        if ( oldConfig == null || !oldConfig.equals( configuration ) )
        {
            // If the new store is not already being used by any other type, it'll need to be registered.
170
            if ( !trustStores.containsKey( configuration ) )
171 172
            {
                // This constructor can throw an exception. If it does, the state of the manager should not have already changed.
173
                final TrustStore store = new TrustStore( configuration, createIfAbsent );
174
                trustStores.put( configuration, store );
175 176
            }

177
            typeToTrustStore.put( type, configuration );
178 179 180


            // If the old store is not used by any other type, it can be shut down.
181
            if ( oldConfig != null && !typeToTrustStore.containsValue( oldConfig ) )
182
            {
183
                trustStores.remove( oldConfig );
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
            }

            // Update all connection listeners that were using the old configuration.
            final ConnectionManagerImpl connectionManager = ((ConnectionManagerImpl) XMPPServer.getInstance().getConnectionManager());
            for ( ConnectionListener connectionListener : connectionManager.getListeners( type ) ) {
                try {
                    connectionListener.setTrustStoreConfiguration( configuration );
                } catch ( RuntimeException e ) {
                    Log.warn( "An exception occurred while trying to update the trust store configuration for connection type '" + type + "'", e );
                }
            }

        }

        // Always store the new configuration in properties, to make sure that we override a potential fallback.
        JiveGlobals.setProperty( type.getPrefix() + "truststore", configuration.getFile().getPath() ); // FIXME ensure that this is relative to Openfire home!
        JiveGlobals.setProperty( type.getPrefix() + "trustpass", new String( configuration.getPassword() )  );
    }

203
    public CertificateStoreConfiguration getIdentityStoreConfiguration( ConnectionType type ) throws IOException
204 205 206 207 208 209 210 211 212 213 214 215
    {
        // Getting individual properties might use fallbacks. It is assumed (but not asserted) that each property value
        // is obtained from the same connectionType (which is either the argument to this method, or one of its
        // fallbacks.
        final String keyStoreType = getKeyStoreType( type );
        final String password = getIdentityStorePassword( type );
        final String location = getIdentityStoreLocation( type );
        final File file = canonicalize( location );

        return new CertificateStoreConfiguration( keyStoreType, file, password.toCharArray() );
    }

216
    public CertificateStoreConfiguration getTrustStoreConfiguration( ConnectionType type ) throws IOException
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
    {
        // Getting individual properties might use fallbacks. It is assumed (but not asserted) that each property value
        // is obtained from the same connectionType (which is either the argument to this method, or one of its
        // fallbacks.
        final String keyStoreType = getKeyStoreType( type );
        final String password = getTrustStorePassword( type );
        final String location = getTrustStoreLocation( type );
        final File file = canonicalize( location );

        return new CertificateStoreConfiguration( keyStoreType, file, password.toCharArray() );
    }

    /**
     * The KeyStore type (jks, jceks, pkcs12, etc) for the identity and trust store for connections created by this
     * listener.
     *
     * @return a store type (never null).
     * @see <a href="https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#KeyStore">Java Cryptography Architecture Standard Algorithm Name Documentation</a>
     */
    static String getKeyStoreType( ConnectionType type )
    {
        final String propertyName = type.getPrefix() + "storeType";
        final String defaultValue = "jks";

        if ( type.getFallback() == null )
        {
            return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
        }
        else
        {
            return JiveGlobals.getProperty( propertyName, getKeyStoreType( type.getFallback() ) ).trim();
        }
    }

    static void setKeyStoreType( ConnectionType type, String keyStoreType )
    {
        // Always set the property explicitly even if it appears the equal to the old value (the old value might be a fallback value).
        JiveGlobals.setProperty( type.getPrefix() + "storeType", keyStoreType );

        final String oldKeyStoreType = getKeyStoreType( type );
        if ( oldKeyStoreType.equals( keyStoreType ) )
        {
            Log.debug( "Ignoring KeyStore type change request (to '{}'): listener already in this state.", keyStoreType );
            return;
        }

        Log.debug( "Changing KeyStore type from '{}' to '{}'.", oldKeyStoreType, keyStoreType );
    }

    /**
     * The password of the identity store for connection created by this listener.
     *
     * @return a password (never null).
     */
    static String getIdentityStorePassword( ConnectionType type )
    {
        final String propertyName = type.getPrefix() + "keypass";
        final String defaultValue = "changeit";

        if ( type.getFallback() == null )
        {
            return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
        }
        else
        {
            return JiveGlobals.getProperty( propertyName, getIdentityStorePassword( type.getFallback() ) ).trim();
        }
    }

    /**
     * The password of the trust store for connections created by this listener.
     *
     * @return a password (never null).
     */
    static String getTrustStorePassword( ConnectionType type )
    {
        final String propertyName = type.getPrefix() + "trustpass";
        final String defaultValue = "changeit";

        if ( type.getFallback() == null )
        {
            return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
        }
        else
        {
            return JiveGlobals.getProperty( propertyName, getTrustStorePassword( type.getFallback() ) ).trim();
        }
    }

    /**
     * The location (relative to OPENFIRE_HOME) of the identity store for connections created by this listener.
     *
     * @return a path (never null).
     */
    static String getIdentityStoreLocation( ConnectionType type )
    {
        final String propertyName = type.getPrefix()  + "keystore";
        final String defaultValue = "resources" + File.separator + "security" + File.separator + "keystore";

        if ( type.getFallback() == null )
        {
            return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
        }
        else
        {
            return JiveGlobals.getProperty( propertyName, getIdentityStoreLocation( type.getFallback() ) ).trim();
        }
    }

    /**
     * The location (relative to OPENFIRE_HOME) of the trust store for connections created by this listener.
     *
     * @return a path (never null).
     */
    static String getTrustStoreLocation( ConnectionType type )
    {
        final String propertyName = type.getPrefix()  + "truststore";
        final String defaultValue = "resources" + File.separator + "security" + File.separator + "truststore";

        if ( type.getFallback() == null )
        {
            return JiveGlobals.getProperty( propertyName, defaultValue ).trim();
        }
        else
        {
            return JiveGlobals.getProperty( propertyName, getTrustStoreLocation( type.getFallback() ) ).trim();
        }
    }

    /**
     * Canonicalizes a path. When the provided path is a relative path, it is interpreted as to be relative to the home
     * directory of Openfire.
     *
     * @param path A path (cannot be null)
     * @return A canonical representation of the path.
     */
    static File canonicalize( String path ) throws IOException
    {
        File file = new File( path );
        if (!file.isAbsolute()) {
            file = new File( JiveGlobals.getHomeDirectory() + File.separator + path );
        }

        return file;
    }
}