mirror of
https://github.com/vanitasvitae/Smack.git
synced 2025-09-09 00:59:39 +02:00
Add smack-android and redesign SASL authentication
This commit marks an important milestone with the addition of the smack-android subproject. Smack is now able to run native on Android without requiring any modifications, which makes the aSmack build environment obsolete. It was necessary to redesign the code for SASL authentication to achieve this. Smack now comes with smack-sasl-provided for SASL implementations that do not rely on additional APIs like javax for platforms where those APIs are not available like Android.
This commit is contained in:
parent
5a2149718a
commit
89dc3a0e85
39 changed files with 1562 additions and 675 deletions
8
smack-sasl-provided/build.gradle
Normal file
8
smack-sasl-provided/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
|||
description = """\
|
||||
SASL with Smack provided code
|
||||
Use Smack provided code for SASL."""
|
||||
|
||||
dependencies {
|
||||
compile project(path: ':smack-core', configuration: 'sasl')
|
||||
testCompile project(':smack-core').sourceSets.test.runtimeClasspath
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2014 Florian Schmaus
|
||||
*
|
||||
* Licensed 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.jivesoftware.smack.sasl.provided;
|
||||
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
|
||||
import org.jivesoftware.smack.SmackException;
|
||||
import org.jivesoftware.smack.sasl.SASLMechanism;
|
||||
import org.jivesoftware.smack.util.ByteUtils;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
|
||||
public class SASLDigestMD5Mechanism extends SASLMechanism {
|
||||
|
||||
public static final String NAME = DIGESTMD5;
|
||||
|
||||
private static final String INITAL_NONCE = "00000001";
|
||||
|
||||
/**
|
||||
* The only 'qop' value supported by this implementation
|
||||
*/
|
||||
private static final String QOP_VALUE = "auth";
|
||||
|
||||
private enum State {
|
||||
INITIAL,
|
||||
RESPONSE_SENT,
|
||||
VALID_SERVER_RESPONSE,
|
||||
}
|
||||
|
||||
private static boolean verifyServerResponse = true;
|
||||
|
||||
public static void setVerifyServerResponse(boolean verifyServerResponse) {
|
||||
SASLDigestMD5Mechanism.verifyServerResponse = verifyServerResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the this instance of SASL DIGEST-MD5 authentication.
|
||||
*/
|
||||
private State state = State.INITIAL;
|
||||
|
||||
private String nonce;
|
||||
private String cnonce;
|
||||
private String digestUri;
|
||||
private String hex_hashed_a1;
|
||||
|
||||
@Override
|
||||
protected void authenticateInternal(CallbackHandler cbh) throws SmackException {
|
||||
throw new UnsupportedOperationException("CallbackHandler not (yet) supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getAuthenticationText() throws SmackException {
|
||||
// DIGEST-MD5 has no initial response, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 210;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SASLDigestMD5Mechanism newInstance() {
|
||||
return new SASLDigestMD5Mechanism();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
|
||||
if (challenge.length == 0) {
|
||||
throw new SmackException("Initial challenge has zero length");
|
||||
}
|
||||
String[] challengeParts = (new String(challenge)).split(",");
|
||||
byte[] response = null;
|
||||
switch (state) {
|
||||
case INITIAL:
|
||||
for (String part : challengeParts) {
|
||||
String[] keyValue = part.split("=");
|
||||
assert (keyValue.length == 2);
|
||||
String key = keyValue[0];
|
||||
String value = keyValue[1];
|
||||
if ("nonce".equals(key)) {
|
||||
if (nonce != null) {
|
||||
throw new SmackException("Nonce value present multiple times");
|
||||
}
|
||||
nonce = value.replace("\"", "");
|
||||
}
|
||||
else if ("qop".equals(key)) {
|
||||
value = value.replace("\"", "");
|
||||
if (!value.equals("auth")) {
|
||||
throw new SmackException("Unsupported qop operation: " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nonce == null) {
|
||||
// RFC 2831 2.1.1 about nonce "This directive is required and MUST appear exactly
|
||||
// once; if not present, or if multiple instances are present, the client should
|
||||
// abort the authentication exchange."
|
||||
throw new SmackException("nonce value not present in initial challenge");
|
||||
}
|
||||
// RFC 2831 2.1.2.1 defines A1, A2, KD and response-value
|
||||
byte[] a1FirstPart = ByteUtils.md5(toBytes(authenticationId + ':' + serviceName + ':'
|
||||
+ password));
|
||||
cnonce = StringUtils.randomString(32);
|
||||
byte[] a1 = ByteUtils.concact(a1FirstPart, toBytes(':' + nonce + ':' + cnonce));
|
||||
digestUri = "xmpp/" + serviceName;
|
||||
hex_hashed_a1 = StringUtils.encodeHex(ByteUtils.md5(a1));
|
||||
String responseValue = calcResponse(DigestType.ClientResponse);
|
||||
// @formatter:off
|
||||
// See RFC 2831 2.1.2 digest-response
|
||||
String saslString = "username=\"" + authenticationId + '"'
|
||||
+ ",realm=\"" + serviceName + '"'
|
||||
+ ",nonce=\"" + nonce + '"'
|
||||
+ ",cnonce=\"" + cnonce + '"'
|
||||
+ ",nc=" + INITAL_NONCE
|
||||
+ ",qop=auth"
|
||||
+ ",digest-uri=\"" + digestUri + '"'
|
||||
+ ",response=" + responseValue
|
||||
+ ",charset=utf-8";
|
||||
// @formatter:on
|
||||
response = toBytes(saslString);
|
||||
state = State.RESPONSE_SENT;
|
||||
break;
|
||||
case RESPONSE_SENT:
|
||||
if (verifyServerResponse) {
|
||||
String serverResponse = null;
|
||||
for (String part : challengeParts) {
|
||||
String[] keyValue = part.split("=");
|
||||
assert (keyValue.length == 2);
|
||||
String key = keyValue[0];
|
||||
String value = keyValue[1];
|
||||
if ("rspauth".equals(key)) {
|
||||
serverResponse = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (serverResponse == null) {
|
||||
throw new SmackException("No server response received while performing " + NAME
|
||||
+ " authentication");
|
||||
}
|
||||
String expectedServerResponse = calcResponse(DigestType.ServerResponse);
|
||||
if (!serverResponse.equals(expectedServerResponse)) {
|
||||
throw new SmackException("Invalid server response while performing " + NAME
|
||||
+ " authentication");
|
||||
}
|
||||
}
|
||||
state = State.VALID_SERVER_RESPONSE;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private enum DigestType {
|
||||
ClientResponse,
|
||||
ServerResponse
|
||||
}
|
||||
|
||||
private String calcResponse(DigestType digestType) {
|
||||
StringBuilder a2 = new StringBuilder();
|
||||
if (digestType == DigestType.ClientResponse) {
|
||||
a2.append("AUTHENTICATE");
|
||||
}
|
||||
a2.append(':');
|
||||
a2.append(digestUri);
|
||||
String hex_hashed_a2 = StringUtils.encodeHex(ByteUtils.md5(toBytes(a2.toString())));
|
||||
|
||||
StringBuilder kd_argument = new StringBuilder();
|
||||
kd_argument.append(hex_hashed_a1);
|
||||
kd_argument.append(':');
|
||||
kd_argument.append(nonce);
|
||||
kd_argument.append(':');
|
||||
kd_argument.append(INITAL_NONCE);
|
||||
kd_argument.append(':');
|
||||
kd_argument.append(cnonce);
|
||||
kd_argument.append(':');
|
||||
kd_argument.append(QOP_VALUE);
|
||||
kd_argument.append(':');
|
||||
kd_argument.append(hex_hashed_a2);
|
||||
byte[] kd = ByteUtils.md5(toBytes(kd_argument.toString()));
|
||||
String responseValue = StringUtils.encodeHex(kd);
|
||||
return responseValue;
|
||||
}
|
||||
|
||||
private static byte[] toBytes(String string) {
|
||||
return StringUtils.toBytes(string);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2014 Florian Schmaus
|
||||
*
|
||||
* Licensed 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.jivesoftware.smack.sasl.provided;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jivesoftware.smack.SASLAuthentication;
|
||||
import org.jivesoftware.smack.initializer.SmackAndOsgiInitializer;
|
||||
|
||||
public class SASLProvidedSmackInitializer extends SmackAndOsgiInitializer {
|
||||
|
||||
@Override
|
||||
public List<Exception> initialize() {
|
||||
SASLAuthentication.registerSASLMechanism(new SASLDigestMD5Mechanism());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Exception> initialize(ClassLoader classLoader) {
|
||||
return initialize();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.2.0"
|
||||
enabled="true" immediate="true" name="Smack Resolver JavaX API">
|
||||
<implementation
|
||||
class="org.jivesoftware.smack.sasl.provided.SASLProvidedSmackInitializer" />
|
||||
</scr:component>
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
*
|
||||
* Copyright 2014 Florian Schmaus
|
||||
*
|
||||
* Licensed 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.jivesoftware.smack.sasl.provided;
|
||||
|
||||
import org.jivesoftware.smack.SmackException;
|
||||
import org.jivesoftware.smack.SmackException.NotConnectedException;
|
||||
import org.jivesoftware.smack.sasl.DigestMd5SaslTest;
|
||||
import org.jivesoftware.smack.sasl.provided.SASLDigestMD5Mechanism;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SASLDigestMD5Test extends DigestMd5SaslTest {
|
||||
public SASLDigestMD5Test() {
|
||||
super(new SASLDigestMD5Mechanism());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDigestMD5() throws NotConnectedException, SmackException {
|
||||
runTest();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue