/* * Copyright (C) 2004-2008 Jive Software, 2017-2025 Ignite Realtime Foundation. All rights reserved. * * 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.jivesoftware.util; import org.apache.commons.lang3.StringUtils; import org.jivesoftware.database.DbConnectionManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.text.DateFormat; import java.util.*; import java.util.Map.Entry; /** * Controls Jive properties. Jive properties are only meant to be set and retrieved * by core Jive classes. Some properties may be stored in XML format while others in the * database.

* * When starting up the application this class needs to be configured so that the initial * configuration of the application may be loaded from the configuration file. The configuration * file holds properties stored in XML format, database configuration and user authentication * configuration. Use {@link #setHomePath(Path)} and {@link #setConfigName(String)} for * setting the home directory and path to the configuration file.

* * XML property names must be in the form prop.name - parts of the name must * be separated by ".". The value can be any valid String, including strings with line breaks. */ public class JiveGlobals { private static final Logger Log = LoggerFactory.getLogger(JiveGlobals.class); private static String JIVE_CONFIG_FILENAME = "conf" + File.separator + "openfire.xml"; private static final String JIVE_SECURITY_FILENAME = "conf" + File.separator + "security.xml"; private static final String ENCRYPTED_PROPERTY_NAME_PREFIX = "encrypt."; private static final String ENCRYPTED_PROPERTY_NAMES = ENCRYPTED_PROPERTY_NAME_PREFIX + "property.name"; private static final String ENCRYPTION_ALGORITHM = ENCRYPTED_PROPERTY_NAME_PREFIX + "algorithm"; private static final String ENCRYPTION_KEY_CURRENT = ENCRYPTED_PROPERTY_NAME_PREFIX + "key.current"; private static final String ENCRYPTION_KEY_NEW = ENCRYPTED_PROPERTY_NAME_PREFIX + "key.new"; private static final String ENCRYPTION_KEY_OLD = ENCRYPTED_PROPERTY_NAME_PREFIX + "key.old"; private static final String ENCRYPTION_ALGORITHM_AES = "AES"; private static final String ENCRYPTION_ALGORITHM_BLOWFISH = "Blowfish"; private static final String BLOWFISH_KDF = ENCRYPTED_PROPERTY_NAME_PREFIX + "blowfish.kdf"; private static final String BLOWFISH_SALT = ENCRYPTED_PROPERTY_NAME_PREFIX + "blowfish.salt"; /** Blowfish key derivation function using PBKDF2-HMAC-SHA512 */ public static final String BLOWFISH_KDF_PBKDF2 = "pbkdf2"; /** Blowfish key derivation function using legacy SHA1 (for backward compatibility) */ public static final String BLOWFISH_KDF_SHA1 = "sha1"; /** * Location of the jiveHome directory. All configuration files should be * located here. */ private static Path home = null; private static boolean failedLoading = false; private static XMLProperties openfireProperties = null; private static XMLProperties securityProperties = null; private static JiveProperties properties = null; private static Locale locale = null; private static TimeZone timeZone = null; private static DateFormat dateFormat = null; private static DateFormat dateTimeFormat = null; private static DateFormat timeFormat = null; private static Encryptor propertyEncryptor = null; private static Encryptor propertyEncryptorNew = null; private static String currentKey = null; /** * Returns the global Locale used by Jive. A locale specifies language * and country codes, and is used for internationalization. The default * locale is system dependent - Locale.getDefault(). * * @return the global locale used by Jive. */ public static Locale getLocale() { if (locale == null) { if (openfireProperties != null) { String [] localeArray; String localeProperty = openfireProperties.getProperty("locale"); if (localeProperty != null) { localeArray = localeProperty.split("_"); } else { localeArray = new String[] {"", ""}; } String language = localeArray[0]; if (language == null) { language = ""; } String country = ""; if (localeArray.length == 2) { country = localeArray[1]; } // If no locale info is specified, return the system default Locale. if (language.isEmpty() && country.isEmpty()) { locale = Locale.getDefault(); } else { locale = new Locale(language, country); } } else { return Locale.getDefault(); } } return locale; } /** * Sets the global locale used by Jive. A locale specifies language * and country codes, and is used for formatting dates and numbers. * The default locale is Locale.US. * * @param newLocale the global Locale for Jive. */ public static void setLocale(Locale newLocale) { locale = newLocale; // Save values to Jive properties. setXMLProperty("locale", locale.toString()); // Reset the date formatter objects timeFormat = null; dateFormat = null; dateTimeFormat = null; } /** * Returns the global TimeZone used by Jive. The default is the VM's * default time zone. * * @return the global time zone used by Jive. */ public static TimeZone getTimeZone() { if (timeZone == null) { if (properties != null) { String timeZoneID = properties.get("locale.timeZone"); if (timeZoneID == null) { timeZone = TimeZone.getDefault(); } else { timeZone = TimeZone.getTimeZone(timeZoneID); } } else { return TimeZone.getDefault(); } } return timeZone; } /** * Sets the global time zone used by Jive. The default time zone is the VM's * time zone. * * @param newTimeZone Time zone to set. */ public static void setTimeZone(TimeZone newTimeZone) { timeZone = newTimeZone; if (timeFormat != null) { timeFormat.setTimeZone(timeZone); } if (dateFormat != null) { dateFormat.setTimeZone(timeZone); } if (dateTimeFormat != null) { dateTimeFormat.setTimeZone(timeZone); } setProperty("locale.timeZone", timeZone.getID()); } /** * Formats a Date object to return a time using the global locale. * * @param date the Date to format. * @return a String representing the time. */ public static String formatTime(Date date) { if (timeFormat == null) { if (properties != null) { timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, getLocale()); timeFormat.setTimeZone(getTimeZone()); } else { DateFormat instance = DateFormat.getTimeInstance(DateFormat.SHORT, getLocale()); instance.setTimeZone(getTimeZone()); return instance.format(date); } } return timeFormat.format(date); } /** * Formats a Date object to return a date using the global locale. * * @param date the Date to format. * @return a String representing the date. */ public static String formatDate(Date date) { if (dateFormat == null) { if (properties != null) { dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, getLocale()); dateFormat.setTimeZone(getTimeZone()); } else { DateFormat instance = DateFormat.getDateInstance(DateFormat.MEDIUM, getLocale()); instance.setTimeZone(getTimeZone()); return instance.format(date); } } return dateFormat.format(date); } /** * Formats a Date object to return a date and time using the global locale. * * @param date the Date to format. * @return a String representing the date and time. */ public static String formatDateTime(Date date) { if (dateTimeFormat == null) { if (properties != null) { dateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, getLocale()); dateTimeFormat.setTimeZone(getTimeZone()); } else { DateFormat instance = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, getLocale()); instance.setTimeZone(getTimeZone()); return instance.format(date); } } return dateTimeFormat.format(date); } /** * Returns the location of the home directory. * * @return the location of the home dir. */ public static Path getHomePath() { if (openfireProperties == null) { loadOpenfireProperties(); } return home; } /** * Sets the location of the home directory. The directory must exist and the * user running the application must have read and write permissions over the specified * directory. * * @param homeDir the location of the home dir. */ public static void setHomePath(Path homeDir) { // Do a permission check on the new home directory if (!Files.exists(homeDir) || !Files.isDirectory(homeDir)) { Log.error("Error - the specified home directory does not exist or is not a directory (" + homeDir + ")"); } else if (!Files.isReadable(homeDir) || !Files.isWritable(homeDir)) { Log.error("Error - the user running this application can not read " + "and write to the specified home directory (" + homeDir + "). " + "Please grant the executing user read and write permissions."); } else { home = homeDir.normalize().toAbsolutePath(); } } /** * Returns a local property. Local properties are stored in the file defined in * {@code JIVE_CONFIG_FILENAME} that exists in the {@code home} directory. * Properties are always specified as "foo.bar.prop", which would map to * the following entry in the XML file: *

     * <foo>
     *     <bar>
     *         <prop>some value</prop>
     *     </bar>
     * </foo>
     * 
* * @param name the name of the property to return. * @return the property value specified by name. */ public static String getXMLProperty(String name) { if (openfireProperties == null) { loadOpenfireProperties(); } return openfireProperties.getProperty(name); } /** * Returns a local property. Local properties are stored in the file defined in * {@code JIVE_CONFIG_FILENAME} that exists in the {@code home} directory. * Properties are always specified as "foo.bar.prop", which would map to * the following entry in the XML file: *
     * <foo>
     *     <bar>
     *         <prop>some value</prop>
     *     </bar>
     * </foo>
     * 
* * If the specified property can't be found, the {@code defaultValue} will be returned. * * @param name the name of the property to return. * @param defaultValue the default value for the property. * @return the property value specified by name. */ public static String getXMLProperty(String name, String defaultValue) { if (openfireProperties == null) { loadOpenfireProperties(); } String value = openfireProperties.getProperty(name); if (value == null) { value = defaultValue; } return value; } /** * Returns an integer value local property. Local properties are stored in the file defined in * {@code JIVE_CONFIG_FILENAME} that exists in the {@code home} directory. * Properties are always specified as "foo.bar.prop", which would map to * the following entry in the XML file: *
     * <foo>
     *     <bar>
     *         <prop>some value</prop>
     *     </bar>
     * </foo>
     * 
* * If the specified property can't be found, or if the value is not a number, the * {@code defaultValue} will be returned. * * @param name the name of the property to return. * @param defaultValue value returned if the property could not be loaded or was not * a number. * @return the property value specified by name or {@code defaultValue}. */ public static int getXMLProperty(String name, int defaultValue) { String value = getXMLProperty(name); if (value != null) { try { return Integer.parseInt(value); } catch (NumberFormatException nfe) { // Ignore. } } return defaultValue; } /** * Returns a boolean value local property. Local properties are stored in the * file defined in {@code JIVE_CONFIG_FILENAME} that exists in the {@code home} * directory. Properties are always specified as "foo.bar.prop", which would map to * the following entry in the XML file: *
     * <foo>
     *     <bar>
     *         <prop>some value</prop>
     *     </bar>
     * </foo>
     * 
* * If the specified property can't be found, the {@code defaultValue} will be returned. * If the property is found, it will be parsed using {@link Boolean#valueOf(String)}. * * @param name the name of the property to return. * @param defaultValue value returned if the property could not be loaded or was not * a number. * @return the property value specified by name or {@code defaultValue}. */ public static boolean getXMLProperty(String name, boolean defaultValue) { String value = getXMLProperty(name); if (value != null) { return Boolean.parseBoolean(value); } return defaultValue; } /** * Sets a local property. If the property doesn't already exists, a new * one will be created. Local properties are stored in the file defined in * {@code JIVE_CONFIG_FILENAME} that exists in the {@code home} directory. * Properties are always specified as "foo.bar.prop", which would map to * the following entry in the XML file: *
     * <foo>
     *     <bar>
     *         <prop>some value</prop>
     *     </bar>
     * </foo>
     * 
* * @param name the name of the property being set. * @param value the value of the property being set. * @return {@code true} if the property was correctly saved to file, otherwise {@code false} */ public static boolean setXMLProperty(String name, String value) { if (openfireProperties == null) { loadOpenfireProperties(); } return openfireProperties.setProperty(name, value); } /** * Sets multiple local properties at once. If a property doesn't already exists, a new * one will be created. Local properties are stored in the file defined in * {@code JIVE_CONFIG_FILENAME} that exists in the {@code home} directory. * Properties are always specified as "foo.bar.prop", which would map to * the following entry in the XML file: *
     * <foo>
     *     <bar>
     *         <prop>some value</prop>
     *     </bar>
     * </foo>
     * 
* * @param propertyMap a map of properties, keyed on property name. */ public static void setXMLProperties(Map propertyMap) { if (openfireProperties == null) { loadOpenfireProperties(); } openfireProperties.setProperties(propertyMap); } /** * Return all immediate children property values of a parent local property as a list of strings, * or an empty list if there are no children. For example, given * the properties {@code X.Y.A}, {@code X.Y.B}, {@code X.Y.C} and {@code X.Y.C.D}, then * the immediate child properties of {@code X.Y} are {@code A}, {@code B}, and * {@code C} (the value of {@code C.D} would not be returned using this method).

* * Local properties are stored in the file defined in {@code JIVE_CONFIG_FILENAME} that exists * in the {@code home} directory. Properties are always specified as "foo.bar.prop", * which would map to the following entry in the XML file: *

     * <foo>
     *     <bar>
     *         <prop>some value</prop>
     *     </bar>
     * </foo>
     * 
* * * @param parent the name of the parent property to return the children for. * @return all child property values for the given parent. */ public static List getXMLProperties(String parent) { if (openfireProperties == null) { loadOpenfireProperties(); } String[] propNames = openfireProperties.getChildrenProperties(parent); List values = new ArrayList<>(); for (String propName : propNames) { String value = getXMLProperty(parent + "." + propName); if (value != null) { values.add(value); } } return values; } /** * Return all property names as a list of strings, or an empty list if jiveHome has not been loaded. * * @return all child property for the given parent. */ public static List getXMLPropertyNames() { if (openfireProperties == null) { loadOpenfireProperties(); } return openfireProperties.getAllPropertyNames(); } /** * Deletes a locale property. If the property doesn't exist, the method * does nothing. * * @param name the name of the property to delete. */ public static void deleteXMLProperty(String name) { if (openfireProperties == null) { loadOpenfireProperties(); } openfireProperties.deleteProperty(name); } /** * Returns a Jive property. * * @param name the name of the property to return. * @return the property value specified by name. */ public static String getProperty(String name) { if (properties == null) { if (isSetupMode()) { return null; } properties = JiveProperties.getInstance(); } return properties.get(name); } /** * Returns a Jive property. If the specified property doesn't exist, the * {@code defaultValue} will be returned. * * @param name the name of the property to return. * @param defaultValue value returned if the property doesn't exist. * @return the property value specified by name. */ public static String getProperty(String name, String defaultValue) { if (properties == null) { if (isSetupMode()) { return defaultValue; } properties = JiveProperties.getInstance(); } String value = properties.get(name); if (value != null) { return value; } else { return defaultValue; } } /** * Returns an enum constant Jive property. If the specified property doesn't exist, or if it's value cannot be parsed * as an enum constant, the {@code defaultValue} will be returned. * * @param name the name of the property to return. * @param enumType the {@code Class} object of the enum type from which to return a constant. * @param defaultValue value returned if the property doesn't exist or it's value could not be parsed. * @param The enum type whose constant is to be returned. * @return the property value (as an enum constant) or {@code defaultValue}. */ public static > E getEnumProperty( String name, Class enumType, E defaultValue ) { String value = getProperty( name ); if ( value != null ) { try { return E.valueOf( enumType, value ); } catch ( IllegalArgumentException e ) { // Ignore } } return defaultValue; } /** * Returns an integer value Jive property. If the specified property doesn't exist, the * {@code defaultValue} will be returned. * * @param name the name of the property to return. * @param defaultValue value returned if the property doesn't exist or was not * a number. * @return the property value specified by name or {@code defaultValue}. */ public static int getIntProperty(String name, int defaultValue) { String value = getProperty(name); if (value != null) { try { return Integer.parseInt(value); } catch (NumberFormatException nfe) { // Ignore. } } return defaultValue; } /** * Returns a long value Jive property. If the specified property doesn't exist, the * {@code defaultValue} will be returned. * * @param name the name of the property to return. * @param defaultValue value returned if the property doesn't exist or was not * a number. * @return the property value specified by name or {@code defaultValue}. */ public static long getLongProperty(String name, long defaultValue) { String value = getProperty(name); if (value != null) { try { return Long.parseLong(value); } catch (NumberFormatException nfe) { // Ignore. } } return defaultValue; } /** * Returns a double value Jive property. If the specified property doesn't exist, the * {@code defaultValue} will be returned. * * @param name the name of the property to return. * @param defaultValue value returned if the property doesn't exist or was not * a number. * @return the property value specified by name or {@code defaultValue}. */ public static double getDoubleProperty(String name, double defaultValue) { String value = getProperty(name); if (value != null) { try { return Double.parseDouble(value); } catch (NumberFormatException nfe) { // Ignore. } } return defaultValue; } /** * Returns a boolean value Jive property. * * @param name the name of the property to return. * @return true if the property value exists and is set to {@code "true"} (ignoring case). * Otherwise {@code false} is returned. */ public static boolean getBooleanProperty(String name) { return Boolean.parseBoolean(getProperty(name)); } /** * Returns a boolean value Jive property. If the property doesn't exist, the {@code defaultValue} * will be returned. * * If the specified property can't be found, or if the value is not a number, the * {@code defaultValue} will be returned. * * @param name the name of the property to return. * @param defaultValue value returned if the property doesn't exist. * @return true if the property value exists and is set to {@code "true"} (ignoring case). * Otherwise {@code false} is returned. */ public static boolean getBooleanProperty(String name, boolean defaultValue) { String value = getProperty(name); if (value != null) { return Boolean.parseBoolean(value); } else { return defaultValue; } } /** * Return all immediate children property names of a parent Jive property as a list of strings, * or an empty list if there are no children. For example, given * the properties {@code X.Y.A}, {@code X.Y.B}, {@code X.Y.C} and {@code X.Y.C.D}, then * the immediate child properties of {@code X.Y} are {@code A}, {@code B}, and * {@code C} ({@code C.D} would not be returned using this method).

* * @param parent Parent "node" to find the children of. * @return a List of all immediate children property names (Strings). */ public static List getPropertyNames(String parent) { if (properties == null) { if (isSetupMode()) { return new ArrayList<>(); } properties = JiveProperties.getInstance(); } return new ArrayList<>(properties.getChildrenNames(parent)); } /** * Return all immediate children property values of a parent Jive property as a list of strings, * or an empty list if there are no children. For example, given * the properties {@code X.Y.A}, {@code X.Y.B}, {@code X.Y.C} and {@code X.Y.C.D}, then * the immediate child properties of {@code X.Y} are {@code X.Y.A}, {@code X.Y.B}, and * {@code X.Y.C} (the value of {@code X.Y.C.D} would not be returned using this method).

* * @param parent the name of the parent property to return the children for. * @return all child property values for the given parent. */ public static List getProperties( String parent ) { return getListProperty( parent, new ArrayList<>() ); } /** * Return all immediate children property values of a parent Jive property as a list of strings, or an default list * if the property does not exist. * * This implementation ignores child property values that are empty (these are excluded from the result). When all * child properties are empty, an empty collection (and explicitly not the default values) is returned. This allows * a property to override a default non-empty collection with an empty one. * * The child properties that are evaluated in this method are the same child properties as those used by * {@link #getProperties(String)}. * * @param parent the name of the parent property to return the children for. * @param defaultValues values returned if the property doesn't exist. * @return all child property values for the given parent. */ public static List getListProperty( String parent, List defaultValues ) { if ( properties == null ) { if ( isSetupMode() ) { return defaultValues; } properties = JiveProperties.getInstance(); } // Check for a legacy, comma separated value. final String legacyValue = JiveGlobals.getProperty( parent ); // Ensure that properties are ordered. final SortedSet propertyNames = new TreeSet<>( properties.getChildrenNames( parent ) ); if ( propertyNames.isEmpty() ) { if ( legacyValue != null ) { Log.info( "Retrieving a list from property '{}' which is stored in a comma-separated format. Consider using child properties instead, via JiveGlobals.setProperty( String value, List values )", parent ); return Arrays.asList( legacyValue.split( "\\s*,\\s*" ) ); } // When there are no child properties, return the default values. return defaultValues; } else if ( legacyValue != null ) { // Raise a warning if two competing sets of data are detected. Log.warn( "Retrieving a list from property '{}' which is stored using child properties, but also in a legacy format! The data that is in the legacy format (the text value of property '{}') is not returned by this call! Its child property values are used instead. Consider removing the text value of the parent property.", parent, parent ); } // When there are child properties, return its non-null, non-empty values (which might be an empty collection). final List values = new ArrayList<>(); for ( String propertyName : propertyNames ) { final String value = getProperty( propertyName ); if ( value != null && !value.isEmpty()) { values.add( value ); } } return values; } /** * Returns all Jive property names. * * @return a List of all property names (Strings). */ public static List getPropertyNames() { if (properties == null) { if (isSetupMode()) { return new ArrayList<>(); } properties = JiveProperties.getInstance(); } return new ArrayList<>(properties.getPropertyNames()); } /** * Sets a Jive property. If the property doesn't already exists, a new * one will be created. * * @param name the name of the property being set. * @param value the value of the property being set. */ public static void setProperty(String name, String value) { setProperty(name, value, false); } /** * Sets a Jive property. If the property doesn't already exists, a new * one will be created. * * @param name the name of the property being set. * @param value the value of the property being set. * @param encrypt {@code true} to encrypt the property in the database, other {@code false} */ public static void setProperty(String name, String value, boolean encrypt) { if (properties == null) { if (isSetupMode()) { return; } properties = JiveProperties.getInstance(); } properties.put(name, value, encrypt); } /** * Sets a Jive property with a list of values. If the property doesn't already exists, a new one will be created. * Empty or null values in the collection are ignored. * * Each value is stored as a direct child property of the property name provided as an argument to this method. When * this method is used, all previous children of the property will be deleted. * * When the provided value is null, any previously stored collection will be removed. If it is an empty collection * (or a collection that consists of null and empty values onlu), it is stored as an empty collection * (represented by a child property that has an empty value). * * The naming convention used by this method to define child properties is subject to change, and should not be * depended on. * * This method differs from {@link #setProperties(Map)}, which is used to set multiple properties. This method sets * one property with multiple values. * * @param name the name of the property being set. * @param values the values of the property. */ public static void setProperty( String name, List values ) { if ( properties == null ) { if ( isSetupMode() ) { return; } properties = JiveProperties.getInstance(); } final List existing = getProperties( name ); if ( existing != null && existing.equals( values ) ) { // no change. return; } properties.remove( name ); if ( values != null ) { int i = 1; for ( final String value : values ) { if ( value != null && !value.isEmpty() ) { final String childName = name + "." + String.format("%05d", i++ ); properties.put( childName, value ); } } // When no non-null, non-empty values are stored, store one to denote an empty collection. if ( i == 1 ) { properties.put( name + ".00001", "" ); } // The put's above will have generated events for each child property. Now, generate an event for the parent. final Map params = new HashMap<>(); params.put("value", values); PropertyEventDispatcher.dispatchEvent(name, PropertyEventDispatcher.EventType.property_set, params); } } /** * Sets multiple Jive properties at once. If a property doesn't already exists, a new one will be created. * * This method differs from {@link #setProperty(String, List)}, which is used to one property with multiple * values. This method sets multiple properties, each with one value. * * @param propertyMap a map of properties, keyed on property name. */ public static void setProperties(Map propertyMap) { if (properties == null) { if (isSetupMode()) { return; } properties = JiveProperties.getInstance(); } properties.putAll(propertyMap); } /** * Deletes a Jive property. If the property doesn't exist, the method * does nothing. All children of the property will be deleted as well. * * @param name the name of the property to delete. */ public static void deleteProperty(String name) { if (properties == null) { if (isSetupMode()) { return; } properties = JiveProperties.getInstance(); } properties.remove(name); clearXMLPropertyEncryptionEntry(name); } static void clearXMLPropertyEncryptionEntry(String name) { if (isSetupMode()) { return; } if (securityProperties == null) { loadSecurityProperties(); } if (openfireProperties == null) { loadOpenfireProperties(); } // Note; only remove the encryption indicator from XML file if the (encrypted) property is not also defined in the XML file if (JiveGlobals.isXMLPropertyEncrypted(name) && openfireProperties.getProperty(name) == null) { securityProperties.removeFromList(ENCRYPTED_PROPERTY_NAMES, name); } } /** * Convenience routine to migrate an XML property into the database * storage method. Will check for the XML property being null before * migrating. * * @param name the name of the property to migrate. */ public static void migrateProperty(String name) { if (isSetupMode()) { return; } if (openfireProperties == null) { loadOpenfireProperties(); } openfireProperties.migrateProperty(name); } /** * Convenience routine to migrate a tree of XML propertis into the database * storage method. * * @param name the name of the base property to migrate. */ public static void migratePropertyTree(String name) { if (isSetupMode()) { return; } if (openfireProperties == null) { loadOpenfireProperties(); } final String[] children = openfireProperties.getChildrenProperties( name ); if ( children != null ) { for ( final String child : children ) { migratePropertyTree( name + "." + child ); } } openfireProperties.migrateProperty(name); } /** * Flags certain properties as being sensitive, based on * property naming conventions. Values for matching property * names are hidden from the Openfire console. * * @param name The name of the property * @return True if the property is considered sensitive, otherwise false */ public static boolean isPropertySensitive(String name) { return name != null && ( name.toLowerCase().contains("passwd") || name.toLowerCase().contains("password") || name.toLowerCase().contains("cookiekey")); } /** * Determines whether an XML property is configured for encryption. * * @param name * The name of the property * @return {@code true} if the property is stored using encryption, otherwise {@code false} */ public static boolean isXMLPropertyEncrypted(final String name) { if (securityProperties == null) { loadSecurityProperties(); } return name != null && !name.startsWith(JiveGlobals.ENCRYPTED_PROPERTY_NAME_PREFIX) && securityProperties.getProperties(JiveGlobals.ENCRYPTED_PROPERTY_NAMES, true).contains(name); } /** * Determines whether a property is configured for encryption. * * @param name * The name of the property * @return {@code true} if the property is stored using encryption, otherwise {@code false} */ public static boolean isPropertyEncrypted(String name) { if (properties == null) { if (isSetupMode()) { return false; } properties = JiveProperties.getInstance(); } return properties.isEncrypted(name); } /** * Set the encryption status for the given property. * * @param name The name of the property * @param encrypt True to encrypt the property, false to decrypt * @return True if the property's encryption status changed, otherwise false */ public static boolean setPropertyEncrypted(String name, boolean encrypt) { if (properties == null) { if (isSetupMode()) { return false; } properties = JiveProperties.getInstance(); } return properties.setPropertyEncrypted(name, encrypt); } /** * Fetches the property encryptor. * * @param useNewEncryptor Should use the new encryptor * @return The property encryptor */ public static Encryptor getPropertyEncryptor(boolean useNewEncryptor) { if (securityProperties == null) { loadSecurityProperties(); } if (propertyEncryptor == null) { String algorithm = securityProperties.getProperty(ENCRYPTION_ALGORITHM); propertyEncryptor = getEncryptor(algorithm, currentKey); propertyEncryptorNew = propertyEncryptor; } return useNewEncryptor ? propertyEncryptorNew : propertyEncryptor; } /** * Fetches the current property encryptor. * * @return The current property encryptor */ public static Encryptor getPropertyEncryptor() { return getPropertyEncryptor(false); } /** * This method is called early during the setup process to * set the algorithm for encrypting property values * @param alg the algorithm used to encrypt properties */ public static void setupPropertyEncryptionAlgorithm(String alg) { // Get the old secret key and encryption type String oldAlg = securityProperties.getProperty(ENCRYPTION_ALGORITHM); String oldKey = getCurrentKey(); if (StringUtils.isNotEmpty(alg) && !oldAlg.equals(alg) && (StringUtils.isNotEmpty(oldKey) || propertyEncryptor != null)) { // update encrypted properties updateEncryptionProperties(alg, oldKey); } // Set the new algorithm if (ENCRYPTION_ALGORITHM_AES.equalsIgnoreCase(alg)) { securityProperties.setProperty(ENCRYPTION_ALGORITHM, ENCRYPTION_ALGORITHM_AES); } else { securityProperties.setProperty(ENCRYPTION_ALGORITHM, ENCRYPTION_ALGORITHM_BLOWFISH); } } /** * This method is called early during the setup process to * set a custom key for encrypting property values * @param key the key used to encrypt properties */ public static void setupPropertyEncryptionKey(String key) { // Get the old secret key and encryption type String oldAlg = securityProperties.getProperty(ENCRYPTION_ALGORITHM); String oldKey = getCurrentKey(); if ((StringUtils.isNotEmpty(oldKey) || propertyEncryptor != null) && StringUtils.isNotEmpty(key) && !key.equals(oldKey) && StringUtils.isNotEmpty(oldAlg)) { // update encrypted properties updateEncryptionProperties(oldAlg, key); } // Set the new key (obfuscated, not encrypted - the key just needs to be hidden from casual viewing) securityProperties.setProperty(ENCRYPTION_KEY_CURRENT, new Obfuscator().obfuscate(key)); currentKey = key == "" ? null : key; propertyEncryptorNew = getEncryptor(oldAlg, key); propertyEncryptor = propertyEncryptorNew; } /** * Get current encryptor key. * The key is stored obfuscated (not encrypted) in security.xml. * Obfuscator is used for backward compatibility - it can deobfuscate * values that were previously encrypted with AesEncryptor using hardcoded IV. * * @see Obfuscator */ private static String getCurrentKey() { String obfuscatedKey = securityProperties.getProperty(ENCRYPTION_KEY_CURRENT); String key = null; if (StringUtils.isNotEmpty(obfuscatedKey)) { key = new Obfuscator().deobfuscate(obfuscatedKey); } return key; } /** * Gets the current master encryption key used for property encryption. * The key is deobfuscated from security.xml. * * This method is primarily used by migration tools and encryption utilities * that need direct access to the master key. * * @return The current master encryption key, or null if no key is configured */ public static String getMasterEncryptionKey() { if (securityProperties == null) { loadSecurityProperties(); } return getCurrentKey(); } /** * Get current encryptor according to alg and key. * * @param alg algorithm type * @param key encryptor key */ private static Encryptor getEncryptor(String alg, String key) { Encryptor encryptor; if (ENCRYPTION_ALGORITHM_AES.equalsIgnoreCase(alg)) { encryptor = new AesEncryptor(key); } else { encryptor = new Blowfish(key); } return encryptor; } /** * Gets the Blowfish encryption salt. If no salt exists, generates a new * cryptographically random 32-byte salt and stores it in security.xml. * * @return Base64-encoded salt (32 bytes) */ public static String getBlowfishSalt() { if (securityProperties == null) { loadSecurityProperties(); } String salt = securityProperties.getProperty(BLOWFISH_SALT); if (salt == null || salt.trim().isEmpty()) { // Generate a new random salt (32 bytes for strong security) byte[] saltBytes = new byte[32]; new java.security.SecureRandom().nextBytes(saltBytes); salt = Base64.getEncoder().encodeToString(saltBytes); securityProperties.setProperty(BLOWFISH_SALT, salt); Log.info("Generated new Blowfish salt for PBKDF2 key derivation"); } return salt; } /** * Gets the Blowfish key derivation function (KDF) type. * Returns "sha1" for legacy single-round SHA1 hashing, or "pbkdf2" for * PBKDF2-HMAC-SHA512 key derivation. * * @return The KDF type ("sha1" or "pbkdf2"), defaults to "sha1" for backward compatibility */ public static String getBlowfishKdf() { if (securityProperties == null) { loadSecurityProperties(); } String kdf = securityProperties.getProperty(BLOWFISH_KDF); // Default to SHA1 for backward compatibility with existing installations return (kdf != null && !kdf.trim().isEmpty()) ? kdf : BLOWFISH_KDF_SHA1; } /** * Sets the Blowfish key derivation function (KDF) type and re-initialises * the property encryptor cache to use the new KDF immediately. * * @param kdf The KDF type ("sha1" or "pbkdf2") */ public static void setBlowfishKdf(String kdf) { if (securityProperties == null) { loadSecurityProperties(); } if (BLOWFISH_KDF_PBKDF2.equalsIgnoreCase(kdf)) { securityProperties.setProperty(BLOWFISH_KDF, BLOWFISH_KDF_PBKDF2); Log.info("Blowfish KDF set to PBKDF2-HMAC-SHA512"); } else { securityProperties.setProperty(BLOWFISH_KDF, BLOWFISH_KDF_SHA1); Log.info("Blowfish KDF set to SHA1 (legacy)"); } // Reinitialise the encryptor cache so new properties use the updated KDF immediately reinitialisePropertyEncryptor(); } /** * Migrates encrypted XML properties (in openfire.xml) from SHA1 to PBKDF2 key derivation. * * This method uses the encryptor swap pattern to re-encrypt properties: * 1. Sets up SHA1 encryptor for decryption and PBKDF2 encryptor for encryption * 2. Reads property names listed as encrypted in security.xml * 3. For each property: reads value from openfire.xml (decrypts with SHA1), * writes back (encrypts with PBKDF2) * 4. Restores original encryptor state * * CRITICAL: This operation cannot be reversed without a backup. * Call this BEFORE updating the KDF setting and BEFORE database migration. * * @return Number of XML properties successfully migrated * @throws IllegalStateException if not using Blowfish encryption or already using PBKDF2 * @throws RuntimeException if migration fails for any property * @since 5.1.0 */ public static int migrateXMLPropertiesFromSHA1ToPBKDF2() { // 1. Verify preconditions String algorithm = getEncryptionAlgorithm(); if (!ENCRYPTION_ALGORITHM_BLOWFISH.equalsIgnoreCase(algorithm)) { throw new IllegalStateException("Cannot migrate: encryption algorithm is " + algorithm + ", not Blowfish"); } String currentKdf = getBlowfishKdf(); if (BLOWFISH_KDF_PBKDF2.equalsIgnoreCase(currentKdf)) { throw new IllegalStateException("Cannot migrate: already using PBKDF2"); } // 2. Get the master encryption key String masterKey = getMasterEncryptionKey(); // 3. Save current encryptor state Encryptor originalEncryptor = propertyEncryptor; Encryptor originalEncryptorNew = propertyEncryptorNew; try { // 4. Create SHA1 encryptor (for decryption) and PBKDF2 encryptor (for encryption) Blowfish sha1Blowfish = new Blowfish(); sha1Blowfish.setKey(masterKey, BLOWFISH_KDF_SHA1); Blowfish pbkdf2Blowfish = new Blowfish(); pbkdf2Blowfish.setKey(masterKey, BLOWFISH_KDF_PBKDF2); // 5. Swap encryptors: old for decrypt, new for encrypt propertyEncryptor = sha1Blowfish; propertyEncryptorNew = pbkdf2Blowfish; // 6. Get list of encrypted property names from security.xml if (securityProperties == null) { loadSecurityProperties(); } List encryptedPropertyNames = securityProperties.getProperties(ENCRYPTED_PROPERTY_NAMES, true); Log.info("Starting XML property migration: {} properties to process", encryptedPropertyNames.size()); // 7. Migrate each property (values stored in openfire.xml) int migrated = 0; int skipped = 0; for (String propertyName : encryptedPropertyNames) { // Get decrypts with SHA1 (via propertyEncryptor) String plaintext = getXMLProperty(propertyName); if (plaintext == null || plaintext.isEmpty()) { // Property doesn't exist or is empty in openfire.xml - skip Log.debug("Skipping empty XML property: {}", propertyName); skipped++; continue; } // Set encrypts with PBKDF2 (via propertyEncryptorNew) setXMLProperty(propertyName, plaintext); migrated++; Log.debug("Migrated XML property: {}", propertyName); } Log.info("XML property migration complete: {} migrated, {} skipped (empty)", migrated, skipped); return migrated; } finally { // 9. Restore original encryptor state propertyEncryptor = originalEncryptor; propertyEncryptorNew = originalEncryptorNew; } } /** * Returns the count of encrypted properties in openfire.xml that have values. * This counts only properties from security.xml that exist in openfire.xml * with non-empty values (i.e., properties that will actually be migrated). * * @return Number of encrypted properties with values in openfire.xml * @since 5.1.0 */ public static int getEncryptedXMLPropertyValueCount() { if (securityProperties == null) { loadSecurityProperties(); } List encryptedPropertyNames = securityProperties.getProperties(ENCRYPTED_PROPERTY_NAMES, true); if (encryptedPropertyNames == null) { return 0; } int count = 0; for (String propertyName : encryptedPropertyNames) { String rawValue = openfireProperties.getProperty(propertyName); if (rawValue != null && !rawValue.isEmpty()) { count++; } } return count; } /** * Checks whether Blowfish encryption migration from SHA1 to PBKDF2 is needed. * Returns true if the server is currently using Blowfish encryption with the * legacy SHA1 key derivation function. * * @return true if migration from SHA1 to PBKDF2 is needed, false otherwise * @since 5.1.0 */ public static boolean isBlowfishMigrationNeeded() { String encryptionAlgorithm = getEncryptionAlgorithm(); if (!ENCRYPTION_ALGORITHM_BLOWFISH.equalsIgnoreCase(encryptionAlgorithm)) { return false; } String kdf = getBlowfishKdf(); return BLOWFISH_KDF_SHA1.equalsIgnoreCase(kdf); } /** * Returns the encryption algorithm configured in security.xml. * * @return The encryption algorithm ("AES" or "Blowfish"), defaults to "Blowfish" if not configured * @since 5.1.0 */ public static String getEncryptionAlgorithm() { if (securityProperties == null) { loadSecurityProperties(); } String algorithm = securityProperties.getProperty(ENCRYPTION_ALGORITHM); return (algorithm != null && !algorithm.trim().isEmpty()) ? algorithm : ENCRYPTION_ALGORITHM_BLOWFISH; } /** * Re-initialises the property encryptor with the current encryption settings. * Call this after changing the encryption algorithm or KDF to ensure new * properties are encrypted with the updated settings without requiring a restart. * * @since 5.1.0 */ public static void reinitialisePropertyEncryptor() { String algorithm = getEncryptionAlgorithm(); String key = getMasterEncryptionKey(); propertyEncryptor = getEncryptor(algorithm, key); propertyEncryptorNew = propertyEncryptor; Log.info("Property encryptor reinitialised with algorithm: {}", algorithm); } /** * Re-encrypted with a new key and new algorithm configuration * * @param newAlg new algorithm type * @param newKey new encryptor key */ private static void updateEncryptionProperties(String newAlg, String newKey) { // load DB properties using the current key if(properties == null) { properties = JiveProperties.getInstance(); } //create the new encryptor currentKey = newKey.isEmpty() ? null : newKey; propertyEncryptorNew = getEncryptor(newAlg, newKey); // Use new key to update configuration properties Iterator> iterator = properties.entrySet().iterator(); Entry entry; String name; while(iterator.hasNext()){ entry = iterator.next(); name = entry.getKey(); // only need to update the encrypted ones if (isPropertyEncrypted(name)) { properties.put(name, entry.getValue()); } } // Update encryption properties to XML, using new encryption key for (String propertyName : securityProperties.getProperties(ENCRYPTED_PROPERTY_NAMES, true)) { String xmlProperty = getXMLProperty(propertyName); // update xml prop if(StringUtils.isNotEmpty(xmlProperty)){ Log.info("Updating encrypted value for " + propertyName); setXMLProperty(propertyName, xmlProperty); } } // Two encryptors are now the same propertyEncryptor = propertyEncryptorNew; } /** * Allows the name of the local config file name to be changed. The * default is "openfire.xml". * * @param configName the name of the config file. */ public static void setConfigName(String configName) { JIVE_CONFIG_FILENAME = configName; } /** * Returns the name of the local config file. * * @return the name of the config file. */ public static String getConfigName() { return JIVE_CONFIG_FILENAME; } /** * Returns the name of the local security config file. * * @return the name of the security config file. */ public static String getSecurityConfigName() { return JIVE_SECURITY_FILENAME; } public static Path getConfigLocation() { return home.resolve(JIVE_CONFIG_FILENAME); } public static Path getSecurityConfigLocation() { return home.resolve(JIVE_SECURITY_FILENAME); } /** * Returns true if in setup mode. A false value means that setup has been completed * or that a connection to the database was possible to properties stored in the * database can be retrieved now. The latter means that once the database settings * during the setup was done a connection to the database should be available thus * properties stored from a previous setup will be available. * * @return true if in setup mode. */ private static boolean isSetupMode() { if (Boolean.parseBoolean(JiveGlobals.getXMLProperty("setup"))) { return false; } // Check if the DB configuration is done if (DbConnectionManager.getConnectionProvider() == null) { // DB setup is still not completed so setup is needed return true; } Connection con = null; PreparedStatement pstmt = null; try { con = DbConnectionManager.getConnection(); // Properties can now be loaded from DB so consider setup done } catch (SQLException e) { // Properties cannot be loaded from DB so do not consider setup done return true; } finally { DbConnectionManager.closeConnection(pstmt, con); } return false; } /** * Loads Openfire properties if necessary. Property loading must be done lazily so * that we give outside classes a chance to set {@code home}. */ private synchronized static void loadOpenfireProperties() { if (openfireProperties == null) { // If home is null then log that the application will not work correctly if (home == null) { if (!failedLoading) { failedLoading = true; System.err.println("Critical Error! The home directory has not been configured, \n" + "which will prevent the application from working correctly.\n\n"); } } // Create a manager with the full path to the Openfire config file. else { try { openfireProperties = new XMLProperties(home.resolve(getConfigName())); } catch (IOException ioe) { Log.error("Unable to load default Openfire properties from: {}{}{}", home, File.separator, getConfigName(), ioe); failedLoading = true; } } // create a default/empty XML properties set (helpful for unit testing) if (openfireProperties == null) { try { Log.warn("Default properties have not been loaded from file. Using a non-persisting dummy properties object."); openfireProperties = XMLProperties.getNonPersistedInstance(); } catch (IOException e) { Log.error("Failed to setup default openfire properties", e); } } } } /** * Lazy-loads the security configuration properties. */ private synchronized static void loadSecurityProperties() { if (securityProperties == null) { // If home is null then log that the application will not work correctly if (home == null) { if (!failedLoading) { failedLoading = true; System.err.println("Critical Error! The home directory has not been configured, \n" + "which will prevent the application from working correctly.\n\n"); } } // Create a manager with the full path to the security XML file. else { try { securityProperties = new XMLProperties(home.resolve(JIVE_SECURITY_FILENAME)); setupPropertyEncryption(); // Migrate all secure XML properties into the database automatically for (String propertyName : securityProperties.getAllPropertyNames()) { if (!propertyName.startsWith(ENCRYPTED_PROPERTY_NAME_PREFIX)) { setPropertyEncrypted(propertyName, true); securityProperties.migrateProperty(propertyName); } } } catch (IOException ioe) { Log.error("Unable to load default security properties from: {}{}{}", home, File.separator, getConfigName(), ioe); failedLoading = true; } } // create a default/empty XML properties set (helpful for unit testing) if (securityProperties == null) { try { Log.warn("Security properties have not been loaded from file. Using a non-persisting dummy properties object."); securityProperties = XMLProperties.getNonPersistedInstance(); } catch (IOException e) { Log.error("Failed to setup default security properties", e); } } } } /** * Setup the property encryption key, rewriting encrypted values as appropriate */ private static void setupPropertyEncryption() { // get/set the current encryption key currentKey = getCurrentKey(); // check to see if a new key has been defined String newKey = securityProperties.getProperty(ENCRYPTION_KEY_NEW, false); if (newKey != null) { Log.info("Detected new encryption key; updating encrypted properties"); // if a new key has been provided, check to see if the old key matches // the current key, otherwise log an error and ignore the new key String oldKey = securityProperties.getProperty(ENCRYPTION_KEY_OLD); if (oldKey == null) { if (currentKey != null) { Log.warn("Old encryption key was not provided; ignoring new encryption key"); return; } } else { if (!oldKey.equals(currentKey)) { Log.warn("Old encryption key does not match current encryption key; ignoring new encryption key"); return; } } String oldAlg = securityProperties.getProperty(ENCRYPTION_ALGORITHM); updateEncryptionProperties(oldAlg, newKey); securityProperties.deleteProperty(ENCRYPTION_KEY_NEW); securityProperties.deleteProperty(ENCRYPTION_KEY_OLD); } // (re)write the encryption key to the security XML file (obfuscated, not encrypted) securityProperties.setProperty(ENCRYPTION_KEY_CURRENT, new Obfuscator().obfuscate(currentKey)); // Initialise Blowfish KDF for new installations initializeBlowfishKdf(); } /** * Initialises the Blowfish key derivation function (KDF) for new installations. * For fresh installations (setup not complete), sets PBKDF2 as the default KDF. * For existing installations, preserves the current KDF (SHA1 or PBKDF2) and logs appropriate messages. * * This method is called once during security properties initialization. It determines whether this * is a new or existing installation by checking if setup has been completed. This ensures new * installations use the stronger PBKDF2-HMAC-SHA512 key derivation whilst existing installations * maintain backward compatibility with their current configuration. */ private static void initializeBlowfishKdf() { String currentKdf = securityProperties.getProperty(BLOWFISH_KDF, false); if (currentKdf == null || currentKdf.trim().isEmpty()) { // No KDF configured yet - determine if this is a new or existing installation if (isSetupMode()) { // New installation (setup not complete): set PBKDF2 as default securityProperties.setProperty(BLOWFISH_KDF, BLOWFISH_KDF_PBKDF2); // Salt will be auto-generated when first accessed by getBlowfishSalt() Log.info("New installation detected: Blowfish KDF set to PBKDF2-HMAC-SHA512"); } else { // Existing installation (setup complete): keep SHA1 for backward compatibility // Don't set the property - getBlowfishKdf() will default to SHA1 Log.warn("Existing installation detected with no Blowfish KDF configured. " + "Defaulting to legacy SHA1 for backward compatibility. " + "Consider migrating to PBKDF2 via the admin console for improved security."); } } else if (BLOWFISH_KDF_SHA1.equalsIgnoreCase(currentKdf)) { // Existing installation explicitly using SHA1 Log.warn("Blowfish is using legacy SHA1 key derivation. " + "Consider migrating to PBKDF2 via the admin console for improved security."); } else if (BLOWFISH_KDF_PBKDF2.equalsIgnoreCase(currentKdf)) { // Already using PBKDF2 Log.info("Blowfish is using PBKDF2-HMAC-SHA512 key derivation"); } } public static final String[] setupExcludePaths = { "setup/index.jsp", "setup/setup-admin-settings.jsp", "setup/setup-completed.jsp", "setup/setup-datasource-jndi.jsp", "setup/setup-datasource-settings.jsp", "setup/setup-datasource-standard.jsp", "setup/setup-finished.jsp", "setup/setup-host-settings.jsp", "setup/setup-ldap-group.jsp", "setup/setup-ldap-server.jsp", "setup/setup-ldap-user.jsp", "setup/setup-profile-settings.jsp" }; }