|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionEncryption is one of those things I prefer not to touch. Yet, it sometimes comes in my way, and I can do nothing to avoid an encounter. This time, I decided to confront it instead. I need to password protect a file?! I will up you one with password protecting a stream! That is exactly what I did. This article is here because I really want feedback on any potential loopholes I’ve missed; how can this code be made more secure? And no, I am not working for a credit card company, so there is no immediate security threat to anyone. Which algorithm to choose?Let’s go briefly over the available options. Which one suites requirements of encrypting a stream the best? Security through obscurityOne could build security through obscurity by writing a proprietary encryption algorithm. There are two drawbacks to this approach, at least in a production environment.
Why bother with the hard and insecure way when .NET offers cryptography classes. What do these classes have to offer and how do they compare to each other? Public key encryption (asymmetric)Asymmetric encryption relies on a public / private key pair. The public key is used to encrypt data, while the private key is used to decrypt it. The two keys are mathematically linked. Since I am not a mathematician, I would never attempt creating such a key pair based on a password. Other disadvantages of asymmetric encryption are:
Secret key encryption (symmetric)This type of encryption is well suited for encrypting streams. That is the preferred way as long as neither the key nor the initialization vector are hard-coded. The program can be disassembled and the secret information discovered. So, we have contradictory requirements to fulfill. One one hand, we must not use hard-coded values. On the other hand, we must not use random or default values either. What we need is to use the password string as the source for the key. Thankfully, .NET provides exactly a class that we need: Tiny piece of code that almost workedThe easiest way to write a public PwdTdsCryptoStream(System.IO.Stream Stream,
CryptoStreamMode Mode, string Password)
: base(Stream, CreateTripleDESTransform(Mode, Password), Mode)
{
}
To initialize the base private static ICryptoTransform CreateTripleDESTransform(
CryptoStreamMode Mode, string Password)
{
byte[] key = null;
byte[] pdbsalt = null;
byte[] iv = null;
try
{
// Salt byte array.
pdbsalt = GetPdbSalt(Password);
// Create PasswordDeriveBytes object that will generate
// a Key for TripleDES algorithm.
PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, pdbsalt);
iv = new byte[8] { 0, 0, 0, 0, 0, 0, 0, 0 };
// Create a private key for TripleDES algorithm.
// The iv parameter is not currently used.
// * http://blogs.msdn.com/shawnfa/archive/2004/04/14/113514.aspx
key = pdb.CryptDeriveKey("TripleDES", "SHA1", 192, iv);
switch (Mode)
{
case CryptoStreamMode.Read:
return TripleDES.Create().CreateDecryptor(key, iv);
case CryptoStreamMode.Write:
return TripleDES.Create().CreateEncryptor(key, iv);
default:
return null;
}
}
catch (CryptographicException)
{
return null;
}
finally
{
if (key != null)
Array.Clear(key, 0, key.Length);
if (pdbsalt != null)
Array.Clear(pdbsalt, 0, pdbsalt.Length);
if (iv != null)
Array.Clear(iv, 0, iv.Length);
}
}
Woohoo!! The password protected crypto stream is done! Battling the Bad DataThis seemed way too easy to be true. And testing proved exactly that. I wrote a test application that password protected and unprotectes files using the new stream. Setting password worked every time. Un-protecting using a correct password also worked every time, but un-protecting using a bad password would crash and burn with This Bad Data exception was killing me. When keys don't match, it is thrown on every call to the At this point, I was really lucky to guess that I should try to dispose off public class PwdTdsCryptoStream : CryptoStream
{
private static ICryptoTransform m_CryptoTransform;
........
private static ICryptoTransform CreateTripleDESTransform(
CryptoStreamMode Mode, string Password)
{
........
switch (Mode)
{
case CryptoStreamMode.Read:
m_CryptoTransform = TripleDES.Create().CreateDecryptor(key, iv);
return m_CryptoTransform;
........
}
public override int ReadByte()
{
try
{
return base.ReadByte();
}
catch (CryptographicException)
{
m_CryptoTransform.Dispose();
this.Close();
}
return -1;
}
}
This setup has worked to perfection! A Bad Data exception was caught as usual. Now, the only problem that remains is the static
Done! That takes care of static. Now that the toughest hurdles are out of the way, the rest is child’s play. Here’s the final implementation of the public class PwdTdsCryptoStream : CryptoStream
{
private ICryptoTransform m_CryptoTransform;
private bool m_bDisposed;
public PwdTdsCryptoStream(System.IO.Stream Stream,
CryptoStreamMode Mode, string Password)
: this(Stream, CreateTripleDESTransform(Mode, Password), Mode)
{
}
public PwdTdsCryptoStream(System.IO.Stream Stream,
ICryptoTransform Transform, CryptoStreamMode Mode)
: base(Stream, Transform, Mode)
{
m_CryptoTransform = Transform;
m_bDisposed = false;
}
private static ICryptoTransform CreateTripleDESTransform(
CryptoStreamMode Mode, string Password)
{
byte[] key = null;
byte[] pdbsalt = null;
byte[] iv = null;
try
{
// Salt byte array.
pdbsalt = GetPdbSalt();
// Create PasswordDeriveBytes object that will generate
// a Key for TripleDES algorithm.
PasswordDeriveBytes pdb = new PasswordDeriveBytes(Password, pdbsalt);
iv = new byte[8] { 0, 0, 0, 0, 0, 0, 0, 0 };
// Create a private key for TripleDES algorithm.
// The iv parameter is not currently used.
// * http://blogs.msdn.com/shawnfa/archive/2004/04/14/113514.aspx
key = pdb.CryptDeriveKey("TripleDES", "SHA1", 192, iv);
switch (Mode)
{
case CryptoStreamMode.Read:
return TripleDES.Create().CreateDecryptor(key, iv);
case CryptoStreamMode.Write:
return TripleDES.Create().CreateEncryptor(key, iv);
default:
return null;
}
}
catch (CryptographicException)
{
return null;
}
finally
{
if (key != null)
Array.Clear(key, 0, key.Length);
if (pdbsalt != null)
Array.Clear(pdbsalt, 0, pdbsalt.Length);
if (iv != null)
Array.Clear(iv, 0, iv.Length);
}
}
private static byte[] GetPdbSalt()
{
RNGCryptoServiceProvider Gen = new RNGCryptoServiceProvider();
// Byte array of the same size as SHA1 hash, which is 160 bits.
// Would PasswordDeriveBytes benefit from a larger size salt array?
byte[] arrRandom = new byte[20];
// Fill the array with random values.
Gen.GetBytes(arrRandom);
return arrRandom;
}
public override int ReadByte()
{
try
{
return base.ReadByte();
}
catch (CryptographicException)
{
KillStream();
}
return -1;
}
public override void Close()
{
if (!m_bDisposed)
m_CryptoTransform.Dispose();
}
private void KillStream()
{
m_CryptoTransform.Dispose();
m_CryptoTransform = null;
m_bDisposed = true;
throw new BadPasswordException();
}
}
The only noticeable difference is the I have just read "Asynchronous Method Invocation", a great article by Mike Peretz. It convinced me not to override Test applicationTo test the password-protected stream, I wrote a little application that password-protects and un-protects files. Once a file is password protected, it can no longer be opened by its associated program. Since a file loses its program association, there is no point keeping its original extension. I use a .pbin extension for password protected files. However, we need to restore the file extension eventually, when the password is successfully verified. To do so, the original file name is the first bit of information written to the crypto stream. First, we get a byte array that represents the file name. The length of this byte array is written to the crypto stream first. The length itself is stored as a byte array of two bytes, which represents an unsigned short. Next, we write the file name byte array. Then, we copy the contents of the file to the password protected stream. // First, store file name length and file name.
byte[] arrName = Encoding.Unicode.GetBytes(OriginalName);
byte[] arrLength = BitConverter.GetBytes((System.UInt16)arrName.Length);
strmPwdProtect.Write(arrLength, 0, arrLength.Length);
strmPwdProtect.Write(arrName, 0, arrName.Length);
CopyStream(strmInput, strmPwdProtect);
Un-protecting a file is the reverse of the procedure above. First, the size of the file name is read and converted to try
{
// Read the file name from the decrypted stream.
byte[] arrLength = new byte[sizeof(System.UInt16)];
strmPwdProtect.Read(arrLength, 0, arrLength.Length);
ushort uLength = BitConverter.ToUInt16(arrLength, 0);
byte[] arrName = new byte[uLength];
strmPwdProtect.Read(arrName, 0, arrName.Length);
string strFilename = Encoding.Unicode.GetString(arrName);
DecryptedFileName = DirectoryName + "\\" + strFilename;
// Create the decrypted file.
strmOutput = new FileStream(DecryptedFileName,
FileMode.Create,
FileAccess.Write);
CopyStream(strmPwdProtect, strmOutput);
}
catch (BadPasswordException)
{
MessageBox.Show("Bad password");
}
Notes and things to avoid
ConclusionLiterally, in several lines of code, a References:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||