Chapter 8

Concepts in Action: Part 2

In this chapter, as in Chapter 4, we will take a closer look at source code and applications that implement some of the theoretical concepts we’ve discussed. This will give you a better feeling for how to apply them in practice. This chapter’s code examples will focus on secure authentication and safeguarding passwords on the device. Recall that we’ve discussed two mechanisms of logging in to back-end applications without storing credentials on the device. Here, we will explore more detailed source code related to that.

OAuth

Let’s revisit the OAuth login example covered in Chapter 6. We discussed developing an application that will interact with Google Picasa Web Albums to read off a list of albums from a specific user. The code in this chapter will do this. Check this book’s page on the Apress web site at www.apress.com for the latest code. First, let’s look at our project structure in Figure 8-1. You will see several source files. We will go over each source file’s key functionality.

9781430240624_Fig08-01.jpg

Figure 8-1.  The OAuth example’s project structure

Retrieving the Token

You can see the structure of the OAuth example project in Figure 8-1. Let’s start with the application’s entry point, which is OAuthPicasaActivity.java, shown in Listing 8-1.

Listing 8-1.  The Application Entry Point

package net.zenconsult.android;
  
import android.app.ListActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
  
public class OAuthPicasaActivity extends ListActivity {
  OAuthPicasaActivity act;
  
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         act = this;
         OAuth o = new OAuth(this);
         Token t = o.getToken();
  
  if (!t.isValidForReq()) {
          Intent intent = new Intent(this, AuthActivity.class);
          this.startActivity(intent);
         }
         if (t.isExpired()) {
                 o.getRequestToken();
         }
  
         DataFetcher df = new DataFetcher(t);
         df.fetchAlbums("sheranapress");
         String[] names = new String[] {}; // Add bridge code here to parse XML
                                           // from DataFetcher and populate
                                           // your List
  
         this.setListAdapter(new ArrayAdapter  < String > (this, R.layout.list_item,
                           names));
  
         ListView lv = getListView();
         lv.setTextFilterEnabled(true);
  
         lv.setOnItemClickListener(new OnItemClickListener() {
                 public void onItemClick(AdapterView  <?>  parent, View view,
                                 int position, long id) {
                            Toast.makeText (getApplicationContext(),
                                              ((TextView) view).getText(),
 Toast.
LENGTH_SHORT).show();
            }
       });
  
  }
  
}

You will see that this file is doing several things. First, it instantiates the OAuth class. Next, it retrieves the Token object and tests whether the token is valid to make a request in the isValidForReq() function. It also tests whether the token is expired in the isExpired() function. If the token is valid, then it goes onto instantiate the DataFetcher object that queries Picasa for a list of all albums belonging to the user, sheranapress. This is done in the df.fetchAlbums("sheranapress") line.

Obviously, the first time this application is run, there won’t be a valid Token object. The application handles this condition by first fetching an authorization code, and then fetching a request token with that authorization code (per Google’s OAuth 2 specification). Let’s see how this is done next.

Handling Authorization

Listing 8-2 shows the source code for the part of our application that handles authorization. If you look at the doAuth() function, you will see that a request to Google is made, and the application displays the response in a WebView object. A WebView object is a field that displays HTML content. You can think of it like a minimalistic browser. This allows the end user to log into her Google account and grant or deny our application access. The user is presented with the Google login web page and is asked to log in with her credentials. These credentials are not stored anywhere in our application. If he grants our application permission to use her Picasa stream, then Google sends back an authorization code. Our application will store this authorization code in the Token object. This is done in the ClientHandler object (see Listing 8-3).

Listing 8-2.  The Auth Activity Gets the Authorization Code.

package net.zenconsult.android;
  
import java.net.URI;
import java.net.URISyntaxException;
  
import org.apache.http.message.BasicNameValuePair;
  
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.webkit.WebView;
  
public class AuthActivity extends Activity {
        private BasicNameValuePair clientId = new BasicNameValuePair("client_id",
                         "200744748489.apps.googleusercontent.com");
        private BasicNameValuePair clientSecret = new BasicNameValuePair(
                         "client_secret", "edxCTl_L8_SFl1rz2klZ4DbB");
        private BasicNameValuePair redirectURI = new BasicNameValuePair(
                         "redirect_uri", "urn:ietf:wg:oauth:2.0:oob");
        private String scope = "scope=https://picasaweb.google.com/data/";
        private String oAuth = "https://accounts.google.com/o/oauth2/auth ?";
        private String httpReqPost = " https://accounts.google.com/o/oauth2/token ";
        private final String FILENAME = ".oauth_settings";
        private URI uri;
        private WebView wv;
        private Context ctx;
        private Token token;

        @Override
        public void onCreate(Bundle savedInstanceState) {
               super.onCreate(savedInstanceState);
               setContentView(R.layout.auth);
               doAuth();
  }
  public void doAuth() {
          try {
                 uri = new URI(oAuth + clientId + "&" + redirectURI + "&" + scope
                              + "&response_type = code");
                 wv = (WebView) findViewById(R.id.webview);
                 wv.setWebChromeClient(new ClientHandler(this));
                 wv.setWebViewClient(new MWebClient());
                 wv.getSettings().setJavaScriptEnabled(true);
                 wv.loadUrl(uri.toASCIIString());
                 Log.v("OAUTH", "Calling " + uri.toASCIIString());
          }catch (URISyntaxException e) {
                 e.printStackTrace();
          }
    }
}

Listing 8-3.  The ClientHandler Writes the Authorization Code to the Token Object.

package net.zenconsult.android;
  
import android.app.Activity;
import android.util.Log;

import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.widget.Toast;
  
public class ClientHandler extends WebChromeClient {
       private Activity activity;
       private OAuth oAuth;

       public ClientHandler(Activity act) {
               activity = act;
               oAuth = new OAuth(activity);
       }

       @Override
        public void onReceivedTitle(WebView view, String title) {
               String code = "";
               if (title.contains("Success")) {
                      code = title.substring(title.indexOf(' = ') + 1, title.length());
                      setAuthCode(code);
                      Log.v("OAUTH", "Code is " + code);
                      oAuth.getRequestToken();
                      oAuth.writeToken(oAuth.getToken());
                      Toast toast = Toast.makeText(activity.getApplicationContext(),
                                      "Authorization Successful", Toast.LENGTH_SHORT);
                      toast.show();
                      activity.finish();
               } else if (title.contains("Denied")) {
                      code = title.substring(title.indexOf(' = ') + 1, title.length());
                      setAuthCode(code);
                      Log.v("OAUTH", "Denied, error was " + code);
                      Toast toast = Toast.makeText(activity.getApplicationContext(),
                                      "Authorization Failed", Toast.LENGTH_SHORT);
                      toast.show();
                      activity.finish();
               }
         }
  
  public String getAuthCode() {
          return oAuth.getToken().getAuthCode();
  }
  
  public void setAuthCode(String authCode) {
          oAuth.getToken().setAuthCode(authCode);
  }
  
  @Override
  public void onProgressChanged(WebView view, int progress) {
  
  }
}

Think of the ClientHandler as an observer. It watches for a specific string—"Success"—in each HTML web page. If it finds the word, then we’ve got the correct authorization code, which means that our end user has approved our access.

After the authorization code has been written to the internal storage, you will need to fetch a request token. In Oauth, you will need a request token to begin the process of requesting access to any resources. Please refer to Figure 6-25 for the OAuth flow process. If you look at our ClientHandler code once more, you will see the lines oAuth.getRequestToken() and oAuth.writeToken(oAuth.getToken()). These two lines use the instantiated OAuth class (see Listing 8-4) to ask for a request token and then write it to the internal storage. The getRequestToken() function handles that part. It is also worth noting that whenever I mention storage, you should consider using encryption. Please refer to the “Data Storage in Android” section in Chapter 5 for more information on implementing secure data storage.

Listing 8-4.  If Authorization Code Is Valid, OAuth Class Gets Request Tokens from Google.

package net.zenconsult.android;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.webkit.WebView;
import android.widget.Toast;

public class OAuth {
        private BasicNameValuePair clientId = new BasicNameValuePair("client_id",
                        "200744748489.apps.googleusercontent.com");
        private BasicNameValuePair clientSecret = new BasicNameValuePair(
                        "client_secret", "edxCTl_L8_SFl1rz2klZ4DbB");
        private BasicNameValuePair redirectURI = new BasicNameValuePair(
                        "redirect_uri", "urn:ietf:wg:oauth:2.0:oob");
        private String scope = "scope=https://picasaweb.google.com/data/";
        private String oAuth = "https://accounts.google.com/o/oauth2/auth?";
        private String httpReqPost = "https://accounts.google.com/o/oauth2/token";
        private final String FILENAME = ".oauth_settings";
        private URI uri;
        private WebView wv;
        private Context ctx;
        private Activity activity;
        private boolean authenticated;
        private Token token;

        public OAuth(Activity act) {
                ctx = act.getApplicationContext();
                activity = act;
                token = readToken();

        }

        public Token readToken() {
                Token token = null;
                FileInputStream fis;
                try {
                        fis = ctx.openFileInput(FILENAME);
                        ObjectInputStream in = new ObjectInputStream(
                                       new BufferedInputStream(fis));
                        token = (Token) in.readObject();
                        if (token == null) {
                                token = new Token();
                                writeToken(token);
                        }
                        in.close();
                        fis.close();
                } catch (FileNotFoundException e) {
                        writeToken(new Token());
                } catch (StreamCorruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                } catch (ClassNotFoundException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                return token;
                }

        public void writeToken(Token token) {
                try {
                        File f = new File(FILENAME);
                        if (f.exists()) {
                                f.delete();
                        }
                        FileOutputStream fos = ctx.openFileOutput(FILENAME,
                                       Context.MODE_PRIVATE);

                        ObjectOutputStream out = new ObjectOutputStream(
                                       new BufferedOutputStream(fos));
                        out.writeObject(token);
                        out.close();
                        fos.close();
                } catch (FileNotFoundException e1) {
                        Log.e("OAUTH", "Error creating settings file");
                } catch (IOException e2) {
                        // TODO Auto-generated catch block
                        e2.printStackTrace();
                }
        }

        public void getRequestToken() {
                HttpClient httpClient = new DefaultHttpClient();
                HttpPost post = new HttpPost(httpReqPost);
                List  <  NameValuePair  > nvPairs = new ArrayList  <  NameValuePair  >  ();
                nvPairs.add(clientId);
                nvPairs.add(clientSecret);
                nvPairs.add(new BasicNameValuePair("code", token.getAuthCode()));
                nvPairs.add(redirectURI);
                nvPairs.add(new BasicNameValuePair("grant_type", "authorization_code"));
                try {
                        post.setEntity(new UrlEncodedFormEntity(nvPairs));
                        HttpResponse response = httpClient.execute(post);
                        HttpEntity httpEntity = response.getEntity();
                        String line = EntityUtils.toString(httpEntity);
                        JSONObject jObj = new JSONObject(line);
                        token.buildToken(jObj);
                        writeToken(token);
                } catch (UnsupportedEncodingException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                } catch (ClientProtocolException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                } catch (IOException e) {
                        if (e.getMessage().equals("No peer certificate")) {
                                Toast toast = Toast.makeTextimage
(activity.getApplicationContext(),
                                                "Possible HTC Error for Android 2.3.3",
                                                Toast.LENGTH_SHORT);
                            toast.show();
                        }
                        Log.e("OAUTH", "IOException " + e.getMessage());
                } catch (JSONException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }

        }

        public Token getToken() {
                return token;
        }

        public void setToken(Token token) {
                this.token = token;
        }
}

You might have already noticed that the token is being used as a singleton. It gets written to and read from the device’s internal storage. This allows different areas of the application to read and write to it during different phases of the authentication process. Ideally, this should be synchronized to ensure that reads and writes occur exclusively from one class.

I have provided the source code to the Token object in Listing 8-5. The object implements the Serializable interface; therefore, it can be written in its entirety to the internal store. Make sure you run it through your data storage encryptor for added security. The Token object contains little logic apart from checking its own expiry date.

Listing 8-5.  The Token Object

package net.zenconsult.android;
  
import java.io.Serializable;
import java.util.Calendar;
import org.json.JSONException;
import org.json.JSONObject;
public class Token implements Serializable {
        /**
        *
        */
        private static final long serialVersionUID = 6534067628631656760L;
        private String refreshToken;
        private String accessToken;
        private Calendar expiryDate;
        private String authCode;
        private String tokenType;
        private String name;

        public Token() {
                setExpiryDate(0);
                setTokenType("");
                setAccessToken("");
                setRefreshToken("");
                setName("");
        }

        public Token(JSONObject response) {
                try {
                      setExpiryDate(response.getInt("expires_in"));
                } catch (JSONException e) {
                      setExpiryDate(0);
                }
                try {
                      setTokenType(response.getString("token_type"));
                } catch (JSONException e) {
                      setTokenType("");
                }
                try {
                      setAccessToken(response.getString("access_token"));
                } catch (JSONException e) {
                      setAccessToken("");
                }
                try {
                      setRefreshToken(response.getString("refresh_token"));
                } catch (JSONException e) {
                      setRefreshToken("");
                }
        }
        public void buildToken(JSONObject response) {
                try {
                      setExpiryDate(response.getInt("expires_in"));
                } catch (JSONException e) {
                      setExpiryDate(0);
                }
                try {
                      setTokenType(response.getString("token_type"));
                } catch (JSONException e) {
                      setTokenType("");
                }
                try {
                      setAccessToken(response.getString("access_token"));
                } catch (JSONException e) {
                      setAccessToken("");
                }
                try {
                      setRefreshToken(response.getString("refresh_token"));
                } catch (JSONException e) {
                      setRefreshToken("");
                }
        }

        public boolean isValidForReq() {
                if (getAccessToken() != null && !getAccessToken().equals("")) {
                      return true;
                } else {
                      return false;
                }
        }

        public boolean isExpired() {
                Calendar now = Calendar.getInstance();
                if (now.after(getExpiryDate()))
                      return true;
                else
                      return false;
        }

        public String getRefreshToken() {
                      return refreshToken;
        }

        public void setRefreshToken(String refreshToken) {
                if (refreshToken == null)
                        refreshToken = "";
                        this.refreshToken = refreshToken;
        }

        public String getAccessToken() {
                      return accessToken;
        }

        public void setAccessToken(String accessToken) {
                if (accessToken == null)
                        accessToken = "";
                this.accessToken = accessToken;
        }

        public Calendar getExpiryDate() {
                      return expiryDate;
        }

        public void setExpiryDate(int seconds) {
                Calendar now = Calendar.getInstance();
                now.add(Calendar.SECOND, seconds);
                this.expiryDate = now;
        }

        public String getAuthCode() {
                      return authCode;
        }

        public void setAuthCode(String authCode) {
                if (authCode == null)
                authCode = "";
                this.authCode = authCode;
        }

        public String getTokenType() {
                      return tokenType;
        }

        public void setTokenType(String tokenType) {
                if (tokenType == null)
                tokenType = "";
                this.tokenType = tokenType;
        }

        public String getName() {
                      return name;
        }

        public void setName(String name) {
                this.name = name;
        }
}

Finally, there is the DataFetcher class (see Listing 8-6). You use this class to make all protected queries to Picasa. For example, you can use this class to fetch albums and photos or even to upload photos. Picasa sends back all its replies in XML (notice that I have left the XML parsing component out). If you want to know how to write a simple XML parser to read Picasa responses, then look in the book’s Appendix.

Listing 8-6.  The DataFetcher Class

package net.zenconsult.android;

import java.io.IOException;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

public class DataFetcher {
        private HttpClient httpClient;

        private Token token;
        public DataFetcher(Token t) {
                token = t;
                httpClient = new DefaultHttpClient();
        }
        public void fetchAlbums(String userId) {
                String url = " https://picasaweb.google.com/data/feed/api/user/ "
                                  + userId;
                try {
                     HttpResponse resp = httpClient.execute(buildGet(
                                   token.getAccessToken(), url));
                     if (resp.getStatusLine().getStatusCode() == 200) {
                                          HttpEntity httpEntity = resp.getEntity();
                                          String line = EntityUtils. toString(httpEntity);
                                          // Do your XML Parsing here
                                          }
                } catch (ClientProtocolException e) {
                                          // TODO Auto-generated catch block
                                          e.printStackTrace();
                } catch (IOException e) {
                                          // TODO Auto-generated catch block
                                          e.printStackTrace();
        }
        }
        public HttpGet buildGet(String accessToken, String url) {
                HttpGet get = new HttpGet(url);
                get.addHeader("Authorization", "Bearer " + accessToken);
                return get;
        }
}

Challenge Response

We very briefly discussed challenge response-based authentication in Chapter 6. Let’s take a closer look at challenge-response authentication techniques. What follows is a brief overview of the steps required, also shown in Figure 8-2. Bear in mind that this is simply a one-way authentication with the server authenticating the client:1.

  1. Client requests a secure resource.
  2. Server sends a challenge string C.
  3. Client generates a random string R.
  4. Client generates a hash based on C, R, and the user’s password.
  5. Client sends R and the hash back to the server.
  6. Server calculates hash based on the stored user password and R.
  7. Server sends back the requested resource if correctly authenticated; otherwise, an error message is sent back.

9781430240624_Fig08-02.jpg

Figure 8-2.  A graphical representation of the data exchange between client and server during a challenge-response session

Note  You could also have a mutual authentication scenario where the client authenticates the server.

Let’s write some simple code that helps us use challenge-response authentication techniques in our applications. You should evolve these sections of code to suit your own needs and then use them them in your applications. They can help reduce the exposure of your end users because you won’t be storing any credentials on your device. I’ve given you examples of both client and server-side code. The server-side code is written in Java, and it can be packaged as a Java Web Archive File (WAR file). To test it, package it as a WAR file and simply drop it in the deployment directory of your servlet container or application server.

Let’s start with the server-side code. We will create a Java servlet that will handle the HTTP communications with our client. Figure 8-3 shows the project structure. The structure illustrates that we have a fairly simple project with only four files.

9781430240624_Fig08-03.jpg

Figure 8-3.  Our challenge-response server-side project structure

One of them, the Hex.java file, is a utility class that I use for converting various data types into hexadecimal strings; the other, Constants.java, holds the username and password. These credentials will be used to compare what the client enters.

You will also notice that we are using the Apache Commons Codec library to help with our Base64 encoding and decoding. In this example, we are adapting the CRAM-MD5 authentication approach to use SHA1 hashes instead. (CRAM is the Challenge Response Authentication Mechanism.)

I’ll lay out the code first, and then explain what we’re trying to do. Let’s start with our servlet Login.java, shown in Listing 8-7. This code has two main branches:

  • Main Branch 1 handles cases where a request is received without the “challenge” parameter.
  • Main Branch 2 handles cases where a request is received with the “challenge” parameter.

Listing 8-7.  The Login Class

package net.zenconsult.android;
  
import java.io.IOException;
  
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
  
/**
        * Servlet implementation class login
        */
@WebServlet(description = "Login Servlet", urlPatterns = { "/login" })
        public class Login extends HttpServlet {
                private static final long serialVersionUID = 1 L;
  
        /**
                * @see HttpServlet#HttpServlet()
                */
                public Login() {
                super();
                // TODO Auto-generated constructor stub
                }
  
                /**
                * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
                * response)
                */
        protected void doGet(HttpServletRequest request,
                        HttpServletResponse response) throws ServletException,image
  IOException {
                HttpSession session = request.getSession();
                String param = request.getParameter("challenge");
                if (param != null) {
                CRAM c = (CRAM) session.getAttribute("challenge");
                if (c == null) {
                c = new CRAM();
                session.setAttribute("challenge", c);
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write(c.generate());
                } else {
                if (c.verifyChallenge(param.trim())) {
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().writeimage
(c.generateReply("Authorized"));
                session.invalidate();
                } else {
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().writeimage
(c.generateReply("Unauthorized"));
                session.invalidate();
                }
                }
                } else {
                CRAM c = new CRAM();
                session.setAttribute("challenge", c);
                response.setHeader("Content-Type", "text/xml");
                response.getWriter().write(c.generate());
                }
  
                }
  
                /**
                * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
                * response)
                */
                protected void doPost(HttpServletRequest request,
                HttpServletResponse response) throws ServletException,image
                IOException {
                // TODO Auto-generated method stub
                }
}

In each case, we are creating a CRAM object. This object will generate our challenge strings and also do a comparison of the user response. We associate the CRAM object with each HTTP session, so that the same challenge bytes are used for verification.

Now would be a great time to take a protocol-level look at what takes place between client and server (see Figure 8-4). The entire flow had four steps and is quite simple:

  1. The client requests a protected resource.
  2. The server replies with a challenge.
  3. The client uses the end-user credentials to calculate the response and send it back to the server.
  4. Finally, the server will calculate the same response, compare it, and decide whether the user is authorized.

9781430240624_Fig08-04.jpg

Figure 8-4.  The challenge response message flow

All of this is done without sending the user credentials over the Web.

The source code for the CRAM object is shown in Listing 8-8.

Listing 8-8.  The CRAM Class

package net.zenconsult.android;
  
import java.io.StringWriter;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Mac;
  
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
  
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
  
public class CRAM implements Constants {
  private final byte[] secret = new byte[32];
  
         public CRAM() {
                 SecureRandom sr = new SecureRandom();
                 sr.nextBytes(secret);
                 }
  
         public String generate() {
                 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
                 DocumentBuilder dBuilder = null;
                 try {
                       dBuilder = dbFactory.newDocumentBuilder();
                 } catch (ParserConfigurationException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                 }
                 Document doc = dBuilder.newDocument();
  
                 // Build Root
                 Element root = doc.createElement("ServerResponse");
                 doc.appendChild(root);
  
                 // Challenge Section
                 Element authChallenge = doc.createElement("AuthChallenge");
                 root.appendChild(authChallenge);
  
                 // The Challenge
                 Element challenge = doc.createElement("Challenge");
                 Text challengeText = doc.createTextNode(Base64
                               .encodeBase64String(secret));
                 challenge.appendChild(challengeText);
                 authChallenge.appendChild(challenge);
  
                 TransformerFactory tFactory = TransformerFactory.newInstance();
                 Transformer transformer = null;
                 try {
                       transformer = tFactory.newTransformer();
                 } catch (TransformerConfigurationException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                 }
                 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
                 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                 StringWriter sw = new StringWriter();
                 StreamResult res = new StreamResult(sw);
                 DOMSource source = new DOMSource(doc);
                 try {
                       transformer.transform(source, res);
                 } catch (TransformerException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                 }
                 String xml = sw.toString();
                 return xml;
                 }
         public boolean verifyChallenge(String userResponse) {
                 String algo = "HmacSHA1";
                 Mac mac = null;
                 try {
                        mac = Mac.getInstance(algo);
                 } catch (NoSuchAlgorithmException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                 }
                 SecretKey key = new SecretKeySpec(PASSWORD.getBytes(), algo);
  
                 try {
                 mac.init(key);
                 } catch (InvalidKeyException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                 }
                 String tmpHash = USERNAME + " " + Hex.toHex(mac.doFinal(secret));
                 String hash = Base64.encodeBase64String(tmpHash.getBytes());
                 return hash.equals(userResponse);
                 }
  
                 public String generateReply(String response) {
                 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
                 DocumentBuilder dBuilder = null;
                 try {
                 dBuilder = dbFactory.newDocumentBuilder();
                 } catch (ParserConfigurationException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                 }
                 Document doc = dBuilder.newDocument();
  
                 // Build Root
                 Element root = doc.createElement("ServerResponse");
                 doc.appendChild(root);
  
                 // Challenge Section
                 Element authChallenge = doc.createElement("AuthChallenge");
                 root.appendChild(authChallenge);
  
                 // Reply
                 Element challenge = doc.createElement("Response");
                 Text challengeText = doc.createTextNode(response);
                 challenge.appendChild(challengeText);
                 authChallenge.appendChild(challenge);
  
                 TransformerFactory tFactory = TransformerFactory.newInstance();
                 Transformer transformer = null;
                 try {
                 transformer = tFactory.newTransformer();
                 } catch (TransformerConfigurationException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                 }
                 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
                 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
                 StringWriter sw = new StringWriter();
                 StreamResult res = new StreamResult(sw);
                 DOMSource source = new DOMSource(doc);
                 try {
                 transformer.transform(source, res);
                 } catch (TransformerException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                 }
                 String xml = sw.toString();
                 return xml;
         }
  
}

At the point when the CRAM object is instantiated, a new 32-byte random number is generated. This is a field, and it is closely associated with the CRAM object. This random string of bytes will be used for further challenge generation and response verification.

Next comes the generate() function, which does nothing more than create a Base64 encoding of the random bytes that we generated. It then creates an XML response, along with this challenge string, and then returns it to the servlet so that it can be sent to the end user.

The next function, verifyChallenge(String userResponse), is an important one. It generates the response that a client should generate if the correct credentials were used. The original random byte sequence is hashed using the HMAC-SHA1 algorithm using the stored user password. The username is then prepended to this hash and Base64 encoded. Next, it is compared to the client response, which should be the sameprovided the username and password are correctly entered, of course.

Finally, the generateReply(String response) function will send back the word specified in the response variable as XML text. The servlet calls this function using either of the following words, depending on whether the client response is correct:

  • "Authorized"
  • "Unauthorized"

You could also have a special authorization cookie set to indicate that the session is authenticated. There are many ways in which this code can be improved and built upon. I’ve included basic code here, so that you can get a better understanding of how to implement a challenge-response authentication mechanism in your front- and back-end applications.

Now that we’ve looked at the server-side code, let’s write some code for the client side. I’ve shown the project structure in Figure 8-5. Once again, the skeletal project is fairly simple, with only three files, not counting the hexadecimal functions class. I will take you through the functionality of each file, starting with the entry point, ChallengeResponseClientActivity.java (see Listing 8-9). The code is fairly straightforward with the creation of a Comms object (see Listing 8-10) and a CRAM object (see Listing 8-11). The Comms object handles all network communication between the client and server, while the CRAM object handles the hash generation part. The CRAM object is very similar to the CRAM object on the server side. In this case, there is no verification component because the client does not verify the server. Instead, the CRAM object uses the HMAC-SHA1 to calculate the hash based on the server challenge.

Listing 8-9.  The Entry Point and Main Activity

package net.zenconsult.android;
  
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
  
public class ChallengeResponseClientActivity extends Activity {
         /** Called when the activity is first created. */
         @Override
         public void onCreate(Bundle savedInstanceState) {
                 super.onCreate(savedInstanceState);
                 setContentView(R.layout. main);
                 final Activity activity = this;
  
                 final Button button = (Button) findViewById(R.id.button1);
                 button.setOnClickListener(new View.OnClickListener() {
                         public void onClick(View v) {
                                 Comms c = new Comms(activity);
                                        String challenge = c.getChallenge();
                                        CRAM cram = new CRAM(activity);
                                        String hash = cram.generate(challenge);
                                        String reply = c.sendResponse(hash);
                                        if (c.authorized(reply)) {
                                                 Toast toast = Toast.makeText(
                                                         activityimage
.getApplicationContext(), "Login success",
         Toast.LENGTH_LONG);
                                            toast.show();
                                        } else {
                                                 Toast toast = Toast.makeText(
                                                         activityimage
.getApplicationContext(), "Login failed",
         Toast.LENGTH_LONG);
                                                 toast.show();
                                        }
                             }
                 });
         }
}

Listing 8-10.  The Comms Class Handles All HTTP Requests for This App.

package net.zenconsult.android;
  
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
  
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
  
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
  
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
  
public class Comms {
  private final String url = "http://192.168.3.117:8080/ChallengeResponse/login";
  private Context ctx;
  private DefaultHttpClient client;
  
  public Comms(Activity act) {
  ctx = act.getApplicationContext();
  client = new DefaultHttpClient();
  }
  
  public String sendResponse(String hash) {
  List  <  NameValuePair  >  params = new ArrayList  <  NameValuePair  >  ();
  params.add(new BasicNameValuePair("challenge", hash));
  String paramString = URLEncodedUtils.format(params, "utf-8");
  String cUrl = url + "?" + paramString;
  return doGetAsString(cUrl);
  }
  
  public boolean authorized(String response) {
  InputStream is = new ByteArrayInputStream(response.getBytes());
  DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
  DocumentBuilder db = null;
  Document doc = null;
  String reply = "";
  try {
  db = dbFactory.newDocumentBuilder();
  doc = db.parse(is);
  NodeList nl = doc.getElementsByTagName("Response");
  reply = nl.item(0).getTextContent();
  is.close();
  } catch (ParserConfigurationException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (SAXException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return reply.matches("Authorized");
  }
  
  public String getChallenge() {
  InputStream challengeText = doGetAsInputStream(url);
  DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
  DocumentBuilder db = null;
  Document doc = null;
  String challenge = "";
  try {
  db = dbFactory.newDocumentBuilder();
  doc = db.parse(challengeText);
  NodeList nl = doc.getElementsByTagName("Challenge");
  challenge = nl.item(0).getTextContent();
  challengeText.close();
  } catch (SAXException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (ParserConfigurationException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return challenge;
  }
  
  public String doGetAsString(String url) {
  HttpGet request = new HttpGet(url);
  String result = "";
  try {
  HttpResponse response = client.execute(request);
  int code = response.getStatusLine().getStatusCode();
  if (code == 200) {
  result = EntityUtils.toString(response.getEntity());
  } else {
  Toast toast = Toast.makeText(ctx, "Status Code " + code,
  Toast.LENGTH_SHORT);
  toast.show();
  }
  } catch (ClientProtocolException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return result;
  
  }
  
  public InputStream doGetAsInputStream(String url) {
  HttpGet request = new HttpGet(url);
  InputStream result = null;
  try {
  HttpResponse response = client.execute(request);
  int code = response.getStatusLine().getStatusCode();
  if (code == 200) {
  result = response.getEntity().getContent();
  } else {
  Toast toast = Toast.makeText(ctx, "Status Code " + code,
  Toast.LENGTH_SHORT);
  toast.show();
  }
  } catch (ClientProtocolException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  } catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
  }
  return result;
  
  }
}

Listing 8-11.  The CRAM Class

package net.zenconsult.android;
  
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
  
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
  
import android.app.Activity;
import android.util.Base64;
import android.widget.TextView;

 public class CRAM {
        private Activity activity;

        public CRAM(Activity act) {
                 activity = act;
  }

        public String generate(String serverChallenge) {
                String algo = "HmacSHA1";
                TextView pass = (TextView) activity.findViewById(R.id.editText2);
                byte[] server = Base64.decode(serverChallenge, Base64.DEFAULT);

                Mac mac = null;
                try {
                        mac = Mac.getInstance(algo);
                } catch (NoSuchAlgorithmException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                String keyText = pass.getText().toString();
                SecretKey key = new SecretKeySpec(keyText.getBytes(), algo);
                try {
                        mac.init(key);
                } catch (InvalidKeyException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                byte[] tmpHash = mac.doFinal(server);
                TextView user = (TextView) activity.findViewById(R.id.editText1);
                String username = user.getText().toString();
                String concat = username + " " + Hex.toHex(tmpHash);
                String hash = Base64.encodeToString(concat.getBytes(), Base64.URL_SAFE);
                return hash;
        }
}

On the client side, if all goes according to plan, your app will greet you with a wonderful “Login success” pop-up message, as shown in Figure 8-5.

9781430240624_Fig08-05.jpg

Figure 8-5.  A successful challenge-response authentication

Summary

I hope these examples give you a better understanding of how to implement alternate authentication mechanisms in your mobile and back-end web applications. By depending less and less on user credential storage, you are improving the security of your app significantly.

Implementing OAuth in your front- and back-end code is not going to be the easiest thing to accomplish. However, it can be rewarding to spend some initial effort and prepare a reusable set of libraries for your future code. The same goes for CRAM. These authentication methods aren’t the first thing that many developers will want to consider because of the effort involved. It can, however, ensure your app is more secure than the ones that store and forward user credentials over the wire.

Hopefully, you will consider what you have learned so far useful. My hopes are that you’ll be convinced that you haven’t been wasting your time on the abbreviation of this new protocol, known as the Challenge Response Authentication Protocol.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.44.52