Archive for April, 2014

Base64 Encoding and Decoding in Java

Base64 encoding is a convenient way to represent binary data as plain text. This allows for transport of said binary data over a medium that is traditionally more text friendly. In the general case, simply storing binary data as text is quite handy sometimes.

The most common ways to accomplish this are either Base64 encoding, or to HEX. Base64 encoding takes 3 bytes of binary data and turns it into 4 bytes of text. HEX encoding takes 1 byte of binary data, and turns it into 2 bytes of text. 3 bytes of binary data would be transformed into 6 bytes with HEX. While the difference between 4 and 6 bytes isn’t that bad, if you multiply that difference millions of times, the savings really become significant. Therefore, Base64 encoding is more efficient in storage/transport than HEX.

So, here’s my shot at a simple Base64 encoding and decoding in Java. I hammered it out in about 15 minutes; hopefully it’s not horrendously bad. There are tons of other tried/tested Base64 encoders out there; this was an academic exercise at best … for now anyway …

/*
 * Copyright (c) 2014
 * Cole Barnes [cryptofreek{at}gmail{dot}com]
 * http://cryptofreek.org/
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package org.cryptofreek.common;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

public class CBase64Utils
{
  private static final String BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

  public static String encode(byte[] ba)
  {
    String strB64 = null;

    if (ba == null)
    {
      strB64 = null;
    }
    else
    {
      try
      {
        ByteArrayInputStream in = new ByteArrayInputStream(ba);
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        encode(in, out);

        out.flush();
        out.close();
        in.close();

        strB64 = new String(out.toByteArray());
      }
      catch (IOException e)
      {
        strB64 = null;
        e.printStackTrace();
      }
    }

    return strB64;
  }

  public static void encode(InputStream in, OutputStream out) throws IOException
  {
    if (in == null || out == null)
    {
      // nothing to do
    }
    else
    {
      byte[] buff = new byte[3];
      int nRead = 0;
      int nPaddingChars = 0;

      while ((nRead = in.read(buff)) > 0)
      {
        if (nRead == 1)
        {
          // pad last 2 bytes
          buff[1] = 0;
          buff[2] = 0;
          nPaddingChars = 2;
        }
        else if (nRead == 2)
        {
          // pad last byte
          buff[2] = 0;
          nPaddingChars = 1;
        }
        else
        {
          // buffer full, no need to pad
        }

        // a little help here from:
        //    http://www.wikihow.com/Encode-a-String-to-Base64-With-Java
        int n = ((buff[0] & 0xff) << 16) + ((buff[1] & 0xff) << 8) + (buff[2] & 0xff);

        byte[] baTmp = new byte[4];
        baTmp[0] = (byte) BASE64_CHARS.charAt((n >> 18) & 0x3f);
        baTmp[1] = (byte) BASE64_CHARS.charAt((n >> 12) & 0x3f);

        if (nPaddingChars == 2)
        {
          baTmp[2] = (byte) '=';
        }
        else
        {
          baTmp[2] = (byte) BASE64_CHARS.charAt((n >> 6) & 0x3f);
        }

        if (nPaddingChars == 1 || nPaddingChars == 2)
        {
          baTmp[3] = (byte) '=';
        }
        else
        {
          baTmp[3] = (byte) BASE64_CHARS.charAt(n & 0x3f);
        }

        out.write(baTmp);
        out.flush();
      }
    }
  }

  public static byte[] decode(String strB64)
  {
    byte[] ba = null;

    if (strB64 == null)
    {
      ba = null;
    }
    else if (strB64.length() == 0)
    {
      ba = new byte[0];
    }
    else
    {
      try
      {
        byte[] baB64 = strB64.getBytes("UTF-8");
        ByteArrayInputStream in = new ByteArrayInputStream(baB64);
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        decode(in, out);

        out.flush();
        out.close();
        in.close();

        ba = out.toByteArray();
      }
      catch (UnsupportedEncodingException e)
      {
        ba = null;
        e.printStackTrace();
      }
      catch (IOException e)
      {
        ba = null;
        e.printStackTrace();
      }
    }

    return ba;
  }

  public static void decode(InputStream in, OutputStream out) throws IOException
  {
    if (in == null || out == null)
    {
      // nothing to do
    }
    else
    {
      byte[] buff = new byte[4];
      int nBytesRead = 0;

      while ((nBytesRead = in.read(buff)) > 0)
      {
        if (nBytesRead != 4) { throw new IOException("The input was not a valid Base64 encoding; block read was less than 4 bytes."); }

        char c0 = (char) buff[0];
        char c1 = (char) buff[1];
        char c2 = (char) buff[2];
        char c3 = (char) buff[3];

        byte b0 = (byte) BASE64_CHARS.indexOf(c0);
        byte b1 = (byte) BASE64_CHARS.indexOf(c1);
        byte b2 = (c2 == '=')?0:(byte) BASE64_CHARS.indexOf(c2);
        byte b3 = (c3 == '=')?0:(byte) BASE64_CHARS.indexOf(c3);

        int n = ((b0 & 0x3f) << 18) + ((b1 & 0x3f) << 12) + ((b2 & 0x3f) << 6) + ((b3 & 0x3f));

        out.write((byte) (n >> 16) & 0xff);
        if( c2!='=' ) out.write((byte) (n >> 8) & 0xff);
        if( c3!='=' ) out.write((byte) (n) & 0xff);
      }
    }
  }

  public static void main(String[] args)
  {
    // example taken from:
    //    http://en.wikipedia.org/wiki/Base64
    String strKnown = "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.";
    String strKnownB64 = "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=";

    byte[] ba = strKnown.getBytes();

    String strB64 = encode(ba);
    System.out.println("Calculated Base64 string:  " + strB64);
    System.out.println("     Known Base64 string:  " + strKnownB64);
    System.out.println("                   Match:  " + strB64.equals(strKnownB64));

    System.out.println();

    byte[] baData = decode(strB64);
    String str = new String(baData);
    System.out.println("Decoded string:  " + str);
    System.out.println("  Known string:  " + strKnown);
    System.out.println("         Match:  " + str.equals(strKnown));
  }
}