AddThis Social Bookmark Button

Print

Basic Crypto w/ the .NET Framework
Pages: 1, 2

Chaining Algorithms

Now we know how to take a stream and either encrypt or decrypt the information contained within using a symmetric algorithm. That's pretty handy, but there's a problem. The output from the encryption process is binary and not printable. What if we need to save the encrypted bytes in some kind of a text medium, like an XML file? An easy way to do this is to Base64 encode the data.



Luckily, the crypto framework provides a pair of ICryptoTransform implementations that will transform to and from a Base64 encoding. The classes are ToBase64Transform and FromBase64Transform. To use them, we will take advantage of CryptoStream chaining. Here are the replacement methods for the code above:


private void Encrypt(SymmetricAlgorithm algo) 
{
  using(Stream cipherText = 
        GetWriteableFileStream(s_ciphertext))
  using(ICryptoTransform b64 = 
        new ToBase64Transform())
  using(ICryptoTransform enc = 
	      algo.CreateEncryptor())
  using(CryptoStream toBase64 = 
	      GetWriteCryptoStream(cipherText, b64))
  using(CryptoStream crypt = 
        GetWriteCryptoStream(toBase64,enc))
  using(Stream input = 
        GetReadOnlyFileStream(s_plaintext)) {
    Pump(input,crypt);        
    //have to call, not called by Dispose
    crypt.Close(); 
  }
}

private void Decrypt(SymmetricAlgorithm algo) {
  using(Stream cipherText = 
        GetReadOnlyFileStream(s_ciphertext))
  using(ICryptoTransform b64 = 
        new FromBase64Transform())
  using(ICryptoTransform dec = 
        algo.CreateDecryptor())
  using(CryptoStream frBase64 = 
        GetReadCryptoStream(cipherText, b64))
  using(CryptoStream decrypt = 
        GetReadCryptoStream(frBase64,dec))
  using(Stream output = 
        GetWriteableFileStream(s_decrypted)) {
    Pump(decrypt,output);
    // have to call, not called by Dispose
    decrypt.Close(); 
  }
}

Notice that we're creating two CryptoStreams in each method instead of one. Also, notice the addition of the FromBase64Transform and the ToBase64Transform. The really interesting bits are these lines:


  using(CryptoStream toBase64 = GetWriteCryptoStream(cipherText,b64))
  using(CryptoStream crypt = GetWriteCryptoStream(toBase64, enc))

You pass the first CryptoStream into the constructor of the second. When you write into the outer CryptoStream (crypt), the write will be passed onto the underlying stream, toBase64. toBase64 then does its work and passes the results onto the underlying FileStream. Pretty nifty! You can continue to chain algorithms (or any other action, really) as much as you see fit. This model is incredibly useful and extensible, and was one of the better decisions made by the crypto framework team.

Hashing

Hashing uses a non-reversible algorithm, such as MD5 or SHA1, to create a unique short sequence of bytes from a larger sequence of bytes. Hashing is useful for verifying that the contents of a message or file has not changed since creation.

The primary interface to HashAlgorithm is the ComputeHash method. It's overloaded to take either a Stream or a byte[]. Here's a sample:


private void HashStream() {
  using(Stream input = GetReadOnlyFileStream(s_plaintext))
  using(HashAlgorithm hashAlg = HashAlgorithm.Create("MD5")) {
    byte[] hash = hashAlg.ComputeHash(input);
    PrintHash(s_plaintext, hash);
  }
}

private void HashBytes() {
  string password ="my-password";
  using(HashAlgorithm hash = HashAlgorithm.Create("MD5")) {
    byte[] hashed = hash.ComputeHash(Encoding.ASCII.GetBytes(password));
    PrintHash(password, hashed);
  }
}

private void PrintHash(string what, byte[] hash) {
  Console.WriteLine("Hash of {0}: {1}", what,
  Convert.ToBase64String(hash));
}

The code is quite a bit simpler than the encryption/decryption code. Also notice the alternate way to create a base64-encoded string from a byte[], Convert.ToBase64String(). Alternatively, instead of base64 encoding the hash code, you could easily put the byte[] into a SQL Server field of type binary. To use other algorithms, just change the string passed to HashAlgorithm.Create(). If you're going to use a KeyedHashAlgorithm, you can just use new on the actual implementation object. That way, you can pass the key into the constructor instead of having to set it after creation by a factory method.

Finally, if you use hashing to store passwords, be sure to use a salt. A salt is a small sequence of bytes prefixed or appended to the password before hashing. For example, instead of just hashing the password "my-password", you would hash "12345" + "my-password." Having a salt helps to prevent dictionary attacks against your password database, should anyone obtain a copy of it. There's a class called PasswordDeriveBytes in the crypto framework that looks interesting, but unfortunately, it appears to only be for generating keys for the symmetric algorithms.

Public Key Crypto

So far, we've covered private key encryption and hashing. The last area to cover is public-key-based cryptography, which is primarily used for key exchange and message signing. Public key encryption uses two keys. Any information encrypted with one key can be decrypted with the other. The two keys are known as the public key and the private key. The private key must be kept safe and secret, while the public key can be distributed to anyone who wants it, through a number of mediums. This makes public key encryption ideal for encrypting traffic between two parties without having to set up a secret key beforehand. The downside is that is very slow.

The framework supports two algorithms, RSA and DSA. RSA can be used for key exchange or to sign messages, while DSA can only be used to sign. First, let's check out the code to sign a message:


public void Run() {
  using(RSA rsa = RSA.Create()) {
    VerifySignature(rsa, CreateSignature(rsa));
  }
}

private byte[] CreateSignature(AsymmetricAlgorithm alg) {
  AsymmetricSignatureFormatter format = 
    new RSAPKCS1SignatureFormatter(alg);
  format.SetHashAlgorithm("SHA1");
  byte[] sig = format.CreateSignature(GetHash(s_plaintext));
  PrintSig(s_plaintext, sig);
  return sig;
}

private void VerifySignature(AsymmetricAlgorithm alg, byte[] sig) {
  AsymmetricSignatureDeformatter format = 
    new RSAPKCS1SignatureDeformatter(alg);
  format.SetHashAlgorithm("SHA1");
  Console.WriteLine(
    "The signature of {0} is {1}", 
    s_plaintext,
    format.VerifySignature(GetHash(s_plaintext), sig) ? "valid" :"invalid");
}

private byte[] GetHash(string path) {
  using(Stream input = 
        GetReadOnlyFileStream(path))
  using(HashAlgorithm hashAlg = 
        HashAlgorithm.Create("SHA1")) {
    return hashAlg.ComputeHash(input);
  }
}

private void PrintSig(string what, 
                      byte[] hash) 
{
  Console.WriteLine("Signature of {0}: {1}", 
                    what, 
                    Convert.ToBase64String(hash));
}

Again, this is pretty quick code for something that's really quite complex. The crypto team did a great job wrapping up support for signature generation. Basically, we create an AsymmetricSignatureFormatter to create a signature and an AsymmetricSignatureDeformatter to verify a signature. One very important thing: we must call SetHashAlgorithm() before calling CreateSignature or VerifySignature, and you must set the hash algorithm to the same type used to create the hash. Notice that I'm using SHA1 both to create the hash and for the signature. Again, to switch algorithms, just instantiate a different formatter.

The second interesting thing you can do with public key crypto is key exchange. With key exchange, I can create a key, sign it with my private key, encrypt it with my partner's public key, and then send it along. My partner can decrypt it with their private key, and verify the signature with my public key. After that's done, we've agreed on a secret key. This concept is codified with the AsymmetricKeyExchangeFormatter and AsymmetricKeyExchangeDeformatter. As of this writing, only RSA-based key exchanges are implemented. Here's a sample that exchanges a key between two parties:


  const string s_secret = "too many secrets"; 
  const string s_publicKeyFile = "blowery.pubkey";

  public override void Run() {
    
    byte[] exchange;
    string pubKeyStr;

    using(RSA privKey = RSA.Create()) {
      // make public key
      pubKeyStr = privKey.ToXmlString(false); 
      Console.WriteLine("My public key is {0}", pubKeyStr);

      // here we're Alice
      using(RSA pubKey = RSA.Create())
        pubKey.FromXmlString(pubKeyStr);
        exchange = CreateKeyExchange(pubKey);
      }
     
      // here we're Bob
      CrackKeyExchange(privKey, exchange);
    }
  }

  private byte[] CreateKeyExchange(RSA rsa) {
    AsymmetricKeyExchangeFormatter format = 
      new RSAPKCS1KeyExchangeFormatter(rsa);
    return format.CreateKeyExchange(CreateSecret());
  }

  private void CrackKeyExchange(RSA rsa, byte[] keyExchange) {
    AsymmetricKeyExchangeDeformatter format = 
      new RSAPKCS1KeyExchangeDeformatter(rsa);
    byte[] secret = format.DecryptKeyExchange(keyExchange);
    Console.WriteLine("Ah ha! The secret is '{0}'", 
                      Encoding.ASCII.GetString(secret));
  }

  private byte[] CreateSecret() {
    Console.WriteLine("Shhhh! The secret is '{0}'", 
                      s_secret);
    return Encoding.ASCII.GetBytes(s_secret);
  }

This example can be a bit confusing at first. Remember that during a key exchange, we have two parties; for instance, Alice and Bob. If Alice wants to send Bob something secret, she has to encrypt the information with Bob's public key. In the sample code, we're pretending first to be Alice, then Bob. Alice only has access to Bob's public key, so she loads it into an RSA algorithm object and creates the key exchange. Bob, who has both the public and private keys, can then crack the key exchange and extract the secret. It amazes me how little code this ends up needing for implementation. Obviously, you'll want a slightly stronger implementation of CreateSecret().

That's It!

Well, that's really it for System.Security.Cryptography. In this rather lengthy article, we've covered symmetric, asymmetric, and one-way algorithms. We've also covered how to use them together and exposed a couple of quirks. I hope this article has been as enlightening for you to read as it was for me to research and write.

Ben Lowery is a developer at FactSet Research Systems, where he works on all things great and small.


Return to ONDotnet.com