FELIX-4299 Support encoded passwords and make sure the default
password is written in encoded form in the code such that when
storing such default configuration, no plain text password is written

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1537892 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/ConfigurationSupport.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/ConfigurationSupport.java
index 1bbf26e..eb728c2 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/ConfigurationSupport.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/ConfigurationSupport.java
@@ -20,6 +20,12 @@
 
 
 import java.util.Dictionary;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.ConfigurationException;
 import org.osgi.service.cm.ManagedService;
 
 
@@ -37,8 +43,65 @@
 
     //---------- ManagedService
 
-    public void updated( Dictionary config )
+    public void updated( Dictionary config ) throws ConfigurationException
     {
-        osgiManager.updateConfiguration( config );
+        // validate hashed password
+        if ( isPasswordHashed( config ) )
+        {
+            osgiManager.updateConfiguration( config );
+        }
+        else
+        {
+            // hash the password, update config and wait for the
+            // updated configuration to be supplied later
+            final BundleContext bc = this.osgiManager.getBundleContext();
+            final ServiceReference ref = bc.getServiceReference( ConfigurationAdmin.class.getName() );
+            if ( ref != null )
+            {
+                final ConfigurationAdmin ca = ( ConfigurationAdmin ) bc.getService( ref );
+                if ( ca != null )
+                {
+                    try
+                    {
+                        Configuration cfg = ca.getConfiguration( this.osgiManager.getConfigurationPid() );
+                        Dictionary newConfig = cfg.getProperties();
+                        if ( newConfig != null )
+                        {
+                            // assumption: config is not null and as a non-null password String property
+                            final String pwd = ( String ) config.get( OsgiManager.PROP_PASSWORD );
+                            final String hashedPassword = Password.hashPassword( pwd );
+                            newConfig.put( OsgiManager.PROP_PASSWORD, hashedPassword );
+                            cfg.update( newConfig );
+                        }
+                    }
+                    catch ( Exception e )
+                    {
+                        // IOException from getting/updated config
+                        // IllegalStateException from hashing password
+                        throw new ConfigurationException( OsgiManager.PROP_PASSWORD, "Cannot update password property",
+                            e );
+                    }
+                    finally
+                    {
+                        bc.ungetService( ref );
+                    }
+                }
+            }
+        }
     }
+
+
+    private boolean isPasswordHashed( final Dictionary config )
+    {
+        // assume hashed (default) password if no config
+        if ( config == null )
+        {
+            return true;
+        }
+
+        // assume hashed (default) password if no password property
+        final Object pwd = config.get( OsgiManager.PROP_PASSWORD );
+        return (pwd instanceof String) && Password.isPasswordHashed((String) pwd);
+    }
+
 }
\ No newline at end of file
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
index 7c2eb4b..14e3a09 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
@@ -159,7 +159,7 @@
 
     static final String DEFAULT_USER_NAME = "admin"; //$NON-NLS-1$
 
-    static final String DEFAULT_PASSWORD = "admin"; //$NON-NLS-1$
+    static final String DEFAULT_PASSWORD = "{sha-256}jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg="; //$NON-NLS-1$
 
     static final String DEFAULT_CATEGORY = "Main"; //$NON-NLS-1$
 
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
index f9635ff..e7957ca 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
@@ -20,7 +20,6 @@
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URL;
-
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -46,7 +45,7 @@
 
     private final String username;
 
-    private final String password;
+    private final Password password;
 
     private final String realm;
 
@@ -56,7 +55,7 @@
     {
         this.tracker = tracker;
         this.username = username;
-        this.password = password;
+        this.password = new Password(password);
         this.realm = realm;
         this.base = httpService.createDefaultHttpContext();
     }
@@ -124,13 +123,11 @@
                 {
                     try
                     {
-                        String srcString = base64Decode( authInfo );
-                        int i = srcString.indexOf( ':' );
-                        String username = srcString.substring( 0, i );
-                        String password = srcString.substring( i + 1 );
+                        byte[][] userPass = base64Decode( authInfo );
+                        final String username = toString( userPass[0] );
 
                         // authenticate
-                        if ( authenticate( provider, username, password ) )
+                        if ( authenticate( provider, username, userPass[1] ) )
                         {
                             // as per the spec, set attributes
                             request.setAttribute( HttpContext.AUTHENTICATION_TYPE, HttpServletRequest.BASIC_AUTH );
@@ -189,27 +186,47 @@
     }
 
 
-    private static String base64Decode( String srcString )
+    private static byte[][] base64Decode( String srcString )
     {
         byte[] transformed = Base64.decodeBase64( srcString );
+        for ( int i = 0; i < transformed.length; i++ )
+        {
+            if ( transformed[i] == ':' )
+            {
+                byte[] user = new byte[i];
+                byte[] pass = new byte[transformed.length - i - 1];
+                System.arraycopy( transformed, 0, user, 0, user.length );
+                System.arraycopy( transformed, i + 1, pass, 0, pass.length );
+                return new byte[][]
+                    { user, pass };
+            }
+        }
+
+        return new byte[][]
+            { transformed, new byte[0] };
+    }
+
+
+    private static String toString( final byte[] src )
+    {
         try
         {
-            return new String( transformed, "ISO-8859-1" );
+            return new String( src, "ISO-8859-1" );
         }
         catch ( UnsupportedEncodingException uee )
         {
-            return new String( transformed );
+            return new String( src );
         }
     }
 
 
-    private boolean authenticate( Object provider, String username, String password )
+    private boolean authenticate( Object provider, String username, byte[] password )
     {
         if ( provider != null )
         {
-            return ( ( WebConsoleSecurityProvider ) provider ).authenticate( username, password ) != null;
+            return ( ( WebConsoleSecurityProvider ) provider ).authenticate( username, toString( password ) ) != null;
         }
-        if ( this.username.equals( username ) && this.password.equals( password ) )
+        if ( this.username.equals( username ) && this.password.matches( password ) )
         {
             return true;
         }
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java
new file mode 100644
index 0000000..5c64e56
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.felix.webconsole.internal.servlet;
+
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+
+/**
+ * The <code>Password</code> class encapsulates encoding and decoding
+ * operations on plain text and hashed passwords.
+ * <p>
+ * Encoded hashed passwords are strings of the form
+ * <code>{hashAlgorithm}base64-encoded-password-hash</code> where
+ * <i>hashAlgorithm</i> is the name of the hash algorithm used to hash
+ * the password and <i>base64-encoded-password-hash</i> is the password
+ * hashed with the indicated hash algorithm and subsequently encoded in
+ * Base64.
+ */
+class Password
+{
+
+    // the default hash algorithm (part of the Java Platform since 1.4)
+    private static final String DEFAULT_HASH_ALGO = "SHA-256";
+
+    // the hash algorithm used to hash the password or null
+    // if the password is not hashed at all
+    private final String hashAlgo;
+
+    // the hashed or plain password
+    private final byte[] password;
+
+
+    /**
+     * Returns {@code true} if the given {@code textPassword} is hashed
+     * and encoded as described in the class comment.
+     *
+     * @param textPassword
+     * @return
+     * @throws NullPointerException if {@code textPassword} is {@code null}.
+     */
+    static boolean isPasswordHashed( final String textPassword )
+    {
+        return getEndOfHashAlgorithm( textPassword ) >= 0;
+    }
+
+
+    /**
+     * Returns the given plain {@code textPassword} as an encoded hashed
+     * password string as described in the class comment.
+     *
+     * @param textPassword
+     * @return
+     * @throws NullPointerException if {@code textPassword} is {@code null}.
+     */
+    static String hashPassword( final String textPassword )
+    {
+        final byte[] bytePassword = Base64.getBytesUtf8( textPassword );
+        return hashPassword( DEFAULT_HASH_ALGO, bytePassword );
+    }
+
+
+    Password( String textPassword )
+    {
+        this.hashAlgo = getPasswordHashAlgorithm( textPassword );
+        this.password = getPasswordBytes( textPassword );
+    }
+
+
+    /**
+     * Returns {@code true} if this password matches the password
+     * {@code toCompare}. If this password is hashed, the {@code toCompare}
+     * password is hashed, too, with the same hash algorithm before
+     * comparison.
+     *
+     * @param toCompare
+     * @return
+     * @throws NullPointerException if {@code toCompare} is {@code null}.
+     */
+    boolean matches( final byte[] toCompare )
+    {
+        return Arrays.equals( this.password, hashPassword( toCompare, this.hashAlgo ) );
+    }
+
+
+    /**
+     * Returns this password as a string hashed and encoded as described
+     * by the class comment. If this password has not been hashed originally,
+     * the default hash algorithm <i>SHA-256</i> is applied.
+     */
+    public String toString()
+    {
+        return hashPassword( this.hashAlgo, this.password );
+    }
+
+
+    private static String hashPassword( final String hashAlgorithm, final byte[] password )
+    {
+        final String actualHashAlgo = ( hashAlgorithm == null ) ? DEFAULT_HASH_ALGO : hashAlgorithm;
+        final byte[] hashedPassword = hashPassword( password, actualHashAlgo );
+        final StringBuffer buf = new StringBuffer( 2 + actualHashAlgo.length() + hashedPassword.length * 3 );
+        buf.append( '{' ).append( actualHashAlgo.toLowerCase() ).append( '}' );
+        buf.append( Base64.newStringUtf8( Base64.encodeBase64( hashedPassword ) ) );
+        return buf.toString();
+    }
+
+
+    private static String getPasswordHashAlgorithm( final String textPassword )
+    {
+        final int endHash = getEndOfHashAlgorithm( textPassword );
+        if ( endHash >= 0 )
+        {
+            return textPassword.substring( 1, endHash );
+        }
+
+        // password is plain text, hence no algorithm
+        return null;
+    }
+
+
+    private static byte[] getPasswordBytes( final String textPassword )
+    {
+        final int endHash = getEndOfHashAlgorithm( textPassword );
+        if ( endHash >= 0 )
+        {
+            final String encodedPassword = textPassword.substring( endHash + 1 );
+            return Base64.decodeBase64( encodedPassword );
+        }
+
+        return Base64.getBytesUtf8( textPassword );
+    }
+
+
+    private static int getEndOfHashAlgorithm( final String textPassword )
+    {
+        if ( textPassword.startsWith( "{" ) )
+        {
+            final int endHash = textPassword.indexOf( "}" );
+            if ( endHash > 0 )
+            {
+                return endHash;
+            }
+        }
+
+        return -1;
+    }
+
+
+    private static byte[] hashPassword( final byte[] pwd, final String hashAlg )
+    {
+        // no hashing if no hash algorithm
+        if ( hashAlg == null || hashAlg.length() == 0 )
+        {
+            return pwd;
+        }
+
+        try
+        {
+            final MessageDigest md = MessageDigest.getInstance( hashAlg );
+            return md.digest( pwd );
+        }
+        catch ( NoSuchAlgorithmException e )
+        {
+            throw new IllegalStateException( "Cannot hash the password: " + e );
+        }
+    }
+}