I have a working implementation of TripleDESCng (tested against some test vectors), but the following happens:
When I encrypt plain text This is a sample message (24 bytes, thus for this it would be 3 blocks) (hex for it is 5468697320697320612073616D706C65206D657373616765) with an example key, I get E81F113DD7C5D965E082F3D42EC1E2CA39BCDBCCBC0A2BD9. However, when I decrypt this with the same example key, I get 5468697320697320612073616D706C650000000000000000, which, when converted back to ASCII, is:
This is a sample.
Any reason other than my code why this would behave this way? To encrypt and decrypt, I use 24 byte keys (ECB mode).
EDIT:
using (var tripleDES = new TripleDESCryptoServiceProvider())
{
byte[] data = ASCIIEncoding.ASCII.GetBytes("This is a sample message");
Console.WriteLine(BitConverter.ToString(data));
tripleDES.IV = new byte[tripleDES.BlockSize / 8];
var encryptor = tripleDES.CreateEncryptor();
byte[] result = new byte[data.Length];
encryptor.TransformBlock(data, 0, data.Length, result, 0);
var decryptor = tripleDES.CreateDecryptor();
byte[] result2 = new byte[result.Length];
decryptor.TransformBlock(result, 0, result.Length, result2, 0);
Console.WriteLine(BitConverter.ToString(result2));
}
Console.ReadLine();
With almost all modes1, you should make sure that the final part of your data is pushed through TransformFinalBlock rather than TransformBlock2, to make sure it knows that no more data is coming and to ensure final blocks are flushed/written.
It's bad form, in general, to assume the output size is going to match the input size.
the mode is not a problem, IV is set to 0s either way
Yes, that'll mean that the first block was not affected by your choice of Mode. But all subsequent blocks will be, because they will use the chaining mode and the previous block, not the IV. So if you want ECB (you shouldn't3) you need to explicitly set that mode.
1Your code is using CBC, not EBC as you claim in your narrative. CBC is the default mode for .NET encryption classes.
2And when using this second method, pay attention to it's return value, as mjwills commented.
3You've picked an outdated crypto algorithm, paired it with an outdated mode of operation, and your words I've quoted above mean that you don't understand modes. Added together, I would suggest that you're not well placed to be writing code that uses crypto currently. The .NET classes can make it seem easy to write crypto code but you still have to understand how to make good choices in using them. Better to spend more time on researching these things before writing code.
I think that your problem is in the method of the encryptor / decryptor that you are using: the TransformBlock method is conceived to transform a block when you will be encrypting multiple blocks.
That is not the case in your code, where you want to transform a single block, and therefore you should be using the TransformFinalBlock method instead. BTW I took the liberty of making your sample buildable.
using System;
using System.Text;
namespace Tests
{
class Program
{
static void Main(string[] args)
{
System.Security.Cryptography.TripleDESCryptoServiceProvider tripleDES = new System.Security.Cryptography.TripleDESCryptoServiceProvider();
byte[] data = Encoding.UTF8.GetBytes("This is a sample message");
byte[] key = Encoding.UTF8.GetBytes("NOSTROMOHASSOMEGODPOWERS");
tripleDES.Key = key;
tripleDES.IV = new byte[tripleDES.BlockSize / 8];
var encryptor = tripleDES.CreateEncryptor();
byte[] result = new byte[data.Length];
result = encryptor.TransformFinalBlock(data, 0, data.Length);
string res = BitConverter.ToString(result).Replace("-","");
Console.WriteLine(BitConverter.ToString(result).Replace("-",""));
byte[] data2 = result;
tripleDES.Key = key;
tripleDES.IV = new byte[tripleDES.BlockSize / 8];
var decryptor = tripleDES.CreateDecryptor();
byte[] result2 = new byte[data2.Length];
result2 = decryptor.TransformFinalBlock(data2, 0, data2.Length);
Console.WriteLine(Encoding.UTF8.GetString(result2));
}
}
}
Related
Consider the following code (you also may check in sandbox):
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
class EncryptionIVTest
{
private static readonly string Data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private static readonly byte[] Password = Guid.NewGuid().ToByteArray().Take(32).ToArray();
static void Main()
{
var iv = Guid.NewGuid().ToByteArray().Take(16).ToArray(); // random initialization vector
var iv2 = new byte[16]; // just another zero-filled initialization vector
var encrypted = Encrypt(iv);
Console.WriteLine($"Original: {Data}");
Console.WriteLine($"Encrypted: {encrypted}");
Console.WriteLine($"Decrypted: {Decrypt(encrypted, iv)}");
Console.WriteLine($"Decrypted with another IV: {Decrypt(encrypted, iv2)}"); // It should throw exception or output completely mangled string
}
private static string Encrypt(byte[] iv)
{
var cipher = CreateCipher(iv);
var buf = Encoding.UTF8.GetBytes(Data);
using var ms = new MemoryStream();
using (var stream = new CryptoStream(ms, cipher.CreateEncryptor(), CryptoStreamMode.Write))
stream.Write(buf, 0, buf.Length);
return Convert.ToBase64String(ms.ToArray());
}
private static string Decrypt(string encrypted, byte[] iv)
{
var cipher = CreateCipher(iv);
using var ms = new MemoryStream(Convert.FromBase64String(encrypted));
using var result = new MemoryStream();
using (var stream = new CryptoStream(ms, cipher.CreateDecryptor(), CryptoStreamMode.Read))
stream.CopyTo(result);
return Encoding.UTF8.GetString(result.GetBuffer(), 0, (int)result.Length);
}
private static Aes CreateCipher(byte[] iv)
{
var cipher = Aes.Create();
cipher.Key = Password;
cipher.IV = iv;
cipher.Mode = CipherMode.CBC;
return cipher;
}
}
It outputs:
Original: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Encrypted: EROKh8lVgREvTqzBYXjEm7EbTIT883uR9wsD82lRM14KtiOYr+/+ZpAwz/UfprqSP5mIQ7Du/d43Y88hAPjvkA==
Decrypted: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Decrypted with another IV: ???#?n? ??7║??Paaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
The fourth string is not fully mangled, it contains an untouched trailing. It seems like encryptor only mangle first 16 bytes (size of an initialization vector) and leaves other untouched. By default, encryptor uses CBC CipherMode and it should mangle all data if I understand correctly.
Is it possible to mangle all data, not only the first part?
The purpose of the IV is not to "mangle" data further or to serve as a second encryption key - that would just make it redundant of the actual key.
The purpose is to provide additional entropy so that two sets of plaintext data encrypted with the same key but with different IVs will appear completely different when encrypted. This makes it harder for an attacker to infer anything about the data. For example, without the IV, sophisticated attackers could run statistical analyses based on language patterns and potentially figure out what certain encrypted packets actually are based on how frequently they occur.
So what you're seeing should not be surprising or concerning. The IV is doing its job.
By the way, using a Guid as a key is NOT secure. First of all it's only 16-bytes not 32 so you only have a 128-bit key basically. See https://learn.microsoft.com/en-us/dotnet/standard/security/generating-keys-for-encryption-and-decryption#symmetric-keys for the right apis to use to generate keys and IVs
I'm a newbie in cryptography and to learn it I tried to encrypt/decrypt with AES in C#.
Sadly I realized, that it isn't as easy as I thought.
So I was looking for a simpler solution.
Later I found a couple of code snippets including some explanation.
I copied the code and tried to implement it into a small application.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace aes
{
class Program
{
public static string passwd = null;
public static string content = null;
public static string encryptedcontent = null;
public static byte[] IV = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
public static int BlockSize = 128;
static void Encrypt()
{
if (passwd == "") return;
//Content to Byte Array
byte[] bytes = Encoding.Unicode.GetBytes(content);
//Encrypt
//Init AES
SymmetricAlgorithm crypt = Aes.Create();
//Init md5 hash
HashAlgorithm hash = MD5.Create();
//AES blocksize (AES 192 etc.) (min 128)
crypt.BlockSize = BlockSize;
//Generating Key
crypt.Key = hash.ComputeHash(Encoding.Unicode.GetBytes(passwd));
//Initialize Vectors
crypt.IV = IV;
//CryptoStram is used for encryption
//The required Encryptor is based on the algorithm above
//Cryptostream sends data of the encrypted byte array to Memorystream
//The memory stream is then converted into a Base64 string and made readable
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream =
new CryptoStream(memoryStream, crypt.CreateEncryptor(), CryptoStreamMode.Write))
{
cryptoStream.Write(bytes, 0, bytes.Length);
}
encryptedcontent = Convert.ToBase64String(memoryStream.ToArray());
}
}
static void Main(string[] args)
{
//Set Password
Console.WriteLine("Passwort angeben");
Console.Write("> ");
passwd = Console.ReadLine();
//Set content to encrypt (String)
Console.WriteLine("Zu verschlüsselner Text angeben");
Console.Write("> ");
content = Console.ReadLine();
Encrypt();
Console.WriteLine(encryptedcontent);
Console.ReadLine();
}
}
}
Subsequently I wanted to try the programm with some testdata.
I actually got a seemingly encrypted string.
PW: supersecretpassword Content: I like to keep my secrets Result: SEEc1sLMIyfVFsoHPFRIcl437+yjUC5uFMgco3iO+oWSgJWQOwKhoDhUbFJREeqiIvaY2DBR+Ih4OJeGAc6JZQ==
I tried to use some online tools to decrypt and check my result.
Sadly most of the Webtools were not able to decrypt my result.
And if I encrypt the sentence I like to keep my secrets with that online tools I get results like:
7IWuebm0T8HdrGdtkBjt5zgjbdEqYfidNZVvfgtOjH4=
My result SEEc1sLMIyfVFsoHPFRIcl437+yjUC5uFMgco3iO+oWSgJWQOwKhoDhUbFJREeqiIvaY2DBR+Ih4OJeGAc6JZQ==
As you can see, the two results are different.
Unfortunately I have no idea why this could be the case.
Thanks for you help
Jonas
P.S Somehow I deleted some of rows written in this question. I hope the new words can clarify what my problem is.
You don't say what online tools did, or did not, succeed in replicating your results, so this is a general answer, instead of specific.
//AES blocksize (AES 192 etc.) (min 128)
crypt.BlockSize = BlockSize;
The BlockSize of AES is 128. Always (contrast with the original algorithm, Rijndael, which allows the BlockSize to change).
AES-128/AES-192/AES-256 are about the KeySize, not the BlockSize.
crypt.Key = hash.ComputeHash(Encoding.Unicode.GetBytes(passwd));
You're using MD5(UTF16(password)) as your Key Deriviation Function (KDF). Maybe you can find an online sample that is using this, but they're more likely to be using MD5(UTF8(password)) (which would come from Encoding.UTF8, vs Encoding.Unicode). A better answer would be to use a proper password-based Key Derivation Function, like PBKDF2 (which is called Rfc2898DeriveBytes in .NET for... reasons).
[When I encrypt I like to keep my secrets I get an answer that is twice as long as online tools.]
You're encrypting the UTF-16 representation of that string. The string is comprised of 25 Unicode codepoint values, all from the US-ASCII range. Therefore the UTF-16 representation is just the codepoint length * 2 (50 bytes).
50 bytes breaks down into 3 16-byte (128-bit) blocks, plus 2 bytes left over. Add padding, that becomes 4 blocks of AES-CBC-PKCS#7 output (64 bytes). 64 bytes converts to Base64 as 21 full values (of 3 bytes -> 4 chars) with 1 byte remaining, so the Base64 value ends in 2 = padding characters with a total length of 88 characters. This matches your description, hooray :).
If, on the other hand, you used the UTF-8 encoding, you'd have 25 bytes into encryption, which becomes 2 blocks of output (32 bytes) which turns into 10 full base64 conversions with 2 bytes remaining, so one = at a total of 44 characters... which looks a lot like what the online tools are using.
You also should produce a new IV for every time you encrypt with the same key. The IV isn't a key, but changing the IV causes the same secret input to get encrypted differently, so someone who can see your encrypted data can't tell that you sent the same message that you just sent. (At least, that's the purpose in CBC block mode, in other block modes it has sometimes more important purposes). The IV can be transmitted with the message... in fact it should be, unless you have some other way of both sides agreeing (without hard-coding it).
And, of course, you should dispose all of your disposable objects. Changing your encoding to UTF-8, but not changing your KDF, would better be
private static string Encrypt(string content, string password)
{
byte[] bytes = Encoding.UTF8.GetBytes(content);
using (SymmetricAlgorithm crypt = Aes.Create())
using (HashAlgorithm hash = MD5.Create())
using (MemoryStream memoryStream = new MemoryStream())
{
crypt.Key = hash.ComputeHash(Encoding.UTF8.GetBytes(password));
// This is really only needed before you call CreateEncryptor the second time,
// since it starts out random. But it's here just to show it exists.
crypt.GenerateIV();
using (CryptoStream cryptoStream = new CryptoStream(
memoryStream, crypt.CreateEncryptor(), CryptoStreamMode.Write))
{
cryptoStream.Write(bytes, 0, bytes.Length);
}
string base64IV = Convert.ToBase64String(crypt.IV);
string base64Ciphertext = Convert.ToBase64String(memoryStream.ToArray());
return base64IV + "!" + base64Ciphertext;
}
}
Some issues that I see is a self defined IV and odd blocksize, edit: and you probably have the wrong value for the password in mind when comparing to online tools where you do have to fill in the password as calculated by the ComputeHash function.
Check out this simple MSDN Example
I have a working implementation of TripleDESCng (tested against some test vectors), but the following happens:
When I encrypt plain text This is a sample message (24 bytes, thus for this it would be 3 blocks) (hex for it is 5468697320697320612073616D706C65206D657373616765) with an example key, I get E81F113DD7C5D965E082F3D42EC1E2CA39BCDBCCBC0A2BD9. However, when I decrypt this with the same example key, I get 5468697320697320612073616D706C650000000000000000, which, when converted back to ASCII, is:
This is a sample.
Any reason other than my code why this would behave this way? To encrypt and decrypt, I use 24 byte keys (ECB mode).
EDIT:
using (var tripleDES = new TripleDESCryptoServiceProvider())
{
byte[] data = ASCIIEncoding.ASCII.GetBytes("This is a sample message");
Console.WriteLine(BitConverter.ToString(data));
tripleDES.IV = new byte[tripleDES.BlockSize / 8];
var encryptor = tripleDES.CreateEncryptor();
byte[] result = new byte[data.Length];
encryptor.TransformBlock(data, 0, data.Length, result, 0);
var decryptor = tripleDES.CreateDecryptor();
byte[] result2 = new byte[result.Length];
decryptor.TransformBlock(result, 0, result.Length, result2, 0);
Console.WriteLine(BitConverter.ToString(result2));
}
Console.ReadLine();
With almost all modes1, you should make sure that the final part of your data is pushed through TransformFinalBlock rather than TransformBlock2, to make sure it knows that no more data is coming and to ensure final blocks are flushed/written.
It's bad form, in general, to assume the output size is going to match the input size.
the mode is not a problem, IV is set to 0s either way
Yes, that'll mean that the first block was not affected by your choice of Mode. But all subsequent blocks will be, because they will use the chaining mode and the previous block, not the IV. So if you want ECB (you shouldn't3) you need to explicitly set that mode.
1Your code is using CBC, not EBC as you claim in your narrative. CBC is the default mode for .NET encryption classes.
2And when using this second method, pay attention to it's return value, as mjwills commented.
3You've picked an outdated crypto algorithm, paired it with an outdated mode of operation, and your words I've quoted above mean that you don't understand modes. Added together, I would suggest that you're not well placed to be writing code that uses crypto currently. The .NET classes can make it seem easy to write crypto code but you still have to understand how to make good choices in using them. Better to spend more time on researching these things before writing code.
I think that your problem is in the method of the encryptor / decryptor that you are using: the TransformBlock method is conceived to transform a block when you will be encrypting multiple blocks.
That is not the case in your code, where you want to transform a single block, and therefore you should be using the TransformFinalBlock method instead. BTW I took the liberty of making your sample buildable.
using System;
using System.Text;
namespace Tests
{
class Program
{
static void Main(string[] args)
{
System.Security.Cryptography.TripleDESCryptoServiceProvider tripleDES = new System.Security.Cryptography.TripleDESCryptoServiceProvider();
byte[] data = Encoding.UTF8.GetBytes("This is a sample message");
byte[] key = Encoding.UTF8.GetBytes("NOSTROMOHASSOMEGODPOWERS");
tripleDES.Key = key;
tripleDES.IV = new byte[tripleDES.BlockSize / 8];
var encryptor = tripleDES.CreateEncryptor();
byte[] result = new byte[data.Length];
result = encryptor.TransformFinalBlock(data, 0, data.Length);
string res = BitConverter.ToString(result).Replace("-","");
Console.WriteLine(BitConverter.ToString(result).Replace("-",""));
byte[] data2 = result;
tripleDES.Key = key;
tripleDES.IV = new byte[tripleDES.BlockSize / 8];
var decryptor = tripleDES.CreateDecryptor();
byte[] result2 = new byte[data2.Length];
result2 = decryptor.TransformFinalBlock(data2, 0, data2.Length);
Console.WriteLine(Encoding.UTF8.GetString(result2));
}
}
}
I try to encrypt a string (json) with Rijndael in C# and come up with a string, which I can offer to a PHP web service. This web service in turn decodes the string using the IV and masterkey (known to them). I have to write the C# code that can talk to the PHP service, I do not control/own the PHP service.
The PHP code for encrypting is as follows:
function encrypt($plaintext) {
$masterkey = 'masterKeyOfLength29Characters';
$td = mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, '');
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
mcrypt_generic_init($td, $masterkey, $iv);
$crypttext = mcrypt_generic($td, $plaintext);
mcrypt_generic_deinit($td);
return base64_encode($iv.$crypttext);
}
$param = array("key" => "value");
$encryptedString = rawurlencode(encrypt(json_encode($param)))
The code above I'll have to convert to C#, so I can encrypt my JSON and offer it to the PHP web service.
There are two problems. The first was with the masterkey length, the second (might be related) is with the rawurlencode of the encrypted data (hard for me to test at this point).
var masterkey = "masterKeyOfLength29Characters";
var data = EncryptData(json, masterkey);
// Some code to URL Encode the data, I haven't gotten as far to test this
// since I can't encrypt with the key used in PHP, so I can't call the service
// to test the encoded string from my C# code.
data = HttpUtility.UrlEncode(data);
data = data.Replace("+", "%20");
public static string EncryptData(string json, string encryptionKey) {
Rijndael rj = Rijndael.Create();
rj.Mode = CipherMode.CBC;
rj.Padding = PaddingMode.PKCS7;
rj.BlockSize = 256;
rj.KeySize = 256;
rj.Key = Encoding.UTF8.GetBytes(encryptionKey); // ERROR here
rj.GenerateIV();
var encryptedJSON = EncryptStringToBytes(json, rj.Key, rj.IV);
var r1 = Convert.ToBase64String(rj.IV);
var r2 = Convert.ToBase64String(encryptedJSON);
return r1 + r2;
}
The EncryptStringToBytes does some checks and uses this code (plucked from the many examples on the internet):
using (Rijndael rijAlg = Rijndael.Create()) {
// Basically I do the same here as above, and I could also generate
// the IV here, but then I'd had to return it too. I know I can clean this
// code up quite a bit, but I'd rather focus on getting it to work first ;)
rijAlg.Mode = CipherMode.CBC;
rijAlg.Padding = PaddingMode.PKCS7;
rijAlg.BlockSize = 256;
rijAlg.KeySize = 256;
rijAlg.Key = Key;
rijAlg.IV = IV;
ICryptoTransform encryptor = rijAlg.CreateEncryptor(rijAlg.Key, rijAlg.IV);
using (MemoryStream msEncrypt = new MemoryStream()) {
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) {
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) {
swEncrypt.Write(plainText);
}
encrypted = msEncrypt.ToArray();
}
}
}
The error I'll get:
Specified key is not a valid size for this algorithm.
So, the problems in short:
1) How come the PHP code accepts the key of length 29 in the Rijndael 256 (CBC mode), and my C# doesn't? I've played around with the Mode, added the Padding later, set the KeySize (was 256 default already), and I just can't see what I'm doing wrong here.
2) When I use a key of length 32, this one is accepted and my code works. I can also decrypt it in C# (but can't test this in PHP). I would like to solve problem 1, and then continue on problem 2, but maybe someone can give me some understanding here. The encrypted string contains 1 '=' in the IV, and 2x '==' (at the end) in the encrypted json. I've read about padding and such, but I was wondering why no '=' signs are visible in the PHP examples I received. Again, maybe after fixing problem 1 this won't be an issue.
Many thanks for reading and I hope I'm not being too stupid here. After a day of trying yesterday I kind of get the feeling I've tried many different approaches and non seem to work.
Just thought I'd add a tiny bit to what #artjom-b has said.
Firstly, it does work :-)
But in addition you need to change your
rj.Padding = PaddingMode.PKCS7
to use
rj.Padding = PaddingMode.Zeros
Also, technically, your two functions aren't returning the same thing. The PHP returns base 64 of two concatenated bits of binary data whereas the C# returns a concatenation of separate b64 strings. The result will be different in the second half of the returned string.
EDIT: The rough and ready decryption routine:
public string DecryptRijndael(byte[] cipherText, string password, byte[] iv)
{
var key = new byte[32];
Encoding.UTF8.GetBytes(password).CopyTo(key, 0);
var cipher = new RijndaelManaged();
cipher.Mode = CipherMode.CBC;
cipher.Padding = PaddingMode.None;
cipher.KeySize = 256;
cipher.BlockSize = 256;
cipher.Key = key;
cipher.IV = iv;
byte[] plain;
using (var decryptor = cipher.CreateDecryptor())
{
using (MemoryStream ms = new MemoryStream())
{
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Write))
{
cs.Write(cipherText, 0, cipherText.Length);
cs.FlushFinalBlock();
plain = ms.ToArray();
}
}
}
return Encoding.UTF8.GetString(plain);
}
NB: All the caveats and warnings from Artjom B still apply.
You're using an old version of PHP which happily accepts keys that have an invalid length. Rijndael supports key sizes of 16, 24 and 32 bytes and nothing inbetween. The mcrypt extension in PHP silently pads the key with 0x00 bytes up to the next valid key size which is 32 bytes. You will have to do the same thing in C#:
byte[] key = new byte[32];
byte[] password = Encoding.UTF8.GetBytes(encryptionKey);
Array.Copy(password, key, password.Length);
rj.Key = key;
Keep in mind that in order to provide some security a key must have high entropy. A password is not a key and therefore doesn't provide much entropy, because of the limited character set and possible use words. Always derive a key from the password with available derivation functions such as Argon2, scrypt, bcrypt or PBKDF2 with a high cost factor/iteration count and a random salt.
You should also add authentication to your ciphertexts. Otherwise, an attacker might change the ciphertext without you knowing it. This either done by using an authenticated mode like GCM/EAX or running HMAC over the ciphertext to produce the authentication tag.
I am trying to write a Python module that will encrypt text that our existing .NET classes can decrypt. As far as I can tell, my code lines, up but it isn't decrypting (I get an 'Invalid padding length' error on the C# side). My pkcs7 code looks good, but research indicates that invalid keys could cause this same problem.
What's different between these two setups?
Python:
derived_key = PBKDF2(crm_key, salt, 256 / 8, iterations)
iv = PBKDF2(crm_key, salt, 128 / 8, iterations)
encoder = pkcs7.PKCS7Encoder()
cipher = AES.new(derived_key, AES.MODE_CBC, iv)
decoded = cipher.decrypt(encoded_secret)
#encode - just stepped so i could debug.
padded_secret = encoder.encode(secret) # 1
encodedtext = cipher.encrypt(padded_secret) # 2
based_secret = base64.b64encode(encodedtext) # 3
I thought that based_secret could get passed up to C# and decoded there. But it fails. The same encrypting c# code is:
var rfc = new Rfc2898DeriveBytes(key, saltBytes);
// create provider & encryptor
using (var cryptoProvider = new AesManaged())
{
// Set cryptoProvider parameters
cryptoProvider.BlockSize = cryptoProvider.LegalBlockSizes[0].MaxSize;
cryptoProvider.KeySize = cryptoProvider.LegalKeySizes[0].MaxSize;
cryptoProvider.Key = rfc.GetBytes(cryptoProvider.KeySize / 8);
cryptoProvider.IV = rfc.GetBytes(cryptoProvider.BlockSize / 8);
using (var encryptor = cryptoProvider.CreateEncryptor())
{
// Create a MemoryStream.
using (var memoryStream = new MemoryStream())
{
// Create a CryptoStream using the MemoryStream and the encryptor.
using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
{
// Convert the passed string to a byte array.
var valueBytes = Encoding.UTF8.GetBytes(plainValue);
// Write the byte array to the crypto stream and flush it.
cryptoStream.Write(valueBytes, 0, valueBytes.Length);
cryptoStream.FlushFinalBlock();
// Get an array of bytes from the
// MemoryStream that holds the
// encrypted data.
var encryptBytes = memoryStream.ToArray();
// Close the streams.
cryptoStream.Close();
memoryStream.Close();
// Return the encrypted buffer.
return Convert.ToBase64String(encryptBytes);
}
}
}
The Python pkcs7 implementation I'm using is:
https://gist.github.com/chrix2/4171336
First off, I verified that Rfc2898 and PBKDF2 are the same thing. Then, as stated above, the problem appears to be a .net ism. I found on msdn
that the implementation of GetBytes inside of Rfc2898DeriveBytes changes on each call, ie. it holds state. (see the remarks about halfway down the page)
Example in Python (pseudo output):
derived_key = PBKDF2(key, salt, 32, 1000)
iv = PBKDF2(key, salt, 16, 1000)
print(base64.b64encode(derived_key))
print(base64.b64encode(iv))
$123456789101112134==
$12345678==
Same(ish) code in .NET (again, pseudo output):
var rfc = new Rfc2898DeriveBytes(key, saltBytes);
using (var cryptoProvider = new AesManaged())
{
// Set cryptoProvider parameters
cryptoProvider.BlockSize = cryptoProvider.LegalBlockSizes[0].MaxSize;
cryptoProvider.KeySize = cryptoProvider.LegalKeySizes[0].MaxSize;
cryptoProvider.Key = rfc.GetBytes(cryptoProvider.KeySize / 8);
cryptoProvider.IV = rfc.GetBytes(cryptoProvider.BlockSize / 8);
}
Console.Writeline(Convert.ToBase64(cryptoProvider.Key));
Console.Writeline(Convert.ToBase64(cryptoProvider.IV));
$123456789101112134==
$600200300==
Subsequent calls to rfc.GetBytes always produces different results. MSDN says it compounds the key sizes on the calls. So if you call GetBytes(20), twice, it's the same as calling GetBytes(20+20) or GetBytes(40). Theoretically, this should just increase the size of the key, not completely change it.
There are some solutions to get around this issue, which could be generating a longer key on the first call, then slicing it into both a derived key AND an IV, or randomly generating an IV, appending it to the encoded message and peeling it off before decrypting it.
Slicing the python output produces the same results as .NET. It looks like this:
derived_key = PBKDF2(key, salt, 32, 1000)
iv = PBKDF2(key, salt, 32 + 16, 1000) # We need 16, but we're compensating for .NETs 'already called' awesomeness on the GetBytes method
split_key = iv[32:]
print(base64.b64encode(derived_key))
print(base64.b64encode(iv))
print(base64.b64encode(split_key))
$ 123456789101112134== # matches our derived key
$ 12345678== # doesn't match
$ 600200300== # matches. this is the base 64 encoded version of the tailing 16 bytes.
Enjoy,