The B64Handler class provides methods to
convert ascii strings into Base64 encoded strings and to decode valid Base64
encoded strings into ascii strings.
Before looking into the accompanying code,
it would perhaps be useful to understand the relevance of this class in the
context of handling email protocols. Books and articles on programming
languages as well as those on networking systems usually contain examples of
SMTP dialogues (and their coded versions in the language under discussion)
between MTAs and UAs to illustrate the
mechanism of email transmission. However implementation of these examples as
shown may not always work properly. The reason is that many SMTP servers
require authentication before accepting mail from a UA and textbook examples
frequently omit the authentication procedure. If you do not authenticate the
transaction, you are likely to get a highly confusing message such as
"Relaying not permitted". Assuming you are sending mail through the
server of the ISP that provides internet access to you and, therefore, you have
a valid username and password, what you need to do when faced with such a
response is to carry out login authentication. A typical SMTP session with
login authentication may look like this:
Server: 220 xyz Server ESMTP (*)
Client: HELO abc
Server: 250 xyz OK, [nn.n.nn.nnn].
Client: AUTH LOGIN
Server: 334 VXNlcm5hbWU6
Client: (Base64 encoded username)
Server: 334 UGFzc3dvcmQ6
Client: (Base64 encoded password)
Server: 235 2.7.0 LOGIN authentication
successful
.
.
.
rest of the session as per SMTP
Note that the prompt-response sequence for
login (lines 5 to 8) involves Base64 encoding and decoding. The string
following '334' on line 5 is the Base64 encoded form of "Username:"
and the one follwing the '334' on line 7 is that of "Password:". The
user response must also be in a Base64 encoded form.
B64Handler is a convenient utility to take
care of the Base64 encoding (and decoding when needed). If you would like to
know more about Base64, you will find
the specification in RFC1521.
B64Handler provides methods for encoding an
ascii string into Base64 format and for decoding Base64 encoded strings into
ascii strings. It can be used in three ways. One way to use it is from the
command line. The code listing gives the usage. The second way is to use the
methods for encoding and decoding in your code. It is not necessary to
instantiate the class as all the methods are static. Finally, if you type
'B64Handler' without any arguments, a GUI will pop up through which short strings
can be encoded or decoded.
The method signatures are as follows:
public static StringBuilder EncodeString(StringBuilder
targetstring)
Returns Base64 encoded StringBuilder if encoding is possible. Otherwise
throws
Exception -- for instance if there is a non-ascii character in the
string to be encoded.
Parameter :
targetstring - the ascii string (represented as a StringBuilder) to be
encoded.
public static StringBuilder DecodeString(StringBuilder targetstring)
Returns ascii StringBuilder if decoding is possible. Otherwise throws
Exception -- for instance if
there is an illegal character in the string to be decoded.
Parameter :
targetstring - the Base64 encoded string (represented as a
StringBuilder) to be decoded.
public static StringBuilder dToBs(int decnum, int charnum)
Converts an integer decimal value, decnum, into a binary bitstring of
length
charnum and returns the binary bitstring. Throws exception if the decimal
number is negative or too large to fit into the specified size. The
return string
is in the form of a StringBuilder.
Parameters :
decnum -- the decimal integer to be converted into bitstring.
charnum -- the length of the bitstring.
public static int bsToD(StringBuilder bitstring)
Converts a binary bitstring into a decimal integer and returns the
integer.
Note that the bitstring is considered to represent a positive binary
number.
In other words the most significant bit is not treated as sign bit.
Parameter :
bitstring -- the binary bitstring (represented as a StringBuilder)to be
converted.
Finally the following points should be
noted:
1.
While RFC1521 recommends that all non-Base64 characters in a Base64
string should be ignored, it also accepts the alternate approach -
the one adapted here - that the presence of non-Base64 characters
may
lead to the decoding of a Base64 string being aborted.
2.
The maximum prescribed length for a Base64 encoded string is 76
characters. Accordingly a 'CRLF' is inserted after every 76
characters.
3.
It is assumed that Base64 strings have had linebreak characters removed
before they are passed to the decodeString method. ASCII strings
containing linebreak characters, however, may be passed to
encodeString
method. Such strings will be encoded prperly.
4.
RFC1521 does not explicitly specify that only ascii strings may be
encoded using the Base64 format. However ascii is the most common form
of characters used in my environment. Accordingly I have restricted the
coding/decoding to the ascii character set. Note that it is a relatively
simple matter to remove this restriction.
Now the code :
START OF CODE
using System;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
class B64Handler
{
static char [] b64table = new char []
{'A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
'3','4','5','6','7','8','9','+','/'};
public static StringBuilder EncodeString(StringBuilder targetstring)
{
StringBuilder stringcode = new StringBuilder();
StringBuilder codedstring = new StringBuilder();
StringBuilder stringtocode = new StringBuilder();
stringtocode = targetstring;
int l = stringtocode.Length;
int padnum = 0;
//reject zero-length strings
if(l==0)
{
throw new Exception("Error : No
characters in string");
}
// convert the string into concatenated binary byte
string
for(int i=0;i<l;i++)
{
// convert a character into a binary byte
string
int charval = stringtocode[i];
if(charval>127)
{
throw new Exception("Error :
Non ASCII character");
}
stringcode = stringcode.Append(dToBs(charval,
8));
}
// figure out how much padding is required
int k = l;
while((k%3)!=0)
{
padnum++;
k++;
}
// insert zeroes at the end of stringcode if
required
// to make the string length a multiple of 6 bits
if(padnum!=0)
{
if(padnum==1)
{
stringcode =
stringcode.Append("00");
}
else if(padnum==2)
{
stringcode = stringcode.Append("0000");
}
}
int stringcodelength = stringcode.Length;// length
of binary string
int cursor = 0;
int maxlength = 0;
StringBuilder bitstring
= new StringBuilder();
while(cursor<stringcodelength)
{
// find decimal value of each 6bit group
int value =
bsToD(bitstring.Append(stringcode.ToString(cursor,6)));
cursor = cursor+6;
// the calculated value can't be greater
than 63
if(value>63)
{
throw new Exception("Coding
error");
}
// update codedstring -- insert crlf after
every 76 characters
if(maxlength==76)
{
codedstring =
codedstring.Append("\r\n");
maxlength = 0;
}
codedstring =
codedstring.Append(b64table[value]);
maxlength++;
value = 0;
bitstring.Remove(0,6);
}
// add required padding
if(padnum==1)
{
codedstring = codedstring.Append("=");
}
else if(padnum==2)
{
codedstring =
codedstring.Append("==");
}
return codedstring;
}
public static StringBuilder DecodeString(StringBuilder targetstring)
{
//check if length multiple of 4. else print error
message.
//if decodable determine extent of padding and get
rid of pads
//find value corresponding to each character and
convert to
//a binary string. if illegal character print error
message,
//otherwise concatenate. delete trailing zeroes as
//per padding. take each byte and convert to
decimal, convert
//to character and create string.
StringBuilder stringcode = new StringBuilder();
StringBuilder decodedstring = new StringBuilder();
StringBuilder stringtodecode = new StringBuilder();
stringtodecode = targetstring;
int l = stringtodecode.Length;
int padnum = 0;
//reject zero-length strings
if(l==0)
{
throw new Exception("Error : No
characters in string");
}
//if string length not multiple of 4 then error
if(l%4!=0)
{
throw new Exception("Illegal string
length. Base64 decoding not possible.");
}
//otherwise remove padding
while(stringtodecode[l-1].Equals('='))
{
padnum++;
l--;
}
//maximum 2 padding characters can be there in a
valid Base64
//encoded string
if(padnum>2)
{
throw new Exception("Excessive
padding. Base64 decoding not possible.");
}
//for all the characters in the string find value,
convert
//into 6bit binary and
form concatenated string
for(int i=0;i<l;i++)
{
bool done = false;
int charval = 0;
while(!done)
{
char check = stringtodecode[i];
//find the value corresponding to
each character
if(b64table[charval].Equals(check))
{
done = true;
//convert the value into
6bit string
//and form concatenated
string
stringcode =
stringcode.Append(dToBs(charval, 6));
}
else
{
//try next entry in b64table
charval++;
}
//if character not in b64table then
error
if(charval>63)
{
throw new
Exception("Illegal character in string : " +check);
}
}
}
//proceed if no error
l = stringcode.Length;
//if padded then the last 2 or 4 bits must be zero.
//if not then error
//otherwise remove the zeroes
if(padnum==1)
{
if((stringcode.ToString(l-2,
2)).Equals("00"))
{
if((l-2)%8==0)
{
l = l-2;
}
else
{
throw new
Exception("Improper coding. Decoding not possible.");
}
}
else
{
throw new Exception("Improper
coding. Decoding not possible.");
}
}
else if(padnum==2)
{
if((stringcode.ToString(l-4,
4)).Equals("0000"))
{
if((l-4)%8==0)
{
l = l-4;
}
else
{
throw new
Exception("Improper coding. Decoding not possible.");
}
}
else
{
throw new Exception("Improper
coding. Decoding not possible.");
}
}
//proceed if no error
StringBuilder bitstring = new StringBuilder();
int cursor = 0;
while(cursor<l)
{
//
find decimal value of each 8bit group
int value =
bsToD(bitstring.Append(stringcode.ToString(cursor,8)));
//the decimal value can't be greater than
127 as the
//original string must have had only ascii
characters
if(value>127)
{
throw new Exception("Improper
coding. Decoding not possible.");
}
cursor = cursor+8;
// update decodedstring
decodedstring =
decodedstring.Append((char)value);
bitstring.Remove(0,8);
value = 0;
}
return decodedstring;
}
//converts a decimal value, decnum, into a binary bitstring of length
charnum.
public static StringBuilder dToBs(int decnum, int charnum)
{
//if the number is negative or too large to be
converted into
//a bitstring of specified size then throw
exception.
if(decnum >=
(int)Math.Pow(2,charnum)||decnum<0)
{
throw new Exception("Error: decimal value too
large for bitstring size.");
}
StringBuilder bitstring = new StringBuilder();
for(int i=charnum-1;i>-1;i--)
{
int powerof2 = (int)Math.Pow(2,i);
if(decnum >= powerof2)
{
bitstring = bitstring.Append('1');
decnum = decnum - powerof2;
}
else
{
bitstring = bitstring.Append('0');
}
}
return bitstring;
}
//converts binary bitstring to decimal value. leading zeroes not
essential
public static int bsToD(StringBuilder bitstring)
{
int stringlength = bitstring.Length;
int cursor = 0;
int value = 0;
for(int i=stringlength-1;i>-1;i--)
{
if(bitstring[cursor]=='1')
{
value =
value+(int)Math.Pow(2,i);
}
cursor++;
}
return value;
}
public static void Main(String[] args)
{
if(args.Length==0)
{
Application.Run(new Base64CoDec());
}
else if(args.Length==2)
{
if(args[1].Equals("encode"))
{
try
{
Console.WriteLine(B64Handler.EncodeString(new
StringBuilder(args[0])));
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
else
if(args[1].Equals("decode"))
{
try
{
Console.WriteLine(B64Handler.DecodeString(new StringBuilder(args[0])));
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
else
{
Console.WriteLine("Usage:
B64handler String s, String ende");
Console.WriteLine(" - s is the string -- without spaces --
to be
encoded or decoded");
Console.WriteLine(" - ende : \"encode\" > string
is to be
encoded");
Console.WriteLine(" - ende : \"decode\" > string
is to be
decoded");
Console.WriteLine(" - Type B64handler without arguments
to launch GUI");
}
}
else
{
Console.WriteLine("Usage:
B64handler String s, String ende");
Console.WriteLine(" - s is the string -- without spaces --
to be
encoded or decoded");
Console.WriteLine(" - ende : \"encode\" > string
is to be
encoded");
Console.WriteLine(" - ende : \"decode\" > string
is to be
decoded");
Console.WriteLine(" - Type B64handler without arguments
to launch GUI");
}
}
}
public class Base64CoDec : Form
{
Label lInput = new Label();
Label lOutput = new Label();
TextBox tInput = new TextBox();
TextBox tOutput = new TextBox();
Button bnEncode = new Button();
Button bnDecode = new Button();
public Base64CoDec()
{
Font lFont = new Font("Arial", 12);
float tFontSize = tInput.Font.Size;
Font tFont = new Font("Arial",
tFontSize);
tInput.TabIndex = 0;
tInput.Size = new Size(250, 25);
tInput.Location = new Point(15,45);
tInput.Font = tFont;
lInput.ForeColor = Color.Blue;
lInput.Font = lFont;
lInput.TabStop = false;
lInput.Text = "Input:";
lInput.Size = new Size(lInput.PreferredWidth, 25);
lInput.Location = new Point(15,15);
tOutput.TabStop = false;
tOutput.Size = new Size(250, 25);
tOutput.Location = new Point(15,115);
tOutput.Font = tFont;
lOutput.ForeColor = Color.Blue;
lOutput.TabStop = false;
lOutput.Font = lFont;
lOutput.Text = "Output:";
lOutput.Size = new Size(lOutput.PreferredWidth,
25);
lOutput.Location = new Point(15,85);
Font bFont = new
Font("Arial", 10);
bnEncode.TabIndex = 1;
bnEncode.Font = bFont;
bnEncode.Text = "Encode";
bnEncode.Size = new Size(70, 25);
bnEncode.Location = new Point(15,150);
bnEncode.BackColor = Color.LightPink;
bnEncode.Click += new
EventHandler(this.bnEncode_Click);
bnDecode.TabIndex = 2;
bnDecode.Font = bFont;
bnDecode.Text = "Decode";
bnDecode.BackColor = Color.LightGreen;
bnDecode.Location = new Point(90,150);
bnDecode.Size = new Size(70, 25);
bnDecode.Click += new EventHandler(this.bnDecode_Click);
this.Size = new Size(330,215);
this.FormBorderStyle = FormBorderStyle.Fixed3D;
this.Text = "Base64CODEC";
this.StartPosition = FormStartPosition.CenterScreen;
this.HelpButton = false;
this.MaximizeBox = false;
this.Controls.Add(tInput);
this.Controls.Add(lInput);
this.Controls.Add(tOutput);
this.Controls.Add(lOutput);
this.Controls.Add(bnEncode);
this.Controls.Add(bnDecode);
}
private void bnEncode_Click(Object sender, EventArgs e)
{
try
{
tOutput.ForeColor = Color.Black;
tOutput.Text =
B64Handler.EncodeString(new
StringBuilder(tInput.Text)).ToString();
}
catch(Exception be)
{
tOutput.ForeColor = Color.Red;
tOutput.Text = be.Message;
}
}
private void bnDecode_Click(Object sender, EventArgs e)
{
try
{
tOutput.ForeColor = Color.Black;
tOutput.Text =
B64Handler.DecodeString(new
StringBuilder(tInput.Text)).ToString();
}
catch(Exception be)
{
tOutput.ForeColor = Color.Red;
tOutput.Text = be.Message;
}
}
}
END OF CODE
To compile and run this program save it as
'B64Handler.cs'. The command 'csc B64Handler.cs' will compile the code and save
it as 'B64Handler.exe'. If you want to use the coding/decoding functions in
your code you can call the static methods 'EncodeString' or 'DecodeString'. The
technique for calling methods in external exe files will not be discussed here.
The command line usage is as follows:
Usage: B64handler String s, String ende
- s is the string -- without spaces -- to be encoded or decoded
- ende : encode means string s is to be
encoded
- ende : decode means string s is to be decoded
To use the GUI type B64Handler without any
arguments.
CODE DISCUSSION:
I strongly recommend a study of RFC1521
before trying to analyze the code. Once the algorithm for Base64 encoding and
decoding is known the basic logic of the programme can be easily understood.
>From the programming point of view there are, however, some issues of interest
that should be commented on.
B64Handler is basically a utility programme
and it does not need any internal data structure other than 'b64table'. It is
therefore natural to think of making the methods available without the need to
create an object of type B64Handler. In other words all the methods should be
static and to make it so, 'b64table' too needs to be static.
The next point to be noted is the extensive
use of StringBuilders. The functional requirements could also have been met by
strings. Then why StringBuilders? The answer lies in the fact that strings are immutable.
That is, once created, they cannot be changed. The methods that allow string
modification actually create a new string incorporating the modifications and
discard the old one. This makes string modification expensive in terms of
memory usage and time. As I need to keep on modifying the strings I work with
in B64Handler, I use StringBuilders and not strings.
B64handler also provides a good example of
how to use Exceptions to handle errors and abnormal situations. Note that many
error conditions have been checked for and each time an error is detected an
Exception is thrown. Whenever an Exception is thrown the method that has
encountered the error returns. The calling method has two options: it can
either handle the error or throw another Exception. In this case I have
centralised the final handling of all Exceptions at the level which calls the
encoding/decoding methods. At this level action is taken to handle the error
conditions and no further Exception is thrown. If this final handling of an Exception
is not provided for in the programme, it will come to an ignominious end and an
"Unhandled Exception" message will be printed on the console. One
must, therefore, plan the Exception handling structure of a programme with due
care. Remember that an extensive error checking scheme together with a well
structured Exception handling capability enhances the robustness of a
programme.
I shall not comment here on that part of
the programme (class Base64CoDec) that deals with the GUI. This is because I am
working on a multipart article on GUI and GDI+. Matters related to user
interfaces will be examined in that forthcoming series. In the meanwhile do
mail me your comments on this article and/or your questions if any.