Lately I used the Rijndael algorithm to setup a single sign-on between our public facing SharePoint site (.NET) and an internet site (PHP). Rijndael is a symmetric encryption algorithm which means that the same key is used to encrypt and decrypt a message.
This algorithm is readily available in PHP as well as in .NET so in theory implementing the secure communication channel would be as simple as using the out-of-the-box functions. As it turned out...the real world proved to be a little more complex.
Both languages use different initialization settings so a simple encrypt-decrypt between both languages turns out to be not working.
First of all the Rijndael algorithm inherits from the SymmetricAlgorithm.
The classes that derive from the SymmetricAlgorithm class use a chaining mode called cipher block chaining (CBC), which requires a key and an initialization vector (IV) to perform cryptographic transformations on data. To decrypt data that was encrypted using one of the SymmetricAlgorithm classes, you must set the Key property and IV property to the same values that were used for encryption.
Secondly most plain text messages do not consist of a number of bytes that completely fill blocks. Often, there are not enough bytes to fill the last block. When this happens, a padding string is added to the text. For example, if the block length is 64 bits and the last block contains only 40 bits, 24 bits of padding are added. For the communication to work, set the PaddingMode to Zeros.
And last but not least, I used MD5 to hash the key used for the encryption. It turns out that the implementation of this algorithm is different for both languages. PHP returns a character string of 32 characters. C# returns a 16 byte binary array. To produce a php-like md5 hash I used the following code:
public static string MD5(string password) {
byte[] textBytes = System.Text.Encoding.Default.GetBytes(password);
try {
System.Security.Cryptography.MD5CryptoServiceProvider cryptHandler;
cryptHandler = new System.Security.Cryptography.MD5CryptoServiceProvider();
byte[] hash = cryptHandler.ComputeHash (textBytes);
string ret = "";
foreach (byte a in hash) {
ret += a.ToString ("x2");
}
return ret ;
}
catch {
throw;
}
To test all this I created a little project.The project contains a .NET webservice with to methods: TestSymmetricDecryption and TestSymmetricEncryption.
When the TestSymmetricEncryption is called, it encrypts a test message and sends the result. The TestSymmetricDecryption decrypts a message send to the service and returns the decrypted result.
Secondly I've got a php page that calls both webservices and shows the result.
c#:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.IO;
using System.Text;
using System.Security.Cryptography;
namespace VNTG.Rijndael.POC
{
///
/// Summary description for Rijndael
///
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
// [System.Web.Script.Services.ScriptService]
public class RijndaelPOC : System.Web.Services.WebService
{
string key = "secret"; // can be any string
string iv = "ujkrtadxcfrzpj1Bs5fpM18doZQDGYS4";
System.Security.Cryptography.Rijndael r = null;
[WebMethod]
public string TestSymmetricEncryption()
{
InitializeRijndael();
return Encrypt("This is a test");
}
[WebMethod]
public string TestSymmetricDecryption(string message)
{
InitializeRijndael();
return Decrypt(message);
}
private void InitializeRijndael()
{
byte[] keyBytes = Encoding.ASCII.GetBytes(EncodeTo64(key));
byte[] hash = MD5.Create().ComputeHash(keyBytes);
string ret = "";
foreach (byte a in hash)
{
ret += a.ToString("x2");
}
string iv64 = EncodeTo64(iv);
byte[] ivBytes = Convert.FromBase64String(iv64);
r = System.Security.Cryptography.Rijndael.Create();
r.Padding = PaddingMode.Zeros;
r.BlockSize = 256;
r.Key = Encoding.ASCII.GetBytes(ret);
r.IV = ivBytes;
}
public string Decrypt(string str)
{
byte[] encryptedBytes = Convert.FromBase64String(str);
byte[] decryptedBytes = transformBytes(
r.CreateDecryptor(), encryptedBytes);
string plaintext = Encoding.ASCII.GetString(decryptedBytes);
int idx = plaintext.IndexOf("\0");
if (idx > -1)
plaintext = plaintext.Substring(0, idx);
return plaintext;
}
public string Encrypt(string plaintext)
{
byte[] plainBytes = Encoding.ASCII.GetBytes(plaintext);
byte[] encryptedBytes = transformBytes(
r.CreateEncryptor(), plainBytes);
return Convert.ToBase64String(encryptedBytes);
}
private byte[] transformBytes(ICryptoTransform transform,
byte[] plainBytes)
{
MemoryStream memStream = new MemoryStream();
CryptoStream cryptStream =
new CryptoStream(memStream, transform,
CryptoStreamMode.Write);
cryptStream.Write(plainBytes, 0, plainBytes.Length);
cryptStream.Close();
byte[] encryptedBytes = memStream.ToArray();
memStream.Close();
return encryptedBytes;
}
private string EncodeTo64(string toEncode)
{
byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(toEncode);
string returnValue = System.Convert.ToBase64String(toEncodeAsBytes);
return returnValue;
}
}
}
PHP:
<?php
// include soap helper library
require_once('nusoap.php');
//initialize Rijndael settings
$key = 'secret';
$iv='ujkrtadxcfrzpj1Bs5fpM18doZQDGYS4'; //must be 32 chars long
function init_rijndael ($key,$iv) {
$td = mcrypt_module_open('rijndael-256', '', 'cbc', '');
if ($td !== FALSE)
{
$expected_key_size = mcrypt_enc_get_key_size($td);
$key = substr(md5(base64_encode($key)), 0, $expected_key_size);
mcrypt_generic_init($td, $key, $iv);
}
return $td;
}
//initialize proxy port
$proxyhost = isset($_POST['proxyhost']) ? $_POST['proxyhost'] : '';
$proxyport = isset($_POST['proxyport']) ? $_POST['proxyport'] : '';
$proxyusername = isset($_POST['proxyusername']) ? $_POST['proxyusername'] : '';
$proxypassword = isset($_POST['proxypassword']) ? $_POST['proxypassword'] : '';
//initialize webservice client
$client = new nusoap_client('http://localhost:1956/RijndaelPOC.asmx?WSDL', 'wsdl',
$proxyhost, $proxyport, $proxyusername, $proxypassword);
$err = $client->getError();
if ($err) {
echo '<h2>Constructor error</h2><pre>' . $err . '</pre>';
}
//PART 1: Encrypting on php side and retrieving result on .NET side
$td = init_rijndael($key,$iv);
$param = array('message' => base64_encode(mcrypt_generic($td, 'Dit is een test')));
// Call to webservice
$decryptedResult = $client->call('TestSymmetricDecryption', array('parameters' => $param));
if ($client->fault) {
echo '<h2>Error has occured.</h2>';
} else {
$err = $client->getError();
if ($err) {
echo '<h2>Error has occured.</h2>';
}
}
//PART 2: Retrieving an encrypted message on .NET side and decrypting it on php side
$td = init_rijndael($key,$iv);
$encryptedResult = $client->call('TestSymmetricEncryption');
if ($client->fault) {
echo '<h2>Error has occured.</h2>';
} else {
$err = $client->getError();
if ($err) {
echo '<h2>Error has occured.</h2>';
}
}
//decrypt encryptedResult
$encryptedResult = mdecrypt_generic($td, base64_decode($encryptedResult['TestSymmetricEncryptionResult']));
?>
<html>
<head>
<title>Rijndael POC</title>
</head>
<body>
<table>
<tr>
<td>Decrypted message on .NET: <?php print "'".$decryptedResult['TestSymmetricDecryptionResult']."'" ?></td>
</tr>
<tr>
<td>Encrypted message on php: <?php print "'".$encryptedResult."'" ?></td>
</tr>
</table>
</body>
</html>
Couldn't have done it without these posts: