001    /*
002     * Created by IntelliJ IDEA.
003     * User: d@hd.org
004     * Date: 27-May-02
005     * Time: 23:23:08
006    
007    Copyright (c) 1996-2012, Damon Hart-Davis
008    All rights reserved.
009    
010    Redistribution and use in source and binary forms, with or without
011    modification, are permitted provided that the following conditions are
012    met:
013    
014      * Redistributions of source code must retain the above copyright
015        notice, this list of conditions and the following disclaimer.
016    
017      * Redistributions in binary form must reproduce the above copyright
018        notice, this list of conditions and the following disclaimer in the
019        documentation and/or other materials provided with the
020        distribution.
021    
022    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
023    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
024    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
025    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
026    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
027    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
028    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
029    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
030    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
032    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033    
034     */
035    package org.hd.d.pg2k.svrCore.props;
036    
037    import java.io.BufferedInputStream;
038    import java.io.File;
039    import java.io.FileInputStream;
040    import java.io.IOException;
041    import java.io.InputStream;
042    import java.util.Collections;
043    import java.util.Enumeration;
044    import java.util.HashMap;
045    import java.util.Map;
046    import java.util.Properties;
047    
048    import org.hd.d.pg2k.svrCore.CoreConsts;
049    import org.hd.d.pg2k.svrCore.ExhibitName;
050    import org.hd.d.pg2k.svrCore.FileTools;
051    
052    /**Encapsulates the simplepass password file.
053     * This can not be serialised and can only be constructed
054     * from the file indicated by LocalProps.getSimplePassfile().
055     */
056    public final class SimplepassProps
057        {
058        /**Returns true if the quoted password is correct for the given author.
059         * This can only return true if simplepass passwords are enabled
060         * and are set up correctly in LocalProps and in a simplepass file.
061         * <p>
062         * Will return false if either parameter is null or zero-length.
063         *
064         * @param auth  the author initials; must be correct syntactically
065         * @param pass  the password; must be of a valid length
066         * @return true if the quoted password is correct
067         */
068        public final static boolean isAuthorUploadPasswordCorrect(final String auth,
069                                                                  final String pass)
070            {
071            final SimplepassProps spp = getSimplepassProps();
072            // If no simplepass, always fail.
073            if(spp == null) { return(false); }
074            // Check against actual passwords.
075            return(spp.authorUploadPasswordCorrect(auth, pass));
076            }
077    
078        /**The current singleton SimplepassProps object, if any.
079         * This may be null if USE_SIMPLEPASS is null or if there
080         * is no simplepass value set in LocalProps or there is no file.
081         */
082        private static SimplepassProps singleton;
083    
084        /**Get the current SimplepassProps object, if any.
085         * This object normally exists as a singleton,
086         * being replaced only if the simplepass file changes
087         * and can be read.
088         */
089        private static synchronized SimplepassProps getSimplepassProps()
090            {
091            // If simple passwords are enabled, we don't create a singleton.
092            if(!USE_SIMPLEPASS) { return(null); }
093    
094            final String passfileName = LocalProps.getSimplePassfile();
095            if(passfileName != null)
096                {
097                final File pf = new File(passfileName);
098                final long timestamp = pf.lastModified();
099    
100                if(pf.isFile())
101                    {
102                    // If the singleton object may be out of sync,
103                    // try to create a new one.
104                    if((singleton == null) || (singleton.timestamp != timestamp))
105                        {
106                        try { singleton = new SimplepassProps(pf); }
107                        catch(final IOException e)
108                            {
109                            // In case of error, dump the problem.
110                            e.printStackTrace();
111                            }
112                        }
113                    }
114                }
115    
116            return(singleton);
117            }
118    
119        /**Construct a new, immutable, properties set.
120         * The properties must be non-null
121         * and the timestamp must be non-negative.
122         * <p>
123         * Minor problems with the properties themselves will
124         * be silently ignored, and defaults substituted
125         * for broken or missing values.
126         * <p>
127         * This is only designed to be fetched by the factory method.
128         *
129         * @param pf  is the passfile
130         */
131        private SimplepassProps(final File pf)
132            throws IOException
133            {
134            Properties props;
135    
136            // Read the properties file in buffered chunks for efficiency.
137            final InputStream is = new BufferedInputStream(
138                new FileInputStream(pf));
139            try { props = FileTools.loadProperties(is); }
140            finally { is.close(); } // Release the handle ASAP...
141    
142            // Capture the timestamp.
143            timestamp = pf.lastModified();
144    
145            // Parse properties.
146    
147            // Set up simplepassMap.
148            if(!USE_SIMPLEPASS) { simplepassMap = null; }
149            else
150                {
151                // Use a HashMap for speed of lookup,
152                // though speed is probably not critical.
153                final HashMap<String,String> hm = new HashMap<String,String>();
154                for(final Enumeration en = props.propertyNames(); en.hasMoreElements(); )
155                    {
156                    final String name = (String) en.nextElement();
157                    if(name.startsWith(SIMPLEPASS_PREFIX))
158                        {
159                        // Extract author initials and ensure that they are valid.
160                        final String author =
161                            name.substring(SIMPLEPASS_PREFIX.length());
162                        if(!ExhibitName.validAuthorSyntax(author))
163                            {
164                            System.err.println("ERROR: invalid simplepass author in: "+name);
165                            continue;
166                            }
167                        final String pass = props.getProperty(name);
168                        // We could validate the password syntax/length here...
169                        if((pass.length() < CoreConsts.MIN_PASSWORD_LEN) ||
170                           (pass.length() > CoreConsts.MAX_PASSWORD_LEN))
171                            {
172                            System.err.println("ERROR: invalid simplepass password in: "+name);
173                            continue;
174                            }
175                        if(hm.put(author, pass) != null)
176                            {
177                            System.err.println("ERROR: duplicate simplepass author in: "+name);
178                            continue;
179                            }
180                        }
181                    }
182                // Save immutable reference to map.
183                simplepassMap = Collections.unmodifiableMap(hm);
184                }
185            }
186    
187        /**Timestamp of this properties set. */
188        public final long timestamp;
189    
190    
191        /**If true, allow use of pg2k.upload.simplepass.XXX simple passwords.
192         * This is a primative password system for author exhibit uploads.
193         * <p>
194         * These will only be used in any case if a more advanced and safer
195         * password store is not available.
196         * <p>
197         * Note that these passwords are:
198         * <ul>
199         * <li>plaintext
200         * <li>not maintainable/changeable by the user
201         * <li>only updatable slowly (when the props are re-read)
202         * </ul>
203         * so should be considered generally unacceptable for normal use.
204         */
205        private static final boolean USE_SIMPLEPASS = true;
206    
207        /**The "simplepass" prefix, including the trailing dot.
208         * This is followed by the author initials with the value
209         * being the password.  The password value must meet constraints
210         * set elsewhere (in the code) such as maximum and minimum length.
211         */
212        private static final String SIMPLEPASS_PREFIX = "pass.";
213    
214        /**The immutable private map from author initials to plaintext password.
215         * It is a shame that these will be floating around in the heap
216         * unprotected.
217         * <p>
218         * This map is threadsafe because it is read-only.
219         * <p>
220         * The map is String to String.
221         * <p>
222         * Will be null if USE_SIMPLEPASS is false.
223         * <p>
224         * We don't directly disclose passwords, just check if they are valid
225         * or not.  We hold a lock on the password while checking
226         * a password, and if it is wrong we wait a while still holding
227         * the lock, to make dictionary attacks harder at the risk of making
228         * some denial of service attacks easier.
229         */
230        private final Map<String,String> simplepassMap;
231    
232        /**How long to wait (ms) on an unsuccessful lookup. */
233        private static final int SIMPLEPASS_FAILURE_PAUSE = 2000;
234    
235        /**Returns true if presented author upload password is correct.
236         * The author name must be valid and the password non-null and
237         * non-zero-length to avoid trivial typo-related attacks.
238         */
239        public final boolean authorUploadPasswordCorrect(final String auth, final String pass)
240            {
241            if(!ExhibitName.validAuthorSyntax(auth)) { return(false); }
242            if((pass == null) || (pass.length() == 0)) { return(false); }
243    
244            final String p = simplepassMap.get(auth);
245    
246            try {
247                // If author not present at all, just wait.
248                if(p == null)
249                    { Thread.sleep(SIMPLEPASS_FAILURE_PAUSE); }
250                else
251                    {
252                        synchronized(pass)
253                            {
254                            if(p.equals(pass)) { return(true); } // Correct password!
255                            Thread.sleep(SIMPLEPASS_FAILURE_PAUSE);
256                            }
257                        }
258                }
259            catch(final InterruptedException e) { } // Ignore.
260    
261            return(false); // Wrong password.
262            }
263        }