/** * BlueCove - Java library for Bluetooth * Copyright (C) 2007-2009 Vlad Skarzhevskyy * * 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. * * @author vlads * @version $Id$ */ package com.intel.bluetooth.obex; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Enumeration; import java.util.Vector; import javax.obex.Authenticator; import javax.obex.PasswordAuthentication; import javax.obex.ServerRequestHandler; import com.intel.bluetooth.DebugLog; class OBEXAuthentication { private static byte[] privateKey; private static long uniqueTimestamp = 0; private static final byte COLUMN[] = { ':' }; static class Challenge { private String realm; private boolean isUserIdRequired; private boolean isFullAccess; byte nonce[]; Challenge(byte data[]) throws IOException { this.read(data); } Challenge(String realm, boolean isUserIdRequired, boolean isFullAccess, byte[] nonce) { this.realm = realm; this.isUserIdRequired = isUserIdRequired; this.isFullAccess = isFullAccess; this.nonce = nonce; } byte[] write() { ByteArrayOutputStream buf = new ByteArrayOutputStream(); buf.write(0x00); // Tag buf.write(0x10); // Len buf.write(nonce, 0, 0x10); byte options = (byte) ((isUserIdRequired ? 1 : 0) | ((!isFullAccess) ? 2 : 0)); buf.write(0x01); // Tag buf.write(0x01); // Len buf.write(options); if (realm != null) { byte realmArray[]; byte charSetCode; try { realmArray = OBEXUtils.getUTF16Bytes(realm); charSetCode = -1; // 0xFF; Unicode } catch (UnsupportedEncodingException e) { try { realmArray = realm.getBytes("iso-8859-1"); } catch (UnsupportedEncodingException e1) { realmArray = new byte[0]; } charSetCode = 1; // iso-8859-1 } buf.write(0x02); // Tag buf.write(realmArray.length + 1); // Len buf.write(charSetCode); buf.write(realmArray, 0, realmArray.length); } return buf.toByteArray(); } void read(byte data[]) throws IOException { DebugLog.debug("authChallenge", data); for (int i = 0; i < data.length;) { int tag = data[i] & 0xFF; int len = data[i + 1] & 0xFF; i += 2; switch (tag) { case 0: if (len != 0x10) { throw new IOException("OBEX Digest Challenge error in tag Nonce"); } nonce = new byte[0x10]; System.arraycopy(data, i, nonce, 0, 0x10); break; case 1: byte options = data[i]; DebugLog.debug("authChallenge options", options); isUserIdRequired = ((options & 1) != 0); isFullAccess = ((options & 2) == 0); break; case 2: int charSetCode = data[i] & 0xFF; byte chars[] = new byte[len - 1]; System.arraycopy(data, i + 1, chars, 0, chars.length); if (charSetCode == 0xFF) { realm = OBEXUtils.newStringUTF16(chars); } else if (charSetCode == 0) { realm = new String(chars, "ASCII"); } else if (charSetCode <= 9) { realm = new String(chars, "ISO-8859-" + charSetCode); } else { DebugLog.error("Unsupported charset code " + charSetCode + " in Challenge"); // throw new UnsupportedEncodingException("charset code // " + charSetCode); // BUG on SE K790a realm = new String(chars, 0, len - 1, "ASCII"); } break; default: DebugLog.error("invalid authChallenge tag " + tag); } i += len; } } public boolean isUserIdRequired() { return isUserIdRequired; } public boolean isFullAccess() { return isFullAccess; } public String getRealm() { return realm; } } static class DigestResponse { byte requestDigest[]; byte userName[]; byte nonce[]; byte[] write() { ByteArrayOutputStream buf = new ByteArrayOutputStream(); buf.write(0x00); // Tag buf.write(0x10); // Len buf.write(requestDigest, 0, 0x10); if (userName != null) { buf.write(0x01); // Tag buf.write(userName.length); // Len buf.write(userName, 0, userName.length); } buf.write(0x02); // Tag buf.write(0x10); // Len buf.write(nonce, 0, 0x10); return buf.toByteArray(); } void read(byte data[]) throws IOException { for (int i = 0; i < data.length;) { int tag = data[i] & 0xFF; int len = data[i + 1] & 0xFF; i += 2; switch (tag) { case 0: if (len != 0x10) { throw new IOException("OBEX Digest Response error in tag request-digest"); } requestDigest = new byte[0x10]; System.arraycopy(data, i, requestDigest, 0, 0x10); break; case 1: userName = new byte[len]; System.arraycopy(data, i, userName, 0, userName.length); break; case 2: if (len != 0x10) { throw new IOException("OBEX Digest Response error in tag Nonce"); } nonce = new byte[0x10]; System.arraycopy(data, i, nonce, 0, 0x10); break; } i += len; } } } static byte[] createChallenge(String realm, boolean isUserIdRequired, boolean isFullAccess) { Challenge challenge = new Challenge(realm, isUserIdRequired, isFullAccess, createNonce()); return challenge.write(); } static boolean handleAuthenticationResponse(OBEXHeaderSetImpl incomingHeaders, Authenticator authenticator, ServerRequestHandler serverHandler, Vector authChallengesSent) throws IOException { if (!incomingHeaders.hasAuthenticationResponses()) { return false; } for (Enumeration iter = incomingHeaders.getAuthenticationResponses(); iter.hasMoreElements();) { byte[] authResponse = (byte[]) iter.nextElement(); DigestResponse dr = new DigestResponse(); dr.read(authResponse); DebugLog.debug("got nonce", dr.nonce); // Verify that we did sent the Challenge that triggered this Responses Challenge challengeSent = null; for (Enumeration challengeIter = authChallengesSent.elements(); challengeIter.hasMoreElements();) { Challenge c = (Challenge) challengeIter.nextElement(); if (equals(c.nonce, dr.nonce)) { challengeSent = c; break; } } if (challengeSent == null) { throw new IOException("Authentication response for unknown challenge"); } byte[] password = authenticator.onAuthenticationResponse(dr.userName); if (password == null) { throw new IOException("Authentication request failed, password is not supplied"); } // DebugLog.debug("authenticate using password", new String(password)); // DebugLog.debug("password used", password); MD5DigestWrapper md5 = new MD5DigestWrapper(); md5.update(dr.nonce); md5.update(COLUMN); md5.update(password); byte[] claulated = md5.digest(); if (!equals(dr.requestDigest, claulated)) { DebugLog.debug("got digest", dr.requestDigest); DebugLog.debug(" expected", claulated); if (serverHandler != null) { serverHandler.onAuthenticationFailure(dr.userName); } else { throw new IOException("Authentication failure"); } } else { return true; } } return false; } static void handleAuthenticationChallenge(OBEXHeaderSetImpl incomingHeaders, OBEXHeaderSetImpl replyHeaders, Authenticator authenticator) throws IOException { if (!incomingHeaders.hasAuthenticationChallenge()) { return; } for (Enumeration iter = incomingHeaders.getAuthenticationChallenges(); iter.hasMoreElements();) { byte[] authChallenge = (byte[]) iter.nextElement(); Challenge challenge = new Challenge(authChallenge); PasswordAuthentication pwd = authenticator.onAuthenticationChallenge(challenge.getRealm(), challenge .isUserIdRequired(), challenge.isFullAccess()); DigestResponse dr = new DigestResponse(); dr.nonce = challenge.nonce; DebugLog.debug("got nonce", dr.nonce); if (challenge.isUserIdRequired()) { dr.userName = pwd.getUserName(); } MD5DigestWrapper md5 = new MD5DigestWrapper(); md5.update(dr.nonce); md5.update(COLUMN); md5.update(pwd.getPassword()); dr.requestDigest = md5.digest(); // DebugLog.debug("password", new String(pwd.getPassword())); // DebugLog.debug("password used", pwd.getPassword()); DebugLog.debug("send digest", dr.requestDigest); replyHeaders.addAuthenticationResponse(dr.write()); } } private static synchronized byte[] createNonce() { MD5DigestWrapper md5 = new MD5DigestWrapper(); md5.update(createTimestamp()); md5.update(COLUMN); md5.update(getPrivateKey()); return md5.digest(); } static boolean equals(byte[] digest1, byte[] digest2) { for (int i = 0; i < 0x10; i++) { if (digest1[i] != digest2[i]) { return false; } } return true; } private static synchronized byte[] getPrivateKey() { if (privateKey != null) { return privateKey; } MD5DigestWrapper md5 = new MD5DigestWrapper(); md5.update(createTimestamp()); privateKey = md5.digest(); return privateKey; } private static synchronized byte[] createTimestamp() { long t = System.currentTimeMillis(); if (t <= uniqueTimestamp) { t = uniqueTimestamp + 1; } uniqueTimestamp = t; byte[] buf = new byte[8]; for (int i = 0; i < buf.length; i++) { buf[i] = (byte) (t >> (buf.length - 1 << 3)); t <<= 8; } return buf; } }