Rijndael PHP против C# - неверный размер ключа в C#, но не в PHP

Я пытаюсь зашифровать строку (json) с помощью Rijndael в C# и придумаю строку, которую я могу предложить веб-сервису PHP. Этот веб-сервис, в свою очередь, декодирует строку, используя IV и masterkey (известный им). Я должен написать код C#, который может общаться со службой PHP, я не контролирую / не владею службой PHP.

Код PHP для шифрования выглядит следующим образом:

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)))

Приведенный выше код мне придется преобразовать в C#, чтобы я мог зашифровать свой JSON и предложить его веб-сервису PHP.

Есть две проблемы. Первый был с длиной мастер-ключа, второй (может быть связан) с rawurlencode зашифрованных данных (мне пока сложно это проверить).

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;
}

EncryptStringToBytes выполняет некоторые проверки и использует этот код (взят из множества примеров в Интернете):

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();
        }
    }
}

Я получу ошибку:

Specified key is not a valid size for this algorithm.

Итак, проблемы вкратце:

1) Почему код PHP принимает ключ длиной 29 в Rijndael 256 (режим CBC), а мой C# - нет? Я поэкспериментировал с Mode, позже добавил Padding, установил KeySize (уже был 256 по умолчанию), и я просто не вижу, что я делаю здесь неправильно.

2) Когда я использую ключ длиной 32, он принимается, и мой код работает. Я также могу расшифровать его в C# (но не могу проверить это в PHP). Я хотел бы решить проблему 1, а затем продолжить проблему 2, но, возможно, кто-то может дать мне некоторое понимание здесь. Зашифрованная строка содержит 1 '=' в IV и 2x '==' (в конце) в зашифрованном json. Я читал о padding и тому подобном, но мне было интересно, почему в примерах PHP, которые я получил, не видно никаких знаков '='. Опять же, возможно, после устранения проблемы 1 это не будет проблемой.

Большое спасибо за чтение, и я надеюсь, что я не слишком глуп. После вчерашнего дня попыток у меня возникло ощущение, что я пробовал много разных подходов, и, похоже, они не работают.

2 ответа

Решение

Просто подумал, что я бы немного добавил к тому, что сказал @artjom-b.

Во-первых, это работает:-)

Но кроме того, вам нужно изменить свой

rj.Padding = PaddingMode.PKCS7

использовать

rj.Padding = PaddingMode.Zeros 

Кроме того, технически ваши две функции не возвращают одно и то же. PHP возвращает базу 64 из двух сцепленных битов двоичных данных, тогда как C# возвращает объединение отдельных строк b64. Результат будет другим во второй половине возвращаемой строки.

РЕДАКТИРОВАТЬ: грубая и готовая процедура расшифровки:

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: Все предостережения и предупреждения от Artjom B все еще применяются.

Вы используете старую версию PHP, которая с радостью принимает ключи недопустимой длины. Rijndael поддерживает размеры ключей 16, 24 и 32 байта и ничего между ними. Расширение mcrypt в PHP молча дополняет ключ 0x00 байтами до следующего допустимого размера ключа, который составляет 32 байта. Вам придется сделать то же самое в C#:

byte[] key = new byte[32];
byte[] password = Encoding.UTF8.GetBytes(encryptionKey);
Array.Copy(password, key, password.Length);
rj.Key = key;

Имейте в виду, что для обеспечения некоторой безопасности ключ должен иметь высокую энтропию. Пароль не является ключом и, следовательно, не обеспечивает особой энтропии из-за ограниченного набора символов и использования возможных слов. Всегда извлекайте ключ из пароля с помощью доступных функций деривации, таких как Argon2, scrypt, bcrypt или PBKDF2, с высоким коэффициентом затрат / количеством итераций и случайной солью.

Вы также должны добавить аутентификацию в ваши шифротексты. В противном случае злоумышленник может изменить зашифрованный текст без вашего ведома. Это можно сделать, используя режим аутентификации, такой как GCM/EAX, или запустив HMAC по зашифрованному тексту, чтобы создать тег аутентификации.

Другие вопросы по тегам