KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > sun > slamd > example > LDAPDigestMD5SocketFactory


1 /*
2  * Sun Public License
3  *
4  * The contents of this file are subject to the Sun Public License Version
5  * 1.0 (the "License"). You may not use this file except in compliance with
6  * the License. A copy of the License is available at http://www.sun.com/
7  *
8  * The Original Code is the SLAMD Distributed Load Generation Engine.
9  * The Initial Developer of the Original Code is Neil A. Wilson.
10  * Portions created by Neil A. Wilson are Copyright (C) 2004.
11  * Some preexisting portions Copyright (C) 2002-2004 Sun Microsystems, Inc.
12  * All Rights Reserved.
13  *
14  * Contributor(s): Neil A. Wilson
15  */

16 package com.sun.slamd.example;
17
18
19
20 import java.io.*;
21 import java.net.*;
22 import java.security.*;
23 import java.util.*;
24 import netscape.ldap.*;
25 import com.sun.slamd.asn1.*;
26 import com.sun.slamd.common.*;
27
28
29
30 /**
31  * This class provides an implementation of an LDAP socket factory that can be
32  * used to perform authentication to the directory server using the DIGEST-MD5
33  * SASL mechanism. It is a relatively ugly hack because the LDAP SDK for Java
34  * does not provide very good support for SASL authentication.
35  * <BR><BR>
36  * There are several things that should be noted about this implementation:
37  * <UL>
38  * <LI>The <CODE>setAuthenticationInfo</CODE> method must be called to provide
39  * the identity and credentials of the user that is to be authenticated.
40  * This must be done before calling the <CODE>connect</CODE> method of the
41  * <CODE>LDAPConnection</CODE> object with which this socket factory is
42  * associated.</LI>
43  * <LI>When calling the <CODE>connect</CODE> method on the
44  * <CODE>LDAPConnection</CODE> object with which this socket factory is
45  * associated, you must only use the version that provides the host name
46  * and port number of the directory server. Do not use any version that
47  * specifies the LDAP protocol version or bind information because that
48  * will perform a bind using simple authentication and will negate the
49  * effect of the DIGEST-MD5 bind. Further, once the connection has
50  * been established, do not call any variants of the
51  * <CODE>authenticate</CODE> or <CODE>bind</CODE> methods.</LI>
52  * <LI>Because the DIGEST-MD5 authentication is performed outside of the LDAP
53  * SDK for Java, the SDK itself has no knowledge of that authentication.
54  * Therefore, methods like <CODE>getAuthenticationDN</CODE>,
55  * <CODE>getAuthenticationMethod</CODE>,
56  * <CODE>getAuthenticationPassword</CODE>, and
57  * <CODE>isAuthenticated</CODE> may not be used because they will provide
58  * an incorrect response.</LI>
59  * <LI>Because the authentication ID and credentials are provided outside the
60  * <CODE>makeSocket</CODE> method, this implementation is not threadsafe.
61  * Therefore, if it is expected that multiple threads may attempt to
62  * concurrently create connections using DIGEST-MD5 authentication, then
63  * they must each have their own instance of this socket factory. It is
64  * not sufficient to use synchronization in an attempt to prevent
65  * concurrent usage of the same instance.</LI>
66  * <LI>It is possible to use this socket factory in conjunction with another
67  * socket factory for additional functionality (e.g., DIGEST-MD5
68  * authentication over an SSL-based connection). To use this socket
69  * factory in conjunction with another socket factory, call the
70  * <CODE>setAdditionalSocketFactory</CODE> method to provide the
71  * additional socket factory. The <CODE>makeSocket</CODE> method of that
72  * socket factory will be invoked as part of the <CODE>makeSocket</CODE>
73  * method of this socket factory. Note that some socket factory
74  * implementations may not behave as expected when used in this
75  * manner.</LI>
76  * </UL>
77  *
78  *
79  * @author Neil A. Wilson
80  */

81 public class LDAPDigestMD5SocketFactory
82        implements LDAPSocketFactory
83 {
84   /**
85    * The set of characters that will be used to generate the cnonce.
86    */

87   public static final char[] CNONCE_ALPHABET =
88        ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +
89         "1234567890+/").toCharArray();
90
91
92
93   /**
94    * The algorithm used by JCE to perform MD5 hashing.
95    */

96   public static final String JavaDoc JCE_DIGEST_ALGORITHM = "MD5";
97
98
99
100   /**
101    * The ASN.1 type used to denote an LDAP bind request protocol op.
102    */

103   public static final byte LDAP_BIND_REQUEST_TYPE = 0x60;
104
105
106
107   /**
108    * The ASN.1 type used to denote an LDAP bind response protocol op.
109    */

110   public static final byte LDAP_BIND_RESPONSE_TYPE = 0x61;
111
112
113
114   /**
115    * The ASN.1 type used to denote the SASL credentials in an LDAP bind request.
116    */

117   public static final byte LDAP_SASL_CREDENTIALS_TYPE = (byte) 0xA3;
118
119
120
121   /**
122    * The ASN.1 type used to denote the SASL credentials in an LDAP bind
123    * response.
124    */

125   public static final byte LDAP_SERVER_SASL_CREDENTIALS_TYPE = (byte) 0x87;
126
127
128
129   /**
130    * The quality of protection that will be used for all authentications. This
131    * implementation does not support either integrity or confidentiality.
132    */

133   public static final String JavaDoc QOP_AUTH = "auth";
134
135
136
137   /**
138    * The name of the DIGEST-MD5 SASL mechanism as it must appear in LDAP bind
139    * requests.
140    */

141   public static final String JavaDoc SASL_MECHANISM_NAME = "DIGEST-MD5";
142
143
144
145   // An additional socket factory that can be used to create the connection with
146
// some additional layer of security (e.g., SSL/TLS).
147
LDAPSocketFactory socketFactory;
148
149   // The object used to create MD5 digests.
150
MessageDigest md5Digest;
151
152   // The random number generator used to create cnonce values.
153
SecureRandom random;
154
155   // The authentication ID that should be used when performing the
156
// authentication.
157
String JavaDoc authID;
158
159   // The clear-text password that should be used when performing the
160
// authentication.
161
String JavaDoc password;
162
163
164
165   /**
166    * Creates a new instance of this DIGEST-MD5 authenticator. Note that
167    * creating an instance of this class for the first time in the life of the
168    * JVM can take a few seconds because of the time required to intialize the
169    * entropy for the random number generator.
170    *
171    * @throws SLAMDException If a problem occurs while initializing this
172    * DIGEST-MD5 authenticator.
173    */

174   public LDAPDigestMD5SocketFactory()
175          throws SLAMDException
176   {
177     try
178     {
179       md5Digest = MessageDigest.getInstance(JCE_DIGEST_ALGORITHM);
180     }
181     catch (Exception JavaDoc e)
182     {
183       throw new SLAMDException("Unable to initialize the MD5 digestor: " + e,
184                                e);
185     }
186
187     random = new SecureRandom();
188
189     authID = null;
190     password = null;
191     socketFactory = null;
192   }
193
194
195
196   /**
197    * Specifies the authentication ID and password for use with the next
198    * connection.
199    *
200    * @param authID The authentication ID for use with the next connection.
201    * @param password The password for use with the next connection.
202    */

203   public void setAuthenticationInfo(String JavaDoc authID, String JavaDoc password)
204   {
205     this.authID = authID;
206     this.password = password;
207   }
208
209
210
211   /**
212    * Specifies an additional socket factory that should be used when creating
213    * connections to the directory server using this socket factory. This makes
214    * it possible to stack this socket factory on top of another one, which
215    * allows for things like using DIGEST-MD5 on top of an SSL-based connection.
216    *
217    * @param socketFactory The additional socket factory that should be used
218    * when creating connections to the directory server
219    * using this socket factory.
220    */

221   public void setAdditionalSocketFactory(LDAPSocketFactory socketFactory)
222   {
223     this.socketFactory = socketFactory;
224   }
225
226
227
228   /**
229    * Establishes a new connection to the directory server and performs a SASL
230    * bind using DIGEST-MD5 before handing the socket off to the Java SDK.
231    *
232    * @param host The address of the server to which the connection should be
233    * established.
234    * @param port The port number of the server to which the connection should
235    * be established.
236    *
237    * @return The socket that may be used to communicate with the directory
238    * server.
239    *
240    * @throws LDAPException If a problem occurs while creating the socket.
241    */

242   public Socket makeSocket(String JavaDoc host, int port)
243          throws LDAPException
244   {
245     if ((authID == null) || (password == null))
246     {
247       throw new LDAPException("Authentication ID and/or password has not been" +
248                               "specified.", LDAPException.PARAM_ERROR);
249     }
250
251
252     Socket socket;
253     if (socketFactory == null)
254     {
255       try
256       {
257         socket = new Socket(host, port);
258       }
259       catch (IOException ioe)
260       {
261         throw new LDAPException("Unable to connect to " + host + ":" + port +
262                                 " -- " + ioe, LDAPException.CONNECT_ERROR);
263       }
264     }
265     else
266     {
267       socket = socketFactory.makeSocket(host, port);
268     }
269
270
271     // Tap into the input and output streams and use them to create an ASN.1
272
// reader and writer.
273
InputStream inputStream;
274     OutputStream outputStream;
275     ASN1Reader asn1Reader;
276     ASN1Writer asn1Writer;
277     try
278     {
279       inputStream = socket.getInputStream();
280       outputStream = socket.getOutputStream();
281       asn1Reader = new ASN1Reader(inputStream);
282       asn1Writer = new ASN1Writer(outputStream);
283     }
284     catch (IOException ioe)
285     {
286       throw new LDAPException("Unable to get input and/or output stream -- " +
287                               ioe, LDAPException.CONNECT_ERROR);
288     }
289
290
291     // Bind the connection to the directory server.
292
try
293     {
294       doBind(asn1Reader, asn1Writer, host, authID, password);
295     }
296     catch (LDAPException le)
297     {
298       throw le;
299     }
300     catch (Exception JavaDoc e)
301     {
302       throw new LDAPException("Internal failure while processing the bind: " +
303                               e);
304     }
305
306
307     // Return the socket to the caller.
308
return socket;
309   }
310
311
312
313   /**
314    * Handles the process of actually performing the bind.
315    *
316    * @param asn1Reader The ASN.1 reader used top read responses from the
317    * server.
318    * @param asn1Writer The ASN.1 writer used to write requests to the server.
319    * @param host The address of the directory server, used to construct
320    * the digest-uri field for the authentication.
321    * @param authID The authentication ID of the user that is to perform
322    * the bind. It is generally in the form "dn:{userdn}".
323    * @param password The password for the user indicated in the auth ID.
324    *
325    * @throws LDAPException If any problem occurs while processing the bind.
326    */

327   private void doBind(ASN1Reader asn1Reader, ASN1Writer asn1Writer, String JavaDoc host,
328                       String JavaDoc authID, String JavaDoc password)
329           throws LDAPException
330   {
331     // First, create the LDAP message for the bind request.
332
ASN1Element[] saslCredentialElements =
333     {
334       new ASN1OctetString(SASL_MECHANISM_NAME),
335       new ASN1OctetString()
336     };
337
338     ASN1Element[] bindRequestElements =
339     {
340       new ASN1Integer(3),
341       new ASN1OctetString(),
342       new ASN1Sequence(LDAP_SASL_CREDENTIALS_TYPE, saslCredentialElements)
343     };
344
345     ASN1Element[] ldapMessageElements =
346     {
347       new ASN1Integer(1),
348       new ASN1Sequence(LDAP_BIND_REQUEST_TYPE, bindRequestElements)
349     };
350
351     ASN1Element messageElement = new ASN1Sequence(ldapMessageElements);
352
353
354     // Send the request to the server.
355
try
356     {
357       asn1Writer.writeElement(messageElement);
358     }
359     catch (IOException ioe)
360     {
361       throw new LDAPException("Unable to send the initial bind request to " +
362                               "the server: " + ioe,
363                               LDAPException.CONNECT_ERROR);
364     }
365
366
367     // Read the response from the server.
368
ASN1Element responseElement;
369     try
370     {
371       responseElement =
372            asn1Reader.readElement(Constants.MAX_BLOCKING_READ_TIME);
373     }
374     catch (ASN1Exception ae)
375     {
376       throw new LDAPException("Unable to decode the initial bind response " +
377                               "from the server: " + ae,
378                               LDAPException.UNAVAILABLE);
379     }
380     catch (IOException ioe)
381     {
382       throw new LDAPException("Unable to read the initial bind response " +
383                               "from the server: " + ioe,
384                               LDAPException.CONNECT_ERROR);
385     }
386
387
388     // Decode the element as a bind response.
389
String JavaDoc responseData = null;
390     try
391     {
392       ASN1Element[] elements = responseElement.decodeAsSequence().getElements();
393       if (elements.length != 2)
394       {
395         throw new LDAPException("Unable to decode the initial bind response " +
396                                 "from the server: response element had an " +
397                                 "invalid number of elements.",
398                                 LDAPException.UNAVAILABLE);
399       }
400
401       if (elements[1].getType() != LDAP_BIND_RESPONSE_TYPE)
402       {
403         throw new LDAPException("Unable to decode the initial bind response " +
404                                 "from the server: response element had an " +
405                                 "invalid protocol op type.",
406                                 LDAPException.UNAVAILABLE);
407       }
408
409       elements = elements[1].decodeAsSequence().getElements();
410       int resultCode = elements[0].decodeAsEnumerated().getIntValue();
411       if (resultCode != LDAPException.SASL_BIND_IN_PROGRESS)
412       {
413         throw new LDAPException("Unable to decode the initial bind response " +
414                                 "from the server: inappropriate result code.",
415                                 LDAPException.UNAVAILABLE);
416       }
417
418       for (int i=1; i < elements.length; i++)
419       {
420         if (elements[i].getType() == LDAP_SERVER_SASL_CREDENTIALS_TYPE)
421         {
422           responseData = elements[i].decodeAsOctetString().getStringValue();
423         }
424       }
425
426       if (responseData == null)
427       {
428         throw new LDAPException("Unable to decode the initial bind response " +
429                                 "from the server: could not obtain the " +
430                                 "server SASL credentials.",
431                                 LDAPException.UNAVAILABLE);
432       }
433     }
434     catch (ASN1Exception ae)
435     {
436       throw new LDAPException("Unable to decode the initial bind response " +
437                               "from the server: " + ae,
438                               LDAPException.UNAVAILABLE);
439     }
440
441
442     // Parse the response data. We need to get the nonce, the realm, and the
443
// character set.
444
StringTokenizer tokenizer = new StringTokenizer(responseData, ",");
445     String JavaDoc nonce = null;
446     String JavaDoc realm = null;
447     String JavaDoc charSet = "utf-8";
448     while (tokenizer.hasMoreTokens())
449     {
450       String JavaDoc token = tokenizer.nextToken();
451       int equalPos = token.indexOf("=");
452       String JavaDoc tokenName = token.substring(0, equalPos).toLowerCase();
453       String JavaDoc tokenValue = token.substring(equalPos+1);
454       if (tokenValue.startsWith("\""))
455       {
456         tokenValue = tokenValue.substring(1, (tokenValue.length() - 1));
457       }
458
459       if (tokenName.equals("nonce"))
460       {
461         nonce = tokenValue;
462       }
463       else if (tokenName.equals("realm"))
464       {
465         realm = tokenValue;
466       }
467       else if (tokenName.equals("charset"))
468       {
469         charSet = tokenValue;
470       }
471     }
472
473
474     // Make sure that at least the nonce and the realm were provided.
475
if ((nonce == null) || (nonce.length() == 0))
476     {
477       throw new LDAPException("Unable to decode the initial bind response " +
478                               "from the server: could not extract the nonce " +
479                               "from the server SASL credentials.",
480                               LDAPException.UNAVAILABLE);
481     }
482     else if ((realm == null) || (realm.length() == 0))
483     {
484       throw new LDAPException("Unable to decode the initial bind response " +
485                               "from the server: could not extract the realm " +
486                               "from the server SASL credentials.",
487                               LDAPException.UNAVAILABLE);
488     }
489
490
491     // At this point, we should have enough information to generate the
492
// response. Create values for the remaining response fields.
493
String JavaDoc cnonce = generateCNonce(Math.max(32, nonce.length()));
494     String JavaDoc nonceCount = "00000001";
495     String JavaDoc qop = "auth";
496     String JavaDoc digestURI = "ldap/" + host;
497
498     String JavaDoc response;
499     try
500     {
501       response = generateResponse(authID, password, realm, nonce, cnonce,
502                                   nonceCount, digestURI, charSet);
503     }
504     catch (Exception JavaDoc e)
505     {
506       throw new LDAPException("Internal failure while generating the " +
507                               "response value to send to the server: " + e,
508                               LDAPException.UNAVAILABLE);
509     }
510
511
512     // Assemble the full response to return to the server.
513
String JavaDoc responseStr = "username=\"" + authID + "\",realm=\"" + realm +
514                          "\",nonce=\"" + nonce + "\",cnonce=\"" + cnonce +
515                          "\",nc=" + nonceCount + ",qop=" + qop +
516                          ",digest-uri=\"" + digestURI + "\",response=" +
517                          response;
518
519
520     // Assemble the new bind request message.
521
saslCredentialElements = new ASN1Element[]
522     {
523       new ASN1OctetString(SASL_MECHANISM_NAME),
524       new ASN1OctetString(responseStr)
525     };
526
527     bindRequestElements = new ASN1Element[]
528     {
529       new ASN1Integer(3),
530       new ASN1OctetString(),
531       new ASN1Sequence(LDAP_SASL_CREDENTIALS_TYPE, saslCredentialElements)
532     };
533
534     ldapMessageElements = new ASN1Element[]
535     {
536       new ASN1Integer(2),
537       new ASN1Sequence(LDAP_BIND_REQUEST_TYPE, bindRequestElements)
538     };
539
540     messageElement = new ASN1Sequence(ldapMessageElements);
541
542
543     // Send the bind request to the directory server.
544
try
545     {
546       asn1Writer.writeElement(messageElement);
547     }
548     catch (IOException ioe)
549     {
550       throw new LDAPException("Unable to send the subsequent bind request to " +
551                               "the server: " + ioe,
552                               LDAPException.CONNECT_ERROR);
553     }
554
555
556     // Read the response from the server.
557
try
558     {
559       responseElement =
560            asn1Reader.readElement(Constants.MAX_BLOCKING_READ_TIME);
561     }
562     catch (ASN1Exception ae)
563     {
564       throw new LDAPException("Unable to decode the subsequent bind response " +
565                               "from the server: " + ae,
566                               LDAPException.UNAVAILABLE);
567     }
568     catch (IOException ioe)
569     {
570       throw new LDAPException("Unable to read the subsequent bind response " +
571                               "from the server: " + ioe,
572                               LDAPException.CONNECT_ERROR);
573     }
574
575
576     // Decode the element as a bind response.
577
try
578     {
579       ASN1Element[] elements = responseElement.decodeAsSequence().getElements();
580       if (elements.length != 2)
581       {
582         throw new LDAPException("Unable to decode the subsequent bind " +
583                                 "response from the server: response element " +
584                                 "had an invalid number of elements.",
585                                 LDAPException.UNAVAILABLE);
586       }
587
588       if (elements[1].getType() != LDAP_BIND_RESPONSE_TYPE)
589       {
590         throw new LDAPException("Unable to decode the subsequent bind " +
591                                 "response from the server: response element " +
592                                 "had an invalid protocol op type.",
593                                 LDAPException.UNAVAILABLE);
594       }
595
596       elements = elements[1].decodeAsSequence().getElements();
597       int resultCode = elements[0].decodeAsEnumerated().getIntValue();
598       if (resultCode == LDAPException.SUCCESS)
599       {
600         return;
601       }
602
603
604       String JavaDoc matchedDN = elements[1].decodeAsOctetString().getStringValue();
605       String JavaDoc errorMessage = elements[2].decodeAsOctetString().getStringValue();
606       throw new LDAPException("The bind attempt was not successful.",
607                               resultCode, errorMessage, matchedDN);
608     }
609     catch (ASN1Exception ae)
610     {
611       throw new LDAPException("Unable to decode the subsequent bind response " +
612                               "from the server: " + ae,
613                               LDAPException.UNAVAILABLE);
614     }
615   }
616
617
618
619   /**
620    * Generates the cnonce string that will be used for a request.
621    *
622    * @param length The number of characters to include in the cnonce.
623    *
624    * @return The generated cnonce string.
625    */

626   private String JavaDoc generateCNonce(int length)
627   {
628     char[] cnonceChars = new char[length];
629
630     for (int i=0; i < cnonceChars.length; i++)
631     {
632       cnonceChars[i] = CNONCE_ALPHABET[(random.nextInt() & 0x7FFFFFFF) %
633                                        CNONCE_ALPHABET.length];
634     }
635
636     return new String JavaDoc(cnonceChars);
637   }
638
639
640
641   /**
642    * Generates the appropriate DIGEST-MD5 response based on the provided
643    * information, as per the specification in RFC 2831.
644    *
645    * @param authID The authentication ID for the user.
646    * @param password The password for the user indicated by the auth ID.
647    * @param realm The realm for the user indicated by the auth ID.
648    * @param nonce The server-generated random string used in the digest.
649    * @param cnonce The client-generated random string used in the digest.
650    * @param nonceCount The number of times the provided nonce has been used by
651    * the client.
652    * @param digestURI The URI that specifies the principal name of the
653    * service in which the authentication is being performed.
654    * @param charset The character set to use when encoding the data.
655    *
656    * @return The generated DIGEST-MD5 response.
657    *
658    * @throws UnsupportedEncodingException If the specified character set is
659    * unsupported.
660    */

661   private String JavaDoc generateResponse(String JavaDoc authID, String JavaDoc password, String JavaDoc realm,
662                                   String JavaDoc nonce, String JavaDoc cnonce,
663                                   String JavaDoc nonceCount, String JavaDoc digestURI,
664                                   String JavaDoc charset)
665           throws UnsupportedEncodingException
666   {
667     String JavaDoc a1Str1 = authID + ":" + realm + ":" + password;
668     byte[] a1bytes1 = md5Digest.digest(a1Str1.getBytes(charset));
669
670     String JavaDoc a1Str2 = ":" + nonce + ":" + cnonce;
671     byte[] a1bytes2 = a1Str2.getBytes(charset);
672
673     byte[] a1 = new byte[a1bytes1.length + a1bytes2.length];
674     System.arraycopy(a1bytes1, 0, a1, 0, a1bytes1.length);
675     System.arraycopy(a1bytes2, 0, a1, a1bytes1.length, a1bytes2.length);
676
677     byte[] a2 = ("AUTHENTICATE:" + digestURI).getBytes(charset);
678
679     String JavaDoc hexHashA1 = getHexString(md5Digest.digest(a1));
680     String JavaDoc hexHashA2 = getHexString(md5Digest.digest(a2));
681
682     String JavaDoc kdStr = hexHashA1 + ":" + nonce + ":" + nonceCount + ":" + cnonce +
683                    ":" + QOP_AUTH + ":" + hexHashA2;
684     return getHexString(md5Digest.digest(kdStr.getBytes(charset)));
685   }
686
687
688
689   /**
690    * Encodes the provided byte array into a string of the hexadecimal digits
691    * corresponding to the values in the array. All the alphabetic hex digits
692    * (a through f) will be return in lowercase.
693    *
694    * @param bytes The byte array to be encoded.
695    *
696    * @return The hexadecimal string representation of the provided byte array.
697    */

698   private String JavaDoc getHexString(byte[] bytes)
699   {
700     StringBuffer JavaDoc buffer = new StringBuffer JavaDoc(2 * bytes.length);
701     for (int i=0; i < bytes.length; i++)
702     {
703       switch ((bytes[i] >> 4) & 0x0F)
704       {
705         case 0x00:
706           buffer.append("0");
707           break;
708         case 0x01:
709           buffer.append("1");
710           break;
711         case 0x02:
712           buffer.append("2");
713           break;
714         case 0x03:
715           buffer.append("3");
716           break;
717         case 0x04:
718           buffer.append("4");
719           break;
720         case 0x05:
721           buffer.append("5");
722           break;
723         case 0x06:
724           buffer.append("6");
725           break;
726         case 0x07:
727           buffer.append("7");
728           break;
729         case 0x08:
730           buffer.append("8");
731           break;
732         case 0x09:
733           buffer.append("9");
734           break;
735         case 0x0a:
736           buffer.append("a");
737           break;
738         case 0x0b:
739           buffer.append("b");
740           break;
741         case 0x0c:
742           buffer.append("c");
743           break;
744         case 0x0d:
745           buffer.append("d");
746           break;
747         case 0x0e:
748           buffer.append("e");
749           break;
750         case 0x0f:
751           buffer.append("f");
752           break;
753       }
754
755       switch (bytes[i] & 0x0F)
756       {
757         case 0x00:
758           buffer.append("0");
759           break;
760         case 0x01:
761           buffer.append("1");
762           break;
763         case 0x02:
764           buffer.append("2");
765           break;
766         case 0x03:
767           buffer.append("3");
768           break;
769         case 0x04:
770           buffer.append("4");
771           break;
772         case 0x05:
773           buffer.append("5");
774           break;
775         case 0x06:
776           buffer.append("6");
777           break;
778         case 0x07:
779           buffer.append("7");
780           break;
781         case 0x08:
782           buffer.append("8");
783           break;
784         case 0x09:
785           buffer.append("9");
786           break;
787         case 0x0a:
788           buffer.append("a");
789           break;
790         case 0x0b:
791           buffer.append("b");
792           break;
793         case 0x0c:
794           buffer.append("c");
795           break;
796         case 0x0d:
797           buffer.append("d");
798           break;
799         case 0x0e:
800           buffer.append("e");
801           break;
802         case 0x0f:
803           buffer.append("f");
804           break;
805       }
806     }
807
808     return buffer.toString();
809   }
810 }
811
812
Popular Tags