001    /*
002     * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org
003     * Copyright (C) 2011 NightLabs Consulting GmbH
004     *
005     * This program is free software: you can redistribute it and/or modify
006     * it under the terms of the GNU Affero General Public License as
007     * published by the Free Software Foundation, either version 3 of the
008     * License, or (at your option) any later version.
009     *
010     * This program is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU Affero General Public License for more details.
014     *
015     * You should have received a copy of the GNU Affero General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    package org.cumulus4j.keystore;
019    
020    import java.io.IOException;
021    import java.util.Date;
022    import java.util.List;
023    
024    import org.cumulus4j.keystore.prop.Long2LongSortedMapProperty;
025    import org.cumulus4j.keystore.prop.Property;
026    import org.slf4j.Logger;
027    import org.slf4j.LoggerFactory;
028    
029    /**
030     * <p>
031     * Key management strategy determining the currently active encryption key by the current time.
032     * </p><p>
033     * See <a target="_blank" href="../../../documentation/date-dependent-key-strategy.html">Date-dependent key-strategy</a> for further
034     * details.
035     * </p>
036     *
037     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
038     */
039    public class DateDependentKeyStrategy
040    {
041            private static final Logger logger = LoggerFactory.getLogger(DateDependentKeyStrategy.class);
042    
043            private KeyStore keyStore;
044    
045            /**
046             * Name of the {@link Property} where the key-strategy's timestamp-to-key-map is stored.
047             * The property is of type {@link Long2LongSortedMapProperty}.
048             */
049            public static final String PROPERTY_ACTIVE_FROM_TIMESTAMP_2_KEY_ID = "DateDependentKeyStrategy.activeFromTimestamp2KeyID";
050    
051            /**
052             * Create a new instance for the given {@link KeyStore}.
053             * @param keyStore the <code>KeyStore</code> to work with. Must not be <code>null</code>.
054             */
055            public DateDependentKeyStrategy(KeyStore keyStore)
056            {
057                    if (keyStore == null)
058                            throw new IllegalArgumentException("keyStore == null");
059    
060                    this.keyStore = keyStore;
061            }
062    
063            /**
064             * Get the {@link KeyStore} that was passed to {@link #DateDependentKeyStrategy(KeyStore)}.
065             * @return the <code>KeyStore</code> this strategy instance works with. Never <code>null</code>.
066             */
067            public KeyStore getKeyStore() {
068                    return keyStore;
069            }
070    
071            /**
072             * <p>
073             * Initialise an {@link KeyStore#isEmpty() empty} <code>KeyStore</code>.
074             * </p><p>
075             * This initialisation consists of creating a user and a few (thousand) keys. How many keys,
076             * depends on the parameters <code>keyActivityPeriodMSec</code> and <code>keyStorePeriodMSec</code>.
077             * The keys are added to a {@link Long2LongSortedMapProperty} (i.e. a <code>Map</code>) with the
078             * key being the "from-timestamp" and the value being the key-ID. The "from-timestamp" is the time
079             * (including) from which on the key will be used as "active encryption key". The "active encryption
080             * key" is the key, that will be used for encryption in the app-server at a certain moment in time.
081             * </p>
082             *
083             * @param userName the initial user to be created.
084             * @param password the password for the initial user.
085             * @param keyActivityPeriodMSec how long (in millisec) should each key be valid. If &lt; 1, the
086             * default value of 24 hours (= 86400000 msec) will be used.
087             * @param keyStorePeriodMSec how long should the key store have fresh, unused keys. This number
088             * divided by the <code>keyActivityPeriodMSec</code> determines, how many keys must be generated.
089             * If &lt; 1, the default value of 50 years (50 * 365 days - ignoring leap years!) will be used.
090             * @throws IOException if writing to the key-store-file failed.
091             * @throws KeyStoreNotEmptyException if the <code>KeyStore</code> is not {@link KeyStore#isEmpty() empty}.
092             */
093            public void init(String userName, char[] password, long keyActivityPeriodMSec, long keyStorePeriodMSec)
094            throws IOException, KeyStoreNotEmptyException
095            {
096                    if (!keyStore.isEmpty())
097                            throw new IllegalStateException("Key store is not empty! Cannot initialise!");
098    
099                    if (keyActivityPeriodMSec < 1)
100                            keyActivityPeriodMSec = 24L * 3600L * 1000L;
101    
102                    if (keyStorePeriodMSec < 1)
103                            keyStorePeriodMSec = 50L * 365L * 24L * 3600L * 1000L;
104    
105                    long numberOfKeysToGenerate = 1 + keyStorePeriodMSec / keyActivityPeriodMSec;
106                    logger.debug("init: Calculated numberOfKeysToGenerate={}", numberOfKeysToGenerate);
107    
108                    if (numberOfKeysToGenerate > Integer.MAX_VALUE)
109                            throw new KeyStoreNotEmptyException("Calculated numberOfKeysToGenerate=" + numberOfKeysToGenerate + " is out of range! Maximum allowed value is " + Integer.MAX_VALUE + "! Reduce keyStorePeriodMSec or increase keyActivityPeriodMSec!");
110    
111                    try {
112                            keyStore.createUser(null, null, userName, password);
113                    } catch (AuthenticationException e) {
114                            throw new RuntimeException(e);
115                    } catch (UserAlreadyExistsException e) {
116                            throw new RuntimeException(e);
117                    }
118    
119                    String authUserName = userName;
120                    char[] authPassword = password;
121    
122                    try {
123                            List<GeneratedKey> generatedKeys = keyStore.generateKeys(authUserName, authPassword, (int)numberOfKeysToGenerate);
124                            long activeFromTimestamp = System.currentTimeMillis();
125                            Long2LongSortedMapProperty activeFromTimestamp2KeyIDMapProperty = keyStore.getProperty(authUserName, authPassword, Long2LongSortedMapProperty.class, PROPERTY_ACTIVE_FROM_TIMESTAMP_2_KEY_ID);
126                            for (GeneratedKey generatedKey : generatedKeys) {
127                                    activeFromTimestamp2KeyIDMapProperty.getValue().put(activeFromTimestamp, generatedKey.getKeyID());
128                                    // calculate next validFromTimestamp
129                                    activeFromTimestamp += keyActivityPeriodMSec;
130                            }
131    
132                            // Put -1 as END marker.
133                            activeFromTimestamp2KeyIDMapProperty.getValue().put(activeFromTimestamp, -1L);
134    
135                            // Write the property.
136                            keyStore.setProperty(authUserName, authPassword, activeFromTimestamp2KeyIDMapProperty);
137                    } catch (AuthenticationException e) { // We just created this user - if that exception really occurs here, it's definitely a RuntimeException.
138                            throw new RuntimeException(e);
139                    }
140            }
141    
142    //      /**
143    //       * Get the timestamp till when the active key will be valid (excluding). This
144    //       * is a convenience method delegating to {@link #getActiveKeyValidUntilExcl(String, char[], Date)}
145    //       * with the argument <code>timestamp</code> being <code>null</code>.
146    //       *
147    //       * @param authUserName the authenticated user authorizing this action.
148    //       * @param authPassword the password for authenticating the user specified by <code>authUserName</code>.
149    //       * @return the timestamp at which the current active key will stop being active.
150    //       * @throws AuthenticationException if the specified <code>authUserName</code> does not exist or the specified <code>authPassword</code>
151    //       * is not correct for the given <code>authUserName</code>.
152    //       */
153    //      public Date getActiveKeyValidUntilExcl(String authUserName, char[] authPassword) throws AuthenticationException
154    //      {
155    //              return getActiveKeyValidUntilExcl(authUserName, authPassword, null);
156    //      }
157    
158    //      /**
159    //       * Get the timestamp till when the active key will be valid (excluding). The active key is
160    //       * determined based on the given <code>timestamp</code> (which can be <code>null</code> to mean 'now').
161    //       *
162    //       * @param authUserName the authenticated user authorizing this action.
163    //       * @param authPassword the password for authenticating the user specified by <code>authUserName</code>.
164    //       * @param timestamp the timestamp specifying the time at which the queried key is / was / will be active.
165    //       * Can be <code>null</code>, which is interpreted as NOW.
166    //       * @return the timestamp at which the given key is valid. This is always after the given timestamp (or now, if <code>timestamp == null</code>),
167    //       * even if there is no key with this validity, because the key-store is extrapolated if necessary (in an eternal cycle).
168    //       * <p>
169    //       * For example, if the key-store was generated with daily (24 h) key-rotation and keys for the time range from
170    //       * 2011-01-01 00:00 [including] until 2011-04-01 00:00 [excluding]
171    //       * and the given timestamp is 2011-05-01 23:45, the active key will be exactly the same as it was 2011-02-01 23:45. Though
172    //       * this key originally was valid only till 2011-02-02 00:00 [excluding], the result of this method would now be 2011-05-02 00:00.
173    //       * </p>
174    //       * @throws AuthenticationException if the specified <code>authUserName</code> does not exist or the specified <code>authPassword</code>
175    //       * is not correct for the given <code>authUserName</code>.
176    //       */
177    //      public Date getActiveKeyValidUntilExcl(String authUserName, char[] authPassword, Date timestamp) throws AuthenticationException
178    //      {
179    //              return new Date(determineActiveKeyIDAndValidFromAndValidTo(authUserName, authPassword, timestamp)[2]);
180    //      }
181    
182    //      /**
183    //       * Get the currently active key's ID. This
184    //       * is a convenience method delegating to {@link #getActiveKeyID(String, char[], Date)}
185    //       * with the argument <code>timestamp</code> being <code>null</code>.
186    //       *
187    //       * @param authUserName the authenticated user authorizing this action.
188    //       * @param authPassword the password for authenticating the user specified by <code>authUserName</code>.
189    //       * @return
190    //       * @throws AuthenticationException if the specified <code>authUserName</code> does not exist or the specified <code>authPassword</code>
191    //       * is not correct for the given <code>authUserName</code>.
192    //       */
193    //      public long getActiveKeyID(String authUserName, char[] authPassword)
194    //      throws AuthenticationException
195    //      {
196    //              return getActiveKeyID(authUserName, authPassword, null);
197    //      }
198    
199            /**
200             * <p>
201             * Get the details of the key which is / was / will be active at the given <code>timestamp</code>.
202             * </p>
203             * @param authUserName the authenticated user authorizing this action.
204             * @param authPassword the password for authenticating the user specified by <code>authUserName</code>.
205             * @param timestamp the timestamp at which the active key should be determined. If <code>null</code>, NOW (<code>new Date()</code>) is assumed.
206             * @return the active key at the given <code>timestamp</code>.
207             * @throws AuthenticationException if the specified <code>authUserName</code> does not exist or the specified <code>authPassword</code>
208             * is not correct for the given <code>authUserName</code>.
209             */
210            public ActiveKey getActiveKey(String authUserName, char[] authPassword, Date timestamp)
211            throws AuthenticationException
212            {
213                    if (timestamp == null)
214                            timestamp = new Date();
215    
216                    Long2LongSortedMapProperty activeFromTimestamp2KeyIDMapProperty = keyStore.getProperty(authUserName, authPassword, Long2LongSortedMapProperty.class, PROPERTY_ACTIVE_FROM_TIMESTAMP_2_KEY_ID);
217                    if (activeFromTimestamp2KeyIDMapProperty.getValue().isEmpty())
218                            throw new IllegalStateException("There is no property named '" + PROPERTY_ACTIVE_FROM_TIMESTAMP_2_KEY_ID + "'! Obviously the key-store was not initalised for this strategy!");
219    
220                    if (activeFromTimestamp2KeyIDMapProperty.getValue().get(activeFromTimestamp2KeyIDMapProperty.getValue().lastKey()) != -1L)
221                            throw new IllegalStateException("Expected last entry to be the END marker, but it is not!");
222    
223                    long timestampMSec = timestamp.getTime();
224                    if (timestampMSec < activeFromTimestamp2KeyIDMapProperty.getValue().firstKey()) {
225                            logger.warn("getActiveKeyID: timestamp is out of range (before). Will reuse another key via cyclic extrapolation.");
226                            while (timestampMSec < activeFromTimestamp2KeyIDMapProperty.getValue().firstKey())
227                                    timestampMSec += activeFromTimestamp2KeyIDMapProperty.getValue().lastKey() - activeFromTimestamp2KeyIDMapProperty.getValue().firstKey();
228                    }
229    
230                    if (timestampMSec >= activeFromTimestamp2KeyIDMapProperty.getValue().lastKey()) {
231                            logger.warn("getActiveKeyID: timestamp is out of range (after). Will reuse another key via cyclic extrapolation.");
232                            while (timestampMSec >= activeFromTimestamp2KeyIDMapProperty.getValue().lastKey()) {
233                                    timestampMSec -= activeFromTimestamp2KeyIDMapProperty.getValue().lastKey() - activeFromTimestamp2KeyIDMapProperty.getValue().firstKey();
234                            }
235                    }
236    
237                    Long currentActiveFromTimestamp = activeFromTimestamp2KeyIDMapProperty.getValue().headMap(timestampMSec + 1L).lastKey(); // We add 1, because our contract is INCLUSIVE while headMap is EXCLUSIVE.
238                    Long keyID = activeFromTimestamp2KeyIDMapProperty.getValue().get(currentActiveFromTimestamp);
239                    if (keyID == null)
240                            throw new IllegalStateException("keyID == null for currentActiveFromTimestamp == " + currentActiveFromTimestamp);
241    
242                    if (keyID < 0)
243                            throw new IllegalStateException("keyID < 0");
244    
245                    Long currentActiveUntilTimestamp = activeFromTimestamp2KeyIDMapProperty.getValue().tailMap(timestampMSec + 1L).firstKey();
246    
247                    long diff = timestamp.getTime() - timestampMSec;
248    
249                    return new ActiveKey(
250                                    keyID,
251                                    new Date(currentActiveFromTimestamp + diff),
252                                    new Date(currentActiveUntilTimestamp + diff)
253                    );
254            }
255    
256            /**
257             * Descriptor of the active key.
258             *
259             * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
260             */
261            public static final class ActiveKey
262            {
263                    private long keyID;
264                    private Date activeFromIncl;
265                    private Date activeToExcl;
266    
267                    private ActiveKey(long keyID, Date activeFromIncl, Date activeToExcl)
268                    {
269                            this.keyID = keyID;
270                            this.activeFromIncl = activeFromIncl;
271                            this.activeToExcl = activeToExcl;
272                    }
273    
274                    /**
275                     * Get the key's identifier.
276                     * @return the key-ID.
277                     */
278                    public long getKeyID() {
279                            return keyID;
280                    }
281                    /**
282                     * <p>
283                     * Get the timestamp from which on the key is active (including).
284                     * </p>
285                     * <p>
286                     * This timestamp is extrapolated (if necessary) according to the timestamp given to
287                     * {@link DateDependentKeyStrategy#getActiveKey(String, char[], Date)}. Please see
288                     * the documentation of {@link #getActiveToExcl()} for more details about this extrapolation.
289                     * </p>
290                     * @return the timestamp from which on the key is active (including).
291                     */
292                    public Date getActiveFromIncl() {
293                            return activeFromIncl;
294                    }
295    
296                    /**
297                     * <p>
298                     * Get the timestamp until which the key is active (excluding).
299                     * </p>
300                     * <p>
301                     * This is always after (never before, never equal to) the timestamp given
302                     * to {@link DateDependentKeyStrategy#getActiveKey(String, char[], Date)},
303                     * even if there is no key with this validity in the key-store, because the key-store is extrapolated if necessary
304                     * (in an eternal cycle).
305                     * </p>
306                     * <p>
307                     * For example, if the key-store was generated with daily (24 h) key-rotation and keys for the time range from
308                     * 2011-01-01 00:00 [including] until 2011-04-01 00:00 [excluding]
309                     * and the given timestamp is 2011-05-01 23:45, the active key will be exactly the same as it was 2011-02-01 23:45. Though
310                     * this key originally was valid only till 2011-02-02 00:00 [excluding], the result of this method would now be 2011-05-02 00:00.
311                     * </p>
312                     * @return the timestamp until which the key is active (excluding).
313                     */
314                    public Date getActiveToExcl() {
315                            return activeToExcl;
316                    }
317            }
318    }