Author: rgodfrey
Date: Thu Apr 24 22:25:22 2014
New Revision: 1589898

URL: http://svn.apache.org/r1589898
Log:
QPID-5639 : Add SASL SCRAM-SHA-1 Java client support

Added:
    
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/
    
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java
    
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java

Added: 
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java
URL: 
http://svn.apache.org/viewvc/qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java?rev=1589898&view=auto
==============================================================================
--- 
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java
 (added)
+++ 
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java
 Thu Apr 24 22:25:22 2014
@@ -0,0 +1,339 @@
+/*
+ *
+ * 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.qpid.client.security.scram;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslException;
+import javax.xml.bind.DatatypeConverter;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.UUID;
+
+public class ScramSHA1SaslClient implements SaslClient
+{
+
+    private static final byte[] INT_1 = new byte[]{0, 0, 0, 1};
+    private static final String GS2_HEADER = "n,,";
+    private static final Charset ASCII = Charset.forName("ASCII");
+
+    private String _username;
+    private final String _clientNonce = UUID.randomUUID().toString();
+    private String _serverNonce;
+    private byte[] _salt;
+    private int _iterationCount;
+    private String _clientFirstMessageBare;
+    private byte[] _serverSignature;
+
+    enum State
+    {
+        INITIAL,
+        CLIENT_FIRST_SENT,
+        CLIENT_PROOF_SENT,
+        COMPLETE
+    }
+
+    public static final String MECHANISM = "SCRAM-SHA-1";
+
+    private final CallbackHandler _callbackHandler;
+
+    private State _state = State.INITIAL;
+
+    public ScramSHA1SaslClient(final CallbackHandler cbh)
+    {
+        _callbackHandler = cbh;
+    }
+
+    @Override
+    public String getMechanismName()
+    {
+        return MECHANISM;
+    }
+
+    @Override
+    public boolean hasInitialResponse()
+    {
+        return true;
+    }
+
+    @Override
+    public byte[] evaluateChallenge(final byte[] challenge) throws 
SaslException
+    {
+        byte[] response;
+        switch(_state)
+        {
+            case INITIAL:
+                response = initialResponse();
+                _state = State.CLIENT_FIRST_SENT;
+                break;
+            case CLIENT_FIRST_SENT:
+                response = calculateClientProof(challenge);
+                _state = State.CLIENT_PROOF_SENT;
+                break;
+            case CLIENT_PROOF_SENT:
+                evaluateOutcome(challenge);
+                response = null;
+                _state = State.COMPLETE;
+                break;
+            default:
+                throw new SaslException("No challenge expected in state " + 
_state);
+        }
+        return response;
+    }
+
+    private void evaluateOutcome(final byte[] challenge) throws SaslException
+    {
+        String serverFinalMessage = new String(challenge, ASCII);
+        String[] parts = serverFinalMessage.split(",");
+        if(!parts[0].startsWith("v="))
+        {
+            throw new SaslException("Server final message did not contain 
verifier");
+        }
+        byte[] serverSignature = 
DatatypeConverter.parseBase64Binary(parts[0].substring(2));
+        if(!Arrays.equals(_serverSignature, serverSignature))
+        {
+            throw new SaslException("Server signature did not match");
+        }
+    }
+
+    private byte[] calculateClientProof(final byte[] challenge) throws 
SaslException
+    {
+        try
+        {
+            String serverFirstMessage = new String(challenge, ASCII);
+            String[] parts = serverFirstMessage.split(",");
+            if(parts.length < 3)
+            {
+                throw new SaslException("Server challenge '" + 
serverFirstMessage + "' cannot be parsed");
+            }
+            else if(parts[0].startsWith("m="))
+            {
+                throw new SaslException("Server requires mandatory extension 
which is not supported: " + parts[0]);
+            }
+            else if(!parts[0].startsWith("r="))
+            {
+                throw new SaslException("Server challenge '" + 
serverFirstMessage + "' cannot be parsed, cannot find nonce");
+            }
+            String nonce = parts[0].substring(2);
+            if(!nonce.startsWith(_clientNonce))
+            {
+                throw new SaslException("Server challenge did not use correct 
client nonce");
+            }
+            _serverNonce = nonce;
+            if(!parts[1].startsWith("s="))
+            {
+                throw new SaslException("Server challenge '" + 
serverFirstMessage + "' cannot be parsed, cannot find salt");
+            }
+            String base64Salt = parts[1].substring(2);
+            _salt = DatatypeConverter.parseBase64Binary(base64Salt);
+            if(!parts[2].startsWith("i="))
+            {
+                throw new SaslException("Server challenge '" + 
serverFirstMessage + "' cannot be parsed, cannot find iteration count");
+            }
+            String iterCountString = parts[2].substring(2);
+            _iterationCount = Integer.parseInt(iterCountString);
+            if(_iterationCount <= 0)
+            {
+                throw new SaslException("Iteration count " + _iterationCount + 
" is not a positive integer");
+            }
+            PasswordCallback passwordCallback = new 
PasswordCallback("Password", false);
+            _callbackHandler.handle(new Callback[] { passwordCallback });
+            byte[] passwordBytes = saslPrep(new 
String(passwordCallback.getPassword())).getBytes("UTF-8");
+
+            byte[] saltedPassword = generateSaltedPassword(passwordBytes);
+
+
+            String clientFinalMessageWithoutProof =
+                    "c=" + 
DatatypeConverter.printBase64Binary(GS2_HEADER.getBytes(ASCII))
+                    + ",r=" + _serverNonce;
+
+            String authMessage = _clientFirstMessageBare + "," + 
serverFirstMessage + "," + clientFinalMessageWithoutProof;
+
+            byte[] clientKey = computeHmacSHA1(saltedPassword, "Client Key");
+            byte[] storedKey = 
MessageDigest.getInstance("SHA1").digest(clientKey);
+
+            byte[] clientSignature = computeHmacSHA1(storedKey, authMessage);
+
+            byte[] clientProof = clientKey.clone();
+            for(int i = 0 ; i < clientProof.length; i++)
+            {
+                clientProof[i] ^= clientSignature[i];
+            }
+            byte[] serverKey = computeHmacSHA1(saltedPassword, "Server Key");
+            _serverSignature = computeHmacSHA1(serverKey, authMessage);
+
+            String finalMessageWithProof = clientFinalMessageWithoutProof
+                                           + ",p=" + 
DatatypeConverter.printBase64Binary(clientProof);
+            return finalMessageWithProof.getBytes();
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+        catch (IllegalArgumentException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+        catch (UnsupportedCallbackException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+        catch (IOException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+        catch (NoSuchAlgorithmException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+    }
+
+    private byte[] computeHmacSHA1(final byte[] key, final String string)
+            throws SaslException, UnsupportedEncodingException
+    {
+        Mac mac = createSha1Hmac(key);
+        mac.update(string.getBytes(ASCII));
+        return mac.doFinal();
+    }
+
+    private byte[] generateSaltedPassword(final byte[] passwordBytes) throws 
SaslException
+    {
+        Mac mac = createSha1Hmac(passwordBytes);
+
+        mac.update(_salt);
+        mac.update(INT_1);
+        byte[] result = mac.doFinal();
+
+        byte[] previous = null;
+        for(int i = 1; i < _iterationCount; i++)
+        {
+            mac.update(previous != null? previous: result);
+            previous = mac.doFinal();
+            for(int x = 0; x < result.length; x++)
+            {
+                result[x] ^= previous[x];
+            }
+        }
+
+        return result;
+    }
+
+    private Mac createSha1Hmac(final byte[] keyBytes)
+            throws SaslException
+    {
+        try
+        {
+            SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA1");
+            Mac mac = Mac.getInstance("HmacSHA1");
+            mac.init(key);
+            return mac;
+        }
+        catch (NoSuchAlgorithmException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+        catch (InvalidKeyException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+    }
+
+
+    private byte[] initialResponse() throws SaslException
+    {
+        try
+        {
+            StringBuffer buf = new StringBuffer("n=");
+            NameCallback nameCallback = new NameCallback("Username?");
+            _callbackHandler.handle(new Callback[] { nameCallback });
+            _username = nameCallback.getName();
+            buf.append(saslPrep(_username));
+            buf.append(",r=");
+            buf.append(_clientNonce);
+            _clientFirstMessageBare = buf.toString();
+            return (GS2_HEADER + _clientFirstMessageBare).getBytes(ASCII);
+        }
+        catch (UnsupportedCallbackException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+        catch (IOException e)
+        {
+            throw new SaslException(e.getMessage(), e);
+        }
+    }
+
+    private String saslPrep(String name) throws SaslException
+    {
+        // TODO - a real implementation of SaslPrep
+
+        if(!ASCII.newEncoder().canEncode(name))
+        {
+            throw new SaslException("Can only encode names and passwords which 
are restricted to ASCII characters");
+        }
+
+        name = name.replace("=", "=3D");
+        name = name.replace(",", "=2C");
+        return name;
+    }
+
+    @Override
+    public boolean isComplete()
+    {
+        return _state == State.COMPLETE;
+    }
+
+    @Override
+    public byte[] unwrap(final byte[] incoming, final int offset, final int 
len) throws SaslException
+    {
+        throw new IllegalStateException("No security layer supported");
+    }
+
+    @Override
+    public byte[] wrap(final byte[] outgoing, final int offset, final int len) 
throws SaslException
+    {
+        throw new IllegalStateException("No security layer supported");
+    }
+
+    @Override
+    public Object getNegotiatedProperty(final String propName)
+    {
+        return null;
+    }
+
+    @Override
+    public void dispose() throws SaslException
+    {
+
+    }
+
+}

Added: 
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java
URL: 
http://svn.apache.org/viewvc/qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java?rev=1589898&view=auto
==============================================================================
--- 
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java
 (added)
+++ 
qpid/trunk/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java
 Thu Apr 24 22:25:22 2014
@@ -0,0 +1,58 @@
+/*
+ *
+ * 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.qpid.client.security.scram;
+
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslClientFactory;
+import javax.security.sasl.SaslException;
+import java.util.Map;
+
+public class ScramSHA1SaslClientFactory implements SaslClientFactory
+{
+    @Override
+    public SaslClient createSaslClient(final String[] mechanisms,
+                                       final String authorizationId,
+                                       final String protocol,
+                                       final String serverName,
+                                       final Map<String, ?> props,
+                                       final CallbackHandler cbh) throws 
SaslException
+    {
+        for (int i = 0; i < mechanisms.length; i++)
+        {
+            if (mechanisms[i].equals(ScramSHA1SaslClient.MECHANISM))
+            {
+                if (cbh == null)
+                {
+                    throw new SaslException("CallbackHandler must not be 
null");
+                }
+                return new ScramSHA1SaslClient(cbh);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public String[] getMechanismNames(final Map<String, ?> props)
+    {
+        return new String[0];
+    }
+}



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to