12 Public Key Cryptography
Asymmetric cryptography, or public key cryptography, uses two keys. One key is used to encrypt data and the other key is used to decrypt the data. Typically, one of these keys is made public which means that, for example, anyone can encrypt data but only the person with the other key, the private key, can decrypt the data. This solves the issue of key exchange because the public key by definition can be made public and anyone can use it to encrypt a secret (symmetric) key to be shared with the person with the private key.
Public key algorithms are quite slow, and it is not feasible to use it to encrypt large amounts of data. However, such algorithms can be used to encrypt small amounts of data (for example a hash) and if a user does this with their private key it means that anyone can decrypt the data. This is not done to secure the data, and usually such an action is applied to public data anyway; it is done to provide authentication and to preserve integrity. Since the public key can only decrypt data encrypted with the private key it means that if the public data can be obtained from the cyphertext with the public key then it means that the cyphertext was generate with the private key. This is the principle of digital signatures.
12.1 Public Key Algorithms
It should be stated early on that it is not feasible to use public key cryptography to encrypt large amounts of data. The algorithms involved are just too slow. Instead, public key cryptography is more suited to encrypt small amounts of data, but the asymmetric property makes public key cryptography vital for digital signatures and key exchange, which are topics that are covered in later sections. In this section we will look at the public key algorithms that form the basis of those protocols.
The base class, AsymmetricAlgorithm, factors the common
features of public key cryptography. These algorithms are based on public and
private keys which are typically very large and unwieldy for a human to
handle. AsymmetricAlgorithm defines methods, FromXmlString and
ToXmlString, that allow algorithms to be initialized with a key
(a single key or a key pair) represented by XML, or to persist a public key or a key pair to
XML. The format of this XML is defined by KeyValue in the
XML Digital
Signature Standard. In effect, these XML schemas persist very large
numbers and it does this by ordering the number in big-endian form and then
applying
base64 encoding.
There are two public key classes in the framework, and these are shown in the following table:
| Class | Description |
|---|---|
DSA |
Digital Signature Algorithm. The US government standard for digital signatures. 512 to 1024 bit keys. |
RSA |
Designed by Ron Rivest, Adi Shamir and Len Adleman at MIT. 384 - 16384 bit keys. Widespread and considered secure if long keys (at least 1024 bits) are used. |
DSA is only used for for digital signatures, whereas RSA
can be used for signatures and encryption. Similar to the framework
symmetric key classes,
the classes in the table above are abstract classes with a static Create
method. Create will create an instance of the default
implementation of the algorithm, for RSA this will be
RSACryptoServiceProvider and for DSA this will be
DSACryptoServiceProvider. The RSA class has two methods,
EncryptValue and DecryptValue, which appear to be
there to allow you to perform encryption. However, the
RSACryptoServiceProvider subclass will throw an exception if you call
these methods. This is actually a good thing because these methods do raw
encryption without padding, so if you were to be able to
call these methods you would have to make sure that you use a good padding
algorithm on the data otherwise the result would be insecure. The
RSACryptoServiceProvider class provides two methods, Encrypt
and Decrypt which will perform the encryption/decryption using
either OAEP
or PKCS#1 v1.5 padding.
To see what the RSA keys look like create a file (rsa.cs)
with the following, then compile and run it:
using System.Security.Cryptography;
class App
{
static void Main()
{
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
Console.WriteLine(rsa.ToXmlString(true));
}
}
When you create an instance of RSACryptoServiceProvider a
random key pair will be created.
This is an interesting point because key generation is a slow process, so
creating an instance of the class will eat CPU cycles. Indeed, the pattern of
calling the default constructor and then initializing the object with the
FromXmlString method will take longer than it really should. Unfortunately,
there is no constructor that takes an XML string parameter.
There are two ways to get around this: use a key pair in a cryptographic
container or use a certificate. |
The call to ToXmlString will
create an XML representation of the key. Here is the result of one run (I have added some newlines to make it
readable):
<Modulus>uTszyKHxlTIpFRlH2U4j2Nar3Z56DIA53Fd+MkiNlIpByC1qu0LO50nxVH
/noclLnQhI1+8oud5zj6ZwMviCv8gGaBA+j2eynfwwurhM7zkyW9hdB3ByWshlkm
mOUW2IlkgBpuFc4QoLMlqgedQclPJqrOAmhtzWiUfQUR/pFf8=</Modulus>
<Exponent>AQAB</Exponent>
<P>5vlvWAs9PLxdePiTI0cWA8zW2t1GyoTw7PWZYYT7829FP5KQeSKrESfZ1Gwi4suZ
2eIoplnOu+1XojkiJREgTw==</P>
<Q>zUz8Ojplv3Kyr23tTFQNaPTENACHnaaYBOMO5CJGMQC1qgkaBMYrodjT/eo3K8ej
lLFZ2Q9WGjciHhzSAGgTUQ==</Q>
<DP>xYdGKKab/UgeLClxM/dEJYXVrSEVvHaK0CuNu6+OBPcA4shGE8KJR8er65V7FDg
I4CQgnXsqaN8mVc7Em6yU0w==</DP>
<DQ>Oe/y8n/OfRPiZ22vXS4PRsJkqIRJwWzlU+O8LRebFXMs0VqWNCi04YzubqbtgPZ
rLKhMQdx5IRbUEwlxHlpAsQ==</DQ>
<InverseQ>ZgOW+H2TwhLqLvUBbcEx5zmtmE2VJcFODdd15Md/ZritQdN0+fQXlt3eU
EdST0UKazNuhwzh8QZ5krZF0+hPoQ==</InverseQ>
<D>Gr+p4rdAI8Nym1FjRsY59v5JI1/XUCbUNDWOS8SebWzpwvaMCy7CojPTXdh6oqpm
+O5RVp16zByLo5rtaO7qMni3qMaoQ7vi64BCG9iTlFKZlb2DWcEQOQ06nDVDELoF
i6o+IAxcnYGkSy9DRQNJqhGn6L6/moBNBmESEos3zQE=</D>
</RSAKeyValue>
Run this another time to convince yourself that a different key is created every time you create an instance.
The public key is represented by the <Modulus> and
<Exponent> and, as you can see, the data is base64 encoded. The
ToXmlString provides a way of persisting the public key (if the
parameter is set to false) or the pair of keys. Clearly, if you want someone else to be able to encrypt data that only you want to decrypt, you'll persist just
the public key. A corresponding application would import the key using
FromXmlString to initialize the object. If you want to pass keys around in your application you
can do this by calling ExportParameters which will create an
RSAParameters object. (Note that there is a DSAParameters
class too.) It is interesting that RSAParameters is marked with
[Serializable], which means that it can be persisted, in addition
,it is marked with [StructLayout(LayoutKind.Sequential)] which
implies that it can be passed to (or initialized from) unmanaged code.
However, closer inspection shows that not all members are serializable:
public struct RSAParameters
{
public byte[] Exponent;
public byte[] Modulus;
[NonSerialized] public byte[] P;
[NonSerialized] public byte[] Q;
[NonSerialized] public byte[] DP;
[NonSerialized] public byte[] DQ;
[NonSerialized] public byte[] InverseQ;
[NonSerialized] public byte[] D;
}
Thus, only the fields that are used with the public key are serializable.
When you generate a key the CryptoAPI will use a key container by
default this will be a temporary container with a name CLR<guid>
where <guid> is a GUID generated for this temporary
action. You may decide to create the key pair and persist it, and to do this
you call the RSACryptoServiceProvider constructor that takes a CspParameters
object. The information in this object is passed to the Crypto Service Provider when the RSA class creates the crypto object:
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(csp);
The constructor parameters are: the provider type code, the provider name
and the key container name. A value of 1 for the provider type code means that
you will get the full RSA implementation. A null for the provider name
indicates that the default provider will be used. The final parameter
is interesting: this gives the name of the key container to use. If you do not
provide a key container name then a temporary container will be used, and
hence the algorithm will generate a new key every time the algorithm is used. However,
if you provide a particular container name the algorithm will attempt
to load a key from that container. If the container does not have a key then
the algorithm will generate a new key and store it in the container. This
container will be persistent, and so the key
can be reused in the future.
Note that the key containers that you create in this way are not associated with
the key containers that you create with the strong name tool.
Thus, if you add a key to a container with sn -i and try to
access the named key container through CspParameters you will
find that the
RSACryptoServiceProvider will not find the key container and create a
new one. |
|
.NET Version 3.0 Version 3.0/2.0 of the framework provides extra constructors for CspParameters.
Clearly, if a container contains a private key you must take steps to ensure
that only the owner of the key can access it. This is usually done with a
password, and one of the new overloads allows you to provide a password in a
SecureString
parameter. Note that a SecureString instance cannot be initialized
in code directly from a string; it can be initialized from an
unmanaged string, and you can build a SecureString by appending
characters.
furthermore the RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
Console.WriteLine("Container name: {0}\nKey type: {1}", rsa.CspKeyContainerInfo.KeyContainerName, rsa.CspKeyContainerInfo.KeyNumber); Console.WriteLine("Accessible? {0}\nExportable? {1}", rsa.CspKeyContainerInfo.Accessible, rsa.CspKeyContainerInfo.Exportable); The results of one run are shown here: Container name:
CLR{3FCE62CE-F275-46E9-B95F-2EA551BAFEB3}
Key type: Exchange Accessible? True Exportable? True Note that the key created is for key exchange, not for signatures. Why is
this important? Well the |
Encrypting data is straight forward, the only issue is to determine what
type of padding you want to use and making sure that the same type of padding
is used for decryption. Add a using statement for
System.Text and then change the code to look like
this:
string clearText = "the quick brown fox...";
byte[] enc = rsa.Encrypt(Encoding.ASCII.GetBytes(clearText), true);
Console.WriteLine(BitConverter.ToString(enc));
byte[] dec = rsa.Decrypt(enc, true);
Console.WriteLine(Encoding.ASCII.GetString(dec));
Compile and run this code. Note that the cyphertext is rather large - the cleartext is 22 characters (and 22 bytes when converted to ASCII), yet the cyphertext is 128 bytes (1024 bits). This is because the RSA algorithm needs a full block which is the size of the key, and this is the reason why padding is necessary. It is also important to understand that you will need the private key to decrypt the data. Make the following changes:
Console.WriteLine(BitConverter.ToString(enc));
RSAParameters publicKey = rsa.ExportParameters(false);
RSACryptoServiceProvider rsaPub = new RSACryptoServiceProvider();
rsaPub.ImportParameters(publicKey);
try
{
byte[] dec = rsaPub.Decrypt(enc, true);
Console.WriteLine(Encoding.ASCII.GetString(dec));
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
This code encrypts data with a key pair (which actually encrypts with the
public key) and then extracts only the public key. The public key is then used
to initialize another RSA object which is then used to decrypt the cyphertext.
Compile and run this code. You'll find that a CryptographicException will be thrown with the
text Error occurred while decoding OAEP padding. (If you use PKCS#1
v1.5 padding then the error will be given as Bad Key). The reason
for this is because the key is an exchange key and such keys are only
used to encrypt with a public key and are only used to decrypt with a private
key.
Typically you will not encrypt data like this instead you'll use it to create a signature. This is covered next.
12.2 Cryptographic Signatures
A signature is an important thing. First, the signature has to be authentic, that is, it is unique to you and identifies you; next, it must not be forgeable, so only you can provide it; finally, it is cannot be repudiated, so once you have provided your signature you cannot claim that it is not yours. These things are true for your handwriting signature (more or less) and are vitally important for digital signatures. A digital signature is attached to, and created from, a document, this means that in addition to the previous statements when a digital signature is provided it means that the document cannot be changed. If the document is changed it will not match the signature. Also, since a digital signature is created from a particular document it means that it cannot be used with another document, this is important because it means that an attacker cannot extract a signature from one document and attach it to another.
To create a digital signature you encrypt the document (or more likely, a hash of the document) with your private key. Then you provide the signature and the public key with the document. Someone trying to authenticate the document can decrypt the signature with the public key, and then compare the result with the original data (or hash of the data). If the comparison fails it means one of two things: either the document has been tampered, or the public key does not correspond to the private key that was used to create the signature. In both cases the authenticator should not trust the signed document, because the signature is suspect.
There are two methods on RSACryptoServiceProvider and DSACryptoServiceProvider
for signing data, SignData and SignHash. At this
point it is important to say that when DSA is used to sign data it will always
use SHA1, whereas RSA can use any hash algorithm. As the name suggests,
SignHash takes an already created hash value that will be signed, whereas SignData
will create a hash from the data and sign it. Along with the hash value, the SignHash
method also take a string identifying the hash
algorithm that will be used (even though this is redundant for DSA). This
string is the ASN.1 Object Identifier (OID) for the
algorithm. These OIDs are not immediately obvious, but to help you the
framework has a method, CryptoConfig.MapNameToOID, that will return
the OID in string form for the name of the hash algorithm that you supply.
This method will take names in various forms, for example for SHA1 you can use
SHA, SHA1, System.Security.Cryptography.SHA1
or System.Security.Cryptography.HashAlgorithm.
To test this out, create a file (sig.cs)
that has the following in the Main method:
string clearText = "the quick brown fox...";
byte[] data = Encoding.ASCII.GetBytes(clearText);
SHA1 sha1 = SHA1.Create();
byte[] hash = sha1.ComputeHash(data);
byte[] sign = rsa.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
Console.WriteLine(BitConverter.ToString(sign));
Compile and run this code to view the signature that is created.
|
.NET Version 3.0 Version 3.0/2.0 of the framework provides new classes to handle OIDs. The Oid
class has two properties, Value and FriendlyName, so
that when one is assigned by you, the other is assigned by the framework. For
example:Oid oid = new Oid();
oid.FriendlyName = "SHA1"; Console.WriteLine(oid.Value); Console.WriteLine(CryptoConfig.MapNameToOID("SHA1")); This will print the string |
If this code was used to sign a document, for example to transmit the data
over a socket to another application, then you will extract the public key
using ToXmlString (or ExportParameters) and transmit
this along with the signature and the document.
Of course, signing a hash is only useful if you can also verify that the
signature is correct. To do this you use VerifyHash, add the
following code:
Console.WriteLine(BitConverter.ToString(sign));
bool bVerify = rsa.VerifyHash(hash, CryptoConfig.MapNameToOID("SHA1"), sign);
Console.WriteLine("signature is verified: {0}", bVerify);
In this example the application reuses the signing object, in a real
application you would create a new RSACryptoServiceProvider
object and initialize it with the public key using FromXmlString
or ImportParameters.
Compile and run this code. You should find that the signature is verified. Now change the hash (in this case it is assumed that the first byte in the hash is not zero) and run the code again:
bool bVerify = rsa.VerifyHash(hash, CryptoConfig.MapNameToOID("SHA1"), sign);
This time you'll find that the signature is not verified. Change line you just added so that you alter the signature (again, assuming that the first byte is not zero):
bool bVerify = rsa.VerifyHash(hash, CryptoConfig.MapNameToOID("SHA1"), sign);
Again, you'll find that the
signature is not verified. So, as you can see, VerifyHash detects
that either the data (ie the hash) or the signature is correct. (When you have
finished this test remove the
line that changes the signature.)
The
SignData method will perform the hash and signature in one action. DSA
always uses SHA1, so the method does not need any hash identifier, however RSA
can use any hash algorithm and so
SignData takes an object to identify the hash algorithm. This object is
either a string (the name that you pass to MapNameToOID), a type
object of the hash algorithm or an instance. Again there is a corresponding
VerifyData to verify the data signed with the signature. Change
the code to:
byte[] sign = rsa.SignData(data, "SHA1");
Console.WriteLine(BitConverter.ToString(sign));
bool bVerify = rsa.VerifyData(data, "SHA1", sign);
Console.WriteLine("signature is verified: {0}", bVerify);
This is more compact code, which makes the code easier to read. Compile and
run this to show that the signature is created. Now change the signature (sign) or the data (data) before calling VerifyData
to confirm that the verification fails if the hash or signature are altered.
The previous mechanism creates the signature directly through the public
key algorithm. The framework provides another mechanism to do the same thing.
The RSAPKCS1SignatureFormatter class is initialized with a public
key algorithm object (the parameter is a AsymmetricAlgorithm, but
you should pass an object of an RSA derived class) , then you can
set the hash algorithm, and finally you can call the object to create the
signature. There is a corresponding RSAPKCS1SignatureDeformatter
class to verify the signature.
Change the code to the following:
RSAPKCS1SignatureFormatter formatter = new RSAPKCS1SignatureFormatter(rsa);
formatter.SetHashAlgorithm("SHA1");
SHA1 sha = SHA1.Create();
byte[] hash = sha.ComputeHash(data);
byte[] sign = formatter.CreateSignature(hash);
Console.WriteLine(BitConverter.ToString(sign));
RSAPKCS1SignatureDeformatter deformatter = new RSAPKCS1SignatureDeformatter(rsa);
deformatter.SetHashAlgorithm("SHA1");
bool bVerify = deformatter.VerifySignature(hash, sign);
Compile and run this, and perform the tests shown earlier to confirm that if the hash or signature changes then the signature cannot be verified.
The DSA classes are only used for signature generation and will only use
SHA1, the signature methods are CreateSignature and
VerifySignature (these actually call SignHash and
VerifyHash). Remove the lines that create the formatter and deformatter
and make these changes:
string clearText = "the quick brown fox...";
byte[] data = Encoding.ASCII.GetBytes(clearText);
SHA1 sha = SHA1.Create();
byte[] hash = sha.ComputeHash(data);
byte[] sign = dsa.CreateSignature(hash);
Console.WriteLine(BitConverter.ToString(sign));
bool bVerify = dsa.VerifySignature(hash, sign);
Console.WriteLine("signature is verified: {0}", bVerify);
Compile and run this code and perform the tests to confirm that the verification process works.
There is a corresponding DSASignatureFormatter and
DSASignatureDeformatter class that provide an alternative way to sign
and verify data.
12.3 Key Exchange
So far on these pages you should have learned that:
- Large amounts of data should be encrypted with a symmetric algorithm because symmetric algorithms are faster than asymmetric algorithms
- Symmetric algorithms use keys that should be kept secret between the people who encrypt and decrypt the data
- Asymmetric algorithms use two keys, one that encrypts the data, and another that decrypts the data
- Typically, one asymmetric key is purposely made public
The problem with symmetric keys is key exchange: how do you pass the secret key from one party to another in such a way that a third party cannot intercept it? This is where public key cryptography excels. If Alice wants to encrypt some data so that Bob can decrypt it she needs to make sure that Bob knows her secret key. To do this Alice obtains Bob's public key and this should be easy to do because the key is meant to be public. Alice then encrypts her secret (symmetric) key with Bob's public key and sends the cyphertext to Bob. Only Bob has his private key, so only Bob can decrypt the cyphertext. Once he has decrypted this data he will have Alice's secret key and so Alice can send Bob encrypted data which he can decrypt.
To try this out I will generate two processes, one is Bob which
will listen on a socket and responds to data sent over connected sockets;
the other is Alice which will connect to Bob and
exchange encrypted data with him.
First we need to set up the sockets
infrastructure. Here's Bob.cs:
using System.Security.Cryptography;
using System.Net.Sockets;
using System.Net;
using System.Text;
class Bob
{
static void Main()
{
TcpListener listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start();
while(true)
{
Console.WriteLine("Waiting for connection...");
TcpClient client = listener.AcceptTcpClient();
NetworkStream stm = client.GetStream();
byte[] buf = new byte[client.ReceiveBufferSize];
int read = stm.Read(buf, 0, buf.Length);
string command = Encoding.ASCII.GetString(buf, 0, read);
if (command.ToLower() != "key")
{
client.Close();
break;
}
Console.WriteLine("command: {0}", command);
byte[] ack = Encoding.ASCII.GetBytes("ACK");
stm.Write(ack, 0, ack.Length);
read = stm.Read(buf, 0, buf.Length);
Console.WriteLine(BitConverter.ToString(buf, 0, read));
client.Close();
}
Console.WriteLine("Stopping...");
listener.Stop();
}
}
I'll explain this bit by bit.
listener.Start();
while(true)
{
TcpClient client = listener.AcceptTcpClient();
NetworkStream stm = client.GetStream();
// other code
client.Close();
}
Console.WriteLine("Stopping...");
listener.Stop();
This code listens on socket 5000, then the code blocks waiting for a client
to connect. Once the client connects the listener obtains stream access to the
socket so that it can read and write data to the connected socket. When the
listener has finished handling the data from the client it calls Close which
closes the stream and the client socket and the loop continues waiting for
another client connection.
Next the listener checks data sent by the client:
int read = stm.Read(buf, 0, buf.Length);
string command = Encoding.ASCII.GetString(buf, 0, read);
if (command.ToLower() != "key")
{
client.Close();
break;
}
Console.WriteLine("command: {0}", command);
This accepts data from the client, converts it to a string and checks to
see if it is the string key. If it is not this string then it is
interpreted as a command to close down so the client connection is closed and
the loop is broken so that the listener can stop listening and the process can
stop. Finally, the listener does this:
stm.Write(ack, 0, ack.Length);
read = stm.Read(buf, 0, buf.Length);
Console.WriteLine(BitConverter.ToString(buf, 0, read));
This sends an acknowledgement to the client that the client (Alice) should send the key.
Alice.cs looks like this:
using System.Security.Cryptography;
using System.Net;
using System.Net.Sockets;
using System.Text;
class Alice
{
static void Main(string[] args)
{
if (args.Length == 0) return;
TcpClient client = new TcpClient();
client.Connect(IPAddress.Loopback, 5000);
NetworkStream stm = client.GetStream();
if (args[0].ToLower() == "quit")
{
byte[] quit = Encoding.ASCII.GetBytes(args[0]);
stm.Write(quit, 0, quit.Length);
client.Close();
return;
}
byte[] buf = Encoding.ASCII.GetBytes("KEY");
stm.Write(buf, 0, buf.Length);
byte[] readBuf = new byte[client.ReceiveBufferSize];
int read = stm.Read(readBuf, 0, readBuf.Length);
string reply = Encoding.ASCII.GetString(readBuf, 0, read);
if (reply.ToLower() == "ack")
{
RandomNumberGenerator rand = RandomNumberGenerator.Create();
byte[] key = new byte[128];
rand.GetBytes(key);
Console.WriteLine(BitConverter.ToString(key));
stm.Write(key, 0, key.Length);
}
client.Close();
}
}
Again, I'll go through this bit by bit.
client.Connect(IPAddress.Loopback, 5000);
NetworkStream stm = client.GetStream();
This code simply connects to Bob. Next, the code checks the command
line:
{
byte[] quit = Encoding.ASCII.GetBytes(args[0]);
stm.Write(quit, 0, quit.Length);
client.Close();
return;
}
If the user provides a command line argument of quit then this is sent to Bob
and Alice finishes. The idea is that to tell Bob to
stop listening you should tell Alice to send him the message
QUIT.
stm.Write(buf, 0, buf.Length);
Here, Alice warns Bob that she is about to send
him the key.
int read = stm.Read(readBuf, 0, readBuf.Length);
string reply = Encoding.ASCII.GetString(readBuf, 0, read);
if (reply.ToLower() == "ack")
{
// code
}
client.Close();
Bob should send the message ack to Alice
to indicate that he wants to hear the key. Alice can then send the key,
and in this case the key is merely a random number:
byte[] key = new byte[128];
rand.GetBytes(key);
Console.WriteLine(BitConverter.ToString(key));
stm.Write(key, 0, key.Length);
Now is a good time to compile both Bob and Alice.
Open two command windows and start Bob in one and run Alice with
a command line parameter
in the other, the parameter you pass is unimportant. Verify
that the number that Alice prints is also printed by Bob.
Run Alice a few times and then finally run Alice
with a command line parameter, so that Bob finishes. You'll see
something like the following:

Now we want to perform the key exchange. Alice needs to know
Bob's public key. To do this I will use a key container, but you
could simply generate a key as XML and store it in an XML file. Bob
will get all of this XML, but Alice will only get the public key
parts. Of course, this supposes that you trust Alice's public key
that you've got. Trust in public keys is an issue that is covered by
certificates, which is a subject covered by a later page.
Using a key container is simple. Add the following to
Bob:
{
CspParameters csp = new CspParameters(1, null, "Bob");
RSACryptoServiceProvider rsaBobPrivKey = new RSACryptoServiceProvider(csp);
Alice is a bit more complicated because I want to extract the public key from the key pair, here's the code:
{
CspParameters csp = new CspParameters(1, null, "Bob");
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(csp);
RSACryptoServiceProvider rsaBobPubKey = new RSACryptoServiceProvider();
rsaBobPubKey.ImportParameters(rsa.ExportParameters(false));
This obtains Bob's key from the container and then exports Bob's public key
and then imports it into rsaBobPubKey. As we saw in the last
section, the framework contains two
classes that can be used to encrypt keys in key exchange: RSAOAEPKeyExchangeFormatter
and RSAPKCS1KeyExchangeFormetter. In this example we'll use RSAOAEPKeyExchangeFormatter:
RSAPKCS1KeyExchangeFormatter exch = new RSAPKCS1KeyExchangeFormatter();
exch.SetKey(rsaBobPubKey);
Rijndael session = Rijndael.Create();
byte[] key = exch.CreateKeyExchange(session.Key);
stm.Write(key, 0, key.Length);
}
The key exchange object is created and then initialized with Bob's
public key. Then a random key is created by creating an instance of the
Rijndael symmetric key object. Finally, the key is encrypted using
CreateKeyExchange. You should now compile Bob and
Alice and confirm that Alice passes some binary data
to Bob.
The next thing is for Alice to send some encrypted data to
Bob using this
session key and then Bob needs to extract the session key from exchange key blob
and use this to decrypt the encrypted data. The symmetric algorithm uses CBC
mode so we need to obtain the initialization vector from Alice and pass that to
Bob. Remember that the IV is important, but it does not have to
be secret, so if it is made public,
it does not compromise the security. Here's the
code for Alice:
stm.Write(key, 0, key.Length);
read = stm.Read(readBuf, 0, readBuf.Length);
reply = Encoding.ASCII.GetString(readBuf, 0, read);
if (reply.ToLower() == "ack")
{
stm.Write(session.IV, 0, session.IV.Length);
}
Again, before the code is send we first wait for Bob to send
an acknowledgement. After the IV is sent we wait for another acknowledgement
and then send the encrypted data:
if (reply.ToLower() == "ack")
{
stm.Write(session.IV, 0, session.IV.Length);
read = stm.Read(readBuf, 0, readBuf.Length);
reply = Encoding.ASCII.GetString(readBuf, 0, read);
if (reply.ToLower() == "ack")
{
CryptoStream cryptostm = new CryptoStream(stm, session.CreateEncryptor(), CryptoStreamMode.Write);
byte[] data = Encoding.ASCII.GetBytes(args[0]);
cryptostm.Write(data, 0, data.Length);
cryptostm.FlushFinalBlock();
cryptostm.Clear();
}
}
This code creates a CryptoStream based on the socket stream so
that as data is written to the stream it is encrypted. Note that the code makes
sure that the final block is flushed.
Bob creates a deformatter object and initializes it with his
private key, then he passes the encrypted session key to
DecryptKeyExchange to extract the (symmetric) session key. This is used
to initialize the symmetric algorithm object. Here's the code
for Bob:
byte[] key = new byte[read];
Array.Copy(buf, 0, key, 0, read);
RSAPKCS1KeyExchangeDeformatter exch = new RSAPKCS1KeyExchangeDeformatter();
exch.SetKey(rsaBobPrivKey);
Rijndael session = Rijndael.Create();
session.Key = exch.DecryptKeyExchange(key);
Now Bob needs to read the IV from the client, so Bob
first sends an acknowledgement and then reads the data from the socket and uses the
data to initialize an array:
read = stm.Read(buf, 0, buf.Length);
byte[] iv = new byte[read];
Array.Copy(buf, 0, iv, 0, read);
stm.Write(ack, 0, ack.Length);
session.IV = iv;
Finally, Bob needs to create a CryptoStream object based on
the socket and decrypt the data as it's read:
read = cryptostm.Read(buf, 0, buf.Length);
Console.WriteLine(Encoding.ASCII.GetString(buf, 0, read));
cryptostm.Clear();
client.Close();
}
You should now compile both Bob and Alice. Run Bob in one command window and run Alice a
few times in another window, each time passing a paramter. Check to make sure that the data that you pass on
the command line to Alice is
received and decrypted by Bob.
| I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com. |
Errata
If you see an error on this page, please contact me and I will fix the problem.