Introduction
This article explains how to build a Password Protected Stream on top of System.IO.Stream
using decorator pattern. The stream can then be used as a normal stream, with slight changes in usage.
Background
This work is just a part of my exercise that I was doing while reading the book titled 'C# 3.0 Design Patterns' by 'Judith Bishop'. It is an exercise of the section 'Decorator Patterns'. I opted to make it a bit complex and more functional rather than just a simple one for the sake of an exercise.
Using the Code
The code has four logical parts:
- The
PasswordProtectedStream
class that extends System.IO.Stream
and overrides several required properties of the stream
class. The main points of interest are how I have overridden the read
and write
methods of Stream
.
- The '
DataEnvelop
' class that is marked as 'Serializable
' and it is the actual place holder for the data. This class is serialized to a class file using 'BinaryFormatter
'. To keep the code simpler, I have saved the data as plain text otherwise for a more real world scenario, encryption can be applied or another stream can be extended as 'EncryptedStream
' using the same 'Decorator Pattern' technique.
- A delegate '
ReadCompleteHandler
' and a corresponding event 'On_ReadComplete
'. This event passes the data to the client application after verifying the password. I couldn't make the data available directly from the read
method as the data to be read was more in length than the actual data (because of serialization). So this technique is used. However, I would request the readers if anyone can help me erase the delegate and make the data available in the buffer directly by resetting the position and length of data to be read.
- A class '
ReadCompleteEventArgs
' that is a parameter to the event 'On_ReadComplete
' and passes the read status and data to the client application.
The whole code works according to the flow below:
- You create an instance of
PasswordProtectedStream
with two arguments: A base stream and a password. - You put this stream into a
StreamReader
or StreamWriter
. - If you put it in
StreamWriter
, it will call the overridden 'Write
' method of the PasswordProtectedStream
. This method will create an instance of a seriliazable object of the class 'DataEnvelop
' and put the password in that object along with data and write it on the actual stream. - If you put it in
StreamReader
, the overridden 'Read' method is called for PasswordProtectedStream. It works slightly different than the usual way of how streams read. It will read all the bytes(including the headers also for the serialized object DataEnvelop
), check the password and it is correct, will pass the data to the 'On_ReadComplete' event for processing by the client application. This is done using 'ReadCompleteEventArgs
' class. Else if the password is wrong, no data is passed to the event and only failed status is reported so that it can be caught in the client application. The status is marked by an enum named 'Status
'
Shortcomings: Since this is the first version, so the code works fine on single reads and single writes. However it is likely to have problems on multiple reads and multiple writes. This issue can be resolved if anyone can suggest a way where I don't have to use delegates and events and can directly reset the buffer in the Read
method of the stream. I tried it but it was looping forever due to some seeking and positioning problems and also due to the length of the buffer having a chunk of 1024 bytes. All suggestions would be most welcome.
The whole code is as below:
public class PasswordProtectedStream : Stream
{
string password; Stream str; bool eventRaised = false;
public delegate void ReadCompleteHandler
(object s, ReadCompleteEventArgs e); public event ReadCompleteHandler On_ReadComplete;
public PasswordProtectedStream(Stream str,
string password) : base()
{
this.str = str;
this.password = password;
}
#region "Overridden Methods of Stream"
public override void Write(byte[] buffer,
int offset, int count) {
byte[] data = new byte[count];
for (int i = offset, j = 0; j < count; i++, j++)
data[j] = buffer[i];
DataEnvelop env = new DataEnvelop(data, password);
BinaryFormatter f = new BinaryFormatter();
f.Serialize(str, env); }
public override int Read(byte[] buffer, int offset, int count) {
int r = str.Read(buffer, offset, count);
byte[] newData = new byte[str.Length];
for (int i = 0; i < str.Length; i++) newData[i] = buffer[i];
MemoryStream mstr = new MemoryStream(newData);
BinaryFormatter f = new BinaryFormatter();
DataEnvelop env = (DataEnvelop)f.Deserialize(mstr);
if (env.password == password) {
if (On_ReadComplete != null && !eventRaised) {
On_ReadComplete(this, new ReadCompleteEventArgs
(ReadCompleteEventArgs.Status.SUCCESS,
env.data)); eventRaised = true; }
}
else {
if (On_ReadComplete != null && !eventRaised) {
On_ReadComplete(this, new ReadCompleteEventArgs
(ReadCompleteEventArgs.Status.FAILURE, null)); eventRaised = true; }
}
return r; }
public override void Close()
{
str.Close();
str.Dispose();
}
#endregion
#region "Overridden Properties of Stream"
public override void SetLength(long value)
{
str.SetLength(value);
}
public override long Seek(long offset, SeekOrigin origin)
{
return str.Seek(offset, origin);
}
public override long Position
{
get
{
return str.Position;
}
set
{
str.Position = value;
}
}
public override long Length
{
get { return str.Length; }
}
public override bool CanWrite
{
get { return str.CanWrite; }
}
public override void Flush()
{
str.Flush();
}
public override bool CanSeek
{
get { return str.CanSeek; }
}
public override bool CanRead
{
get { return str.CanRead; }
}
#endregion
[Serializable]
class DataEnvelop {
public byte[] data { get; set; }
public string password { get; set; }
public DataEnvelop(byte[] data, string password)
{
this.data = data;
this.password = password;
}
}
public class ReadCompleteEventArgs : EventArgs {
byte[] data; Status status;
public enum Status { SUCCESS, FAILURE }
public ReadCompleteEventArgs(Status status, byte[] data)
{
this.data = data;
this.status = status;
}
public byte[] Data { get { return data; } }
public Status ReadStatus { get { return status; } }
}
}
private void button1_Click(object sender, EventArgs e)
{
PasswordProtectedStream st = new PasswordProtectedStream
(new FileStream("C:/pwdsample.txt",
FileMode.Create), "12345"); StreamWriter w = new StreamWriter(st);
w.Write("Hye this is test"); w.Flush();
w.Close();
st.Close();
st.Dispose();
MessageBox.Show("Data Written Successfully");
}
private void button2_Click(object sender, EventArgs e)
{
PasswordProtectedStream st = new PasswordProtectedStream(new FileStream
("C:/pwdsample.txt", FileMode.Open), "12345"); st.On_ReadComplete += new PasswordProtectedStream.ReadCompleteHandler
(st_On_ReadComplete);
StreamReader w = new StreamReader(st);
w.ReadToEnd(); w.Close();
st.Close();
st.Dispose();
}
void st_On_ReadComplete
(object s, PasswordProtectedStream.ReadCompleteEventArgs e)
{
if (e.ReadStatus == PasswordProtectedStream.ReadCompleteEventArgs.
Status.FAILURE) MessageBox.Show("Wrong Password");
else
MessageBox.Show("Data Read back: " +
Encoding.ASCII.GetString(e.Data)); }
Points of Interest
- Design Patterns
- I/O Streams in .NET
History
- This is the first version of this sample application.