diff options
| author | Robert Godfrey <rgodfrey@apache.org> | 2014-04-24 22:25:22 +0000 |
|---|---|---|
| committer | Robert Godfrey <rgodfrey@apache.org> | 2014-04-24 22:25:22 +0000 |
| commit | 6c082f6883922e98e627394c3fa951680a7729b2 (patch) | |
| tree | 57197cef8b16e03f7941c6678371cd7f27b6f06e /qpid/java | |
| parent | e50105c859d536424397c3d9fdd3c5df6efc36f6 (diff) | |
| download | qpid-python-6c082f6883922e98e627394c3fa951680a7729b2.tar.gz | |
QPID-5639 : Add SASL SCRAM-SHA-1 Java client support
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk@1589898 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'qpid/java')
2 files changed, 397 insertions, 0 deletions
diff --git a/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java b/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java new file mode 100644 index 0000000000..91c03e18c1 --- /dev/null +++ b/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java @@ -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 + { + + } + +} diff --git a/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java b/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java new file mode 100644 index 0000000000..8a54abbc04 --- /dev/null +++ b/qpid/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java @@ -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]; + } +} |
