001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 * 
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 * 
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 * 
019 */
020package org.apache.directory.server.kerberos.shared.replay;
021
022
023import java.io.Serializable;
024import java.time.Duration;
025
026import javax.security.auth.kerberos.KerberosPrincipal;
027
028import org.apache.directory.shared.kerberos.KerberosTime;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import com.github.benmanes.caffeine.cache.Cache;
033import com.github.benmanes.caffeine.cache.Caffeine;
034
035
036/**
037 * "The replay cache will store at least the server name, along with the client name,
038 * time, and microsecond fields from the recently-seen authenticators, and if a
039 * matching tuple is found, the KRB_AP_ERR_REPEAT error is returned."
040 * 
041 * We will store the entries in Ehacache instance
042 * 
043 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
044 */
045public class ReplayCacheImpl implements ReplayCache
046{
047
048    private static final Logger LOG = LoggerFactory.getLogger( ReplayCacheImpl.class );
049
050    /** Caffeine based storage to store the entries */
051    Cache<String, Object> cache;
052
053    /** default clock skew */
054    private static final long DEFAULT_CLOCK_SKEW = 5L * KerberosTime.MINUTE;
055
056    /** The clock skew */
057    private long clockSkew = DEFAULT_CLOCK_SKEW;
058
059    /**
060     * A structure to hold an entry
061     */
062    public static class ReplayCacheEntry implements Serializable
063    {
064        private static final long serialVersionUID = 1L;
065
066        /** The server principal */
067        private KerberosPrincipal serverPrincipal;
068
069        /** The client principal */
070        private KerberosPrincipal clientPrincipal;
071
072        /** The client time */
073        private KerberosTime clientTime;
074
075        /** The client micro seconds */
076        private int clientMicroSeconds;
077
078
079        /**
080         * Creates a new instance of ReplayCacheEntry.
081         * 
082         * @param serverPrincipal
083         * @param clientPrincipal
084         * @param clientTime
085         * @param clientMicroSeconds
086         */
087        public ReplayCacheEntry( KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal,
088            KerberosTime clientTime, int clientMicroSeconds )
089        {
090            this.serverPrincipal = serverPrincipal;
091            this.clientPrincipal = clientPrincipal;
092            this.clientTime = clientTime;
093            this.clientMicroSeconds = clientMicroSeconds;
094        }
095
096
097        /**
098         * Returns whether this {@link ReplayCacheEntry} is equal to another {@link ReplayCacheEntry}.
099         * {@link ReplayCacheEntry}'s are equal when the server name, client name, client time, and
100         * the client microseconds are equal.
101         *
102         * @param that
103         * @return true if the ReplayCacheEntry's are equal.
104         */
105        public boolean equals( ReplayCacheEntry that )
106        {
107            return serverPrincipal.equals( that.serverPrincipal ) && clientPrincipal.equals( that.clientPrincipal )
108                && clientTime.equals( that.clientTime ) && clientMicroSeconds == that.clientMicroSeconds;
109        }
110
111
112        /**
113         * Returns whether this {@link ReplayCacheEntry} is older than a given time.
114         *
115         * @param clockSkew
116         * @return true if the {@link ReplayCacheEntry}'s client time is outside the clock skew time.
117         */
118        public boolean isOutsideClockSkew( long clockSkew )
119        {
120            return !clientTime.isInClockSkew( clockSkew );
121        }
122
123
124        /**
125         * @return create a key to be used while storing in the cache
126         */
127        private String createKey()
128        {
129            StringBuilder sb = new StringBuilder();
130            sb.append( ( clientPrincipal == null ) ? "null" : clientPrincipal.getName() );
131            sb.append( '#' );
132            sb.append( ( serverPrincipal == null ) ? "null" : serverPrincipal.getName() );
133            sb.append( '#' );
134            sb.append( ( clientTime == null ) ? "null" : clientTime.getDate() );
135            sb.append( '#' );
136            sb.append( clientMicroSeconds );
137
138            return sb.toString();
139        }
140    }
141
142
143    /**
144     * Creates a new instance of InMemoryReplayCache. Sets the
145     * delay between each cleaning run to 5 seconds. Sets the
146     * clockSkew to the given value
147     * 
148     * @param clockSkew the allowed skew (milliseconds)
149     */
150    public ReplayCacheImpl( long clockSkew )
151    {
152        this.clockSkew = clockSkew;
153        this.cache = Caffeine.newBuilder().expireAfterWrite( Duration.ofMillis( clockSkew )).build();
154    }
155
156
157    /**
158     * Check if an entry is a replay or not.
159     */
160    public synchronized boolean isReplay( KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal,
161        KerberosTime clientTime, int clientMicroSeconds )
162    {
163        ReplayCacheEntry entry = new ReplayCacheEntry( serverPrincipal, 
164            clientPrincipal, clientTime, clientMicroSeconds );
165        ReplayCacheEntry found = ( ReplayCacheEntry ) cache.getIfPresent( entry.createKey() );
166
167        if ( found == null )
168        {
169            return false;
170        }
171
172        entry = found;
173
174        return serverPrincipal.equals( entry.serverPrincipal ) &&
175            clientTime.equals( entry.clientTime ) &&
176            ( clientMicroSeconds == entry.clientMicroSeconds );
177    }
178
179
180    /**
181     * Add a new entry into the cache. A thread will clean all the timed out
182     * entries.
183     */
184    public synchronized void save( KerberosPrincipal serverPrincipal, KerberosPrincipal clientPrincipal,
185        KerberosTime clientTime, int clientMicroSeconds )
186    {
187        ReplayCacheEntry entry = new ReplayCacheEntry( serverPrincipal, clientPrincipal, clientTime, clientMicroSeconds );
188
189        cache.put( entry.createKey(), entry );
190    }
191
192
193    /**
194     * {@inheritDoc}
195     */
196    public void clear()
197    {
198        LOG.debug( "removing all the elements from cache" );
199        cache.invalidateAll();
200    }
201}