/*
*
*
* Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.sun.jsr082.obex;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Vector;
import javax.obex.Authenticator;
import javax.obex.PasswordAuthentication;
/*
* Obex protocol authentication functinality.
*/
class ObexAuth {
/* Debug information, should be false for RR. */
private static final boolean DEBUG = false;
private static byte[] column = { (byte)':' };
String realm;
boolean userID;
boolean access;
byte[] nonce;
private static int counter = 0;
// used in prepareChallenge, addChallenge
private byte[] realm_array;
private int challengeLength;
static ObexAuth createChallenge(String realm,
boolean userID, boolean access) {
return new ObexAuth(realm, null, userID, access);
}
private ObexAuth(String realm, byte[] nonce, boolean userID,
boolean access) {
this.realm = realm;
this.nonce = nonce;
this.userID = userID;
this.access = access;
}
/*
* Prepare challenge before adding to packet.
* @return length of challenge
*/
int prepareChallenge() {
if (challengeLength != 0) {
return challengeLength;
}
try {
int len = 24;
realm_array = null;
if (realm != null) {
realm_array = realm.getBytes("UTF-16BE");
len += 3 + realm_array.length;
}
challengeLength = len;
} catch (UnsupportedEncodingException e) {
if (DEBUG) {
System.out.println("prepareChallenge(): ERROR, no encoding");
}
return 0;
}
return challengeLength;
}
int addChallenge(byte[] packet, int offset) throws IOException {
int len = prepareChallenge();
packet[offset] = (byte)ObexPacketStream.HEADER_AUTH_CHALLENGE;
packet[offset+1] = (byte) (len >> 8);
packet[offset+2] = (byte) (len & 255);
packet[offset+3] = (byte) 0; // nonce tag (0x0)
packet[offset+4] = (byte) 16; // nonce len (16) (1 bytes)
nonce = makeNonce(); // 16 byte of nonce
if (DEBUG) {
print("addChallenge: nonce", nonce);
}
System.arraycopy(nonce, 0, packet, offset + 5, 16);
packet[offset+21] = (byte) 1; // options tag (0x1)
packet[offset+22] = (byte) 1; // options length (1)
packet[offset+23] = (byte) ((userID ? 1 : 0) + (access ? 0 : 2));
if (realm != null) {
int realm_len = realm_array.length;
packet[offset+24] = (byte) 2; // realm tag (0x2)
// realm length including encoding (1 byte)
packet[offset+25] = (byte) (realm_len + 1);
packet[offset+26] = (byte) 0xFF; // realm encoding UNICODE
System.arraycopy(realm_array, 0, packet, offset+27, realm_len);
}
return len;
}
private static byte[] makeNonce() throws IOException {
SSLWrapper md5 = new SSLWrapper();
byte[] timestamp = createTimestamp();
md5.update(timestamp, 0, timestamp.length);
md5.update(column, 0, 1);
byte[] privateKey = getPrivateKey();
byte[] nonce = new byte[16];
md5.doFinal(privateKey, 0, privateKey.length, nonce, 0);
return nonce;
}
/*
* Creates timestamp.
* No strict specification for timestamp generation in OBEX 1.2
* @return timestamp value
*/
private static byte[] createTimestamp() {
long time = System.currentTimeMillis();
byte[] timestamp = new byte[9];
timestamp[0] = (byte)(time >> 56);
timestamp[1] = (byte)(time >> 48);
timestamp[2] = (byte)(time >> 40);
timestamp[3] = (byte)(time >> 32);
timestamp[4] = (byte)(time >> 24);
timestamp[5] = (byte)(time >> 16);
timestamp[6] = (byte)(time >> 8);
timestamp[7] = (byte)(time);
synchronized (ObexAuth.class) {
timestamp[8] = (byte)(counter++);
}
return timestamp;
}
private static byte[] privateKey = null;
/*
* Create and return private key.
* Weak security scheme. Should be rewritten for more secure
* implementation if used outside emulator.
*/
private synchronized static byte[] getPrivateKey() throws IOException {
if (privateKey != null) {
return privateKey;
}
SSLWrapper md5 = new SSLWrapper();
byte[] keyData = null;
try {
keyData = "timestamp = ".getBytes("ISO-8859-1");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
md5.update(keyData, 0, keyData.length);
byte[] timestamp = createTimestamp();
privateKey = new byte[16];
md5.doFinal(timestamp, 0, timestamp.length, privateKey, 0);
return privateKey;
}
static void makeDigest(byte[] buffer, int offset, byte[] nonce,
byte[] password)
throws IOException {
SSLWrapper md5 = new SSLWrapper();
md5.update(nonce, 0, 16);
md5.update(column, 0, 1);
md5.doFinal(password, 0, password.length, buffer, offset);
}
static ObexAuth parseAuthChallenge(byte[] buffer, int packetOffset,
int length)
throws IOException {
if (DEBUG) {
System.out.println("ObexAuth.parseAuthChallenge()");
}
// default values
boolean readonly = false;
boolean needUserid = false;
byte[] nonce = null;
String realm = null;
// skiping header type and length
int offset = packetOffset + 3;
length += packetOffset;
// decoding data in buffer
while (offset < length) {
int tag = buffer[offset] & 0xFF;
int len = buffer[offset + 1] & 0xFF;
offset += 2;
switch (tag) {
case 0x0: // nonce
if (len != 16 || nonce != null) {
throw new IOException("protocol error");
}
nonce = new byte[16];
System.arraycopy(buffer, offset, nonce, 0, 16);
if (DEBUG) {
print("got challenge: nonce", nonce);
}
break;
case 0x1: // options
if (len != 1) {
throw new IOException("protocol error");
}
int options = buffer[offset];
readonly = ((options & 2) != 0);
needUserid = ((options & 1) != 0);
break;
case 0x2: // realm
try {
int encodingID = buffer[offset] & 0xFF;
String encoding = null;
if (encodingID == 255) encoding = "UTF-16BE";
else if (encodingID == 0) encoding = "US-ASCII";
else if (encodingID < 10) encoding = "ISO-8859-"
+ encoding;
else throw new UnsupportedEncodingException();
realm = new String(buffer, offset + 1,
len - 1, encoding);
} catch (UnsupportedEncodingException e) {
// already: realm = null;
}
}
offset += len;
}
if (offset != length) {
throw new IOException("protocol error");
}
return new ObexAuth(realm, nonce, needUserid, !readonly);
}
int replyAuthChallenge(byte[] buffer, int packetOffset,
Authenticator authenticator) throws IOException {
if (DEBUG) {
System.out.println("ObexAuth.replyAuthChallenge()");
}
if (realm == null) {
realm = "";
}
byte[] password = null;
byte[] username = null;
try {
PasswordAuthentication pass =
authenticator.onAuthenticationChallenge(realm, userID, access);
password = pass.getPassword();
int uidLen = 0;
// userid subheader length with subheader <tag> and <len>
username = pass.getUserName();
if (userID || username != null) {
// username is required but not provided
if (userID && username.length == 0) {
if (DEBUG) {
System.out.println("ObexAuth.replyAuthChallenge():"
+ " required username not provided");
}
throw new Exception();
}
uidLen = 2 + username.length;
// maximum supported username length = 20
if (uidLen > 22) uidLen = 22;
}
int len = 39 + uidLen;
// byte[] response = new byte[len];
buffer[packetOffset + 0] = (byte)ObexPacketStream
.HEADER_AUTH_RESPONSE;
buffer[packetOffset + 1] = (byte) (len >> 8);
buffer[packetOffset + 2] = (byte) (len & 255);
buffer[packetOffset + 3] = 0x0; // tag (Request-Digest)
buffer[packetOffset + 4] = 16; // digest len (16)
makeDigest(buffer, packetOffset + 5, nonce, password);
buffer[packetOffset + 21] = 0x02; // tag nonce
buffer[packetOffset + 22] = 16; // nonce len (16)
System.arraycopy(nonce, 0, buffer, packetOffset + 23, 16);
if (DEBUG) {
print("send response: nonce", nonce);
}
if (uidLen > 2) {
buffer[packetOffset + 39] = 0x01; // tag userid
buffer[packetOffset + 40] = (byte) (uidLen - 2); // userid len
System.arraycopy(username, 0, buffer, packetOffset + 41,
uidLen - 2);
}
if (DEBUG) {
System.out.println("ObexAuth.replyAuthChallenge():"
+ " response generated");
}
return len;
// need to create authentication response
// we should resend previous packet with the authentication response
} catch (Throwable t) {
if (DEBUG) {
System.out.println("ObexAuth.replyAuthChallenge(): exception");
t.printStackTrace();
}
// will caught NullPointerException if authenticator
// was not provided
// will caught exceptions in handler
// will caught NullPointerException exception
// if username == null and needUserid
//
// wrong response from client application,
// ignoring authentication challenge
// client will receive UNAUTHORIZED
}
return 0;
}
private static void print(String msg, byte[] array) {
if (DEBUG) {
System.out.println(msg);
if (array == null) {
System.out.println("[0] = NULL");
return;
}
System.out.print("[" + array.length + "]");
for (int i = 0; i < array.length; i++) {
System.out.print(" " + Integer.toHexString(array[i] & 0xFF));
}
System.out.println("");
}
}
private static boolean compare(byte[] src1, byte[] src2) {
for (int i = 0; i < 16; i++) {
if (src1[i] != src2[i])
return false;
}
return true;
}
static boolean checkAuthResponse(byte[] buffer, int packetOffset,
int length, ObexPacketStream stream, Vector challenges)
throws IOException {
if (DEBUG) {
System.out.println("ObexAuth.parseAuthResponse()");
}
// skiping header type and length
int offset = packetOffset + 3;
length += packetOffset;
byte[] digest = null;
byte[] username = null;
byte[] nonce = null;
// decoding data in buffer
while (offset < length) {
int tag = buffer[offset] & 0xFF;
int len = buffer[offset + 1] & 0xFF;
offset += 2;
switch (tag) {
case 0x0: // digest
if (DEBUG) {
System.out.println("got digest");
}
if (len != 16 || digest != null) {
throw new IOException("protocol error (1)");
}
digest = new byte[16];
System.arraycopy(buffer, offset, digest, 0, 16);
if (DEBUG) {
print("got response: digest", digest);
}
break;
case 0x1: // username
if (DEBUG) {
System.out.println("got username");
}
if (len > 20 || len == 0 || username != null) {
throw new IOException("protocol error (2)");
}
username = new byte[len];
System.arraycopy(buffer, offset, username, 0, len);
break;
case 0x2: // nonce
if (DEBUG) {
System.out.println("got nonce");
}
if (len != 16 || nonce != null) {
throw new IOException("protocol error (3)");
}
nonce = new byte[16];
System.arraycopy(buffer, offset, nonce, 0, 16);
if (DEBUG) {
print("got response: nonce", nonce);
}
break;
default:
if (DEBUG) {
System.out.println("unknown tag = " + tag);
}
}
offset += len;
}
if (offset != length) {
throw new IOException("protocol error (4)");
}
// check nonce and select auth object
ObexAuth auth = null;
if (nonce == null) {
if (DEBUG) {
System.out.println("no nonce received, using first auth");
}
if (challenges.size() == 0) {
return false;
}
auth = (ObexAuth) challenges.elementAt(0);
nonce = auth.nonce;
challenges.removeElementAt(0);
} else {
if (DEBUG) {
System.out.println("nonce provided, searching for auth");
System.out.println("challenges = " + challenges.size());
}
for (int i = 0; i < challenges.size(); i++) {
ObexAuth a = (ObexAuth) challenges.elementAt(i);
if (compare(nonce, a.nonce)) {
if (DEBUG) {
System.out.println("nonce is in " + i + " challenge");
}
auth = a;
challenges.removeElementAt(i);
break;
}
}
if (DEBUG) {
System.out.println("auth = " + auth);
}
if (auth == null)
return false;
}
// check username existance
if (auth.userID && username == null) {
if (DEBUG) {
System.out.println("need username!");
}
// NOTE: may be too strict
stream.onAuthenticationFailure(username);
return false;
}
// ask password from authenticator and check digest
try {
if (DEBUG) {
System.out.println("running onAuthenticationResponse()...");
}
byte[] password =
stream.authenticator.onAuthenticationResponse(username);
byte[] localDigest = new byte[16];
makeDigest(localDigest, 0, nonce, password);
if (DEBUG) {
System.out.println("digest created");
}
boolean res = compare(localDigest, digest);
if (res == false) {
if (DEBUG) {
System.out.println("Calling onAuthenticationFailure()..");
}
stream.onAuthenticationFailure(username);
}
return res;
} catch (Throwable t) {
// catch localDigest = null, crypto and user code exception
if (DEBUG) {
System.out.println("exception");
}
stream.onAuthenticationFailure(username);
return false;
}
}
}