8

I am seeing a small percentage of production users randomly report this exception related to encrypting/decrypting strings with Xamarin.Android but unfortunately I cannot reproduce it.

What could cause this and/or how could I reproduce the exception so that I can figure out a fix/workaround?

[CryptographicException: Bad PKCS7 padding. Invalid length 147.]
    Mono.Security.Cryptography.SymmetricTransform.ThrowBadPaddingException(PaddingMode padding, Int32 length, Int32 position):0
    Mono.Security.Cryptography.SymmetricTransform.FinalDecrypt(System.Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount):0
    Mono.Security.Cryptography.SymmetricTransform.TransformFinalBlock(System.Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount):0
    System.Security.Cryptography.CryptoStream.FlushFinalBlock():0
    com.abc.mobile.shared.Security+PasswordEncoder.DecryptWithByteArray(System.String strText, System.String strEncrypt):0

EDIT: Here's the code I am using to encrypt/decrypt

    private string EncryptWithByteArray(string inPassword, string inByteArray)
    {

        byte[] tmpKey = new byte[20];
        tmpKey = System.Text.Encoding.UTF8.GetBytes(inByteArray.Substring(0, 8));
        DESCryptoServiceProvider des = new DESCryptoServiceProvider();
        byte[] inputArray = System.Text.Encoding.UTF8.GetBytes(inPassword);
        MemoryStream ms = new MemoryStream();
        CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(tmpKey, mInitializationVector), CryptoStreamMode.Write);
        cs.Write(inputArray, 0, inputArray.Length);
        cs.FlushFinalBlock();
        return Convert.ToBase64String(ms.ToArray());

    }

        private string DecryptWithByteArray (string strText, string strEncrypt)
        {

            try
            {
                byte[] tmpKey = new byte[20];
                tmpKey = System.Text.Encoding.UTF8.GetBytes (strEncrypt.Substring (0, 8));
                DESCryptoServiceProvider des = new DESCryptoServiceProvider ();
                Byte[] inputByteArray = Convert.FromBase64String (strText);
                MemoryStream ms = new MemoryStream ();
                CryptoStream cs = new CryptoStream (ms, des.CreateDecryptor (tmpKey, mInitializationVector), CryptoStreamMode.Write);
                cs.Write (inputByteArray, 0, inputByteArray.Length);
            try {
                cs.FlushFinalBlock();
            } catch (Exception ex) {
                throw(ex);
            }
            System.Text.Encoding encoding = System.Text.Encoding.UTF8;
            return encoding.GetString(ms.ToArray());
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

EDIT 2:

The encryption key is always the local Device ID. Here's how I am getting this:

        TelephonyManager telephonyMgr = Application.Context.GetSystemService(Context.TelephonyService) as TelephonyManager;
        string deviceId = telephonyMgr.DeviceId == null ? "UNAVAILABLE" : telephonyMgr.DeviceId;

Here's an example of how it's called:

string mByteArray = GetDeviceId();
string mEncryptedString = EncryptWithByteArray(stringToEncrypt, mByteArray);
string mDecryptedString = DecryptWithByteArray(mEncryptedString, mByteArray);
Le-roy Staines
  • 2,037
  • 2
  • 22
  • 40
  • What block cipher and mode of operation? Can you provide a copy of the message that failed to decrypt? 147 seems like an odd length. It *usually* should be a multiple of the block cipher's block length (often 16). However, modes like CTS and CTR don't have that requirement. Assuming a mode like CBC, then it almost looks like an incomplete message is being processed. – jww Feb 04 '15 at 20:56
  • I added the code to my originalquestion =) – Le-roy Staines Feb 05 '15 at 23:34
  • based on your edit and the code using DES, 147 ***is*** wrong. DES's block size is 8 bytes, so the message must be a multiple of 8 bytes. Find the missing 3 bytes (it should be 152 bytes), and your problem is solved. Also, you should be using a `byte[]`, not a `String`, for the encrypted data. The encrypted data could include a 0x00 byte, which could cause problems. Or Base64 the data so it can handle the NULL bytes. – jww Feb 05 '15 at 23:41
  • Hmm I thought it was Base64-ing the data! But thanks, you've given me a better starting point to work with anyway. – Le-roy Staines Feb 05 '15 at 23:57

1 Answers1

4

You have not provided much details about your use case but I would say this is happening because you are not using the same cipher settings during the encryption and decryption operations. Symmetric ciphers require you to use exactly the same settings/parameters during the data encryption and also decryption. For example for AES CBC you would need to use exactly the same key, IV, cipher mode and padding on both devices. It is best to set these setting explicitly in the code:

System.Security.Cryptography.RijndaelManaged aes = new System.Security.Cryptography.RijndaelManaged();
aes.Key = new byte[] { ... };
aes.IV = new byte[] { ... };
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;

If you are sure you are using the same settings then you should also consider scenario that some data get corrupted or altered during the network transfer.

Edit after some code fragments have been provided:

Decryption method you have provided does not work for me at all so I have put together all your samples and turned them into the code which does the same thing as yours but uses IMO a slightly cleaner approach. For example this code uses more robust "key derivation" (please forgive me cryptoguys) and it has also passed basic code analysis.

You should be able to easily use public methods to do what you need:

string plainData = "This information should be encrypted";
string encryptedData = EncryptStringified(plainData);
string decryptedData = DecryptStringified(encryptedData);
if (plainData != decryptedData)
    throw new Exception("Decryption failed");

Implementation and private methods follows:

/// <summary>
/// Encrypts string with the key derived from device ID
/// </summary>
/// <returns>Base64 encoded encrypted data</returns>
/// <param name="stringToEncrypt">String to encrypt</param>
public string EncryptStringified(string stringToEncrypt)
{
    if (stringToEncrypt == null)
        throw new ArgumentNullException("stringToEncrypt");

    byte[] key = DeviceIdToDesKey();
    byte[] plainData = Encoding.UTF8.GetBytes(stringToEncrypt);
    byte[] encryptedData = Encrypt(key, plainData);
    return Convert.ToBase64String(encryptedData);
}

/// <summary>
/// Decrypts Base64 encoded data with the key derived from device ID
/// </summary>
/// <returns>Decrypted string</returns>
/// <param name="b64DataToDecrypt">Base64 encoded data to decrypt</param>
public string DecryptStringified(string b64DataToDecrypt)
{
    if (b64DataToDecrypt == null)
        throw new ArgumentNullException("b64DataToDecrypt");

    byte[] key = DeviceIdToDesKey();
    byte[] encryptedData = Convert.FromBase64String(b64DataToDecrypt);
    byte[] decryptedData = Decrypt(key, encryptedData);
    return Encoding.UTF8.GetString(decryptedData);
}

private byte[] DeviceIdToDesKey()
{
    TelephonyManager telephonyMgr = Application.Context.GetSystemService(Context.TelephonyService) as TelephonyManager;
    string deviceId = telephonyMgr.DeviceId ?? "UNAVAILABLE";

    // Compute hash of device ID so we are sure enough bytes have been gathered for the key
    byte[] bytes = null;
    using (SHA1 sha1 = SHA1.Create())
        bytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(deviceId));

    // Get last 8 bytes from device ID hash as a key
    byte[] desKey = new byte[8];
    Array.Copy(bytes, bytes.Length - desKey.Length, desKey, 0, desKey.Length);
    return desKey;
}

private byte[] Encrypt(byte[] key, byte[] plainData)
{
    if (key == null)
        throw new ArgumentNullException("key");

    if (plainData == null)
        throw new ArgumentNullException("plainData");

    using (DESCryptoServiceProvider desProvider = new DESCryptoServiceProvider())
    {
        if (!desProvider.ValidKeySize(key.Length * 8))
            throw new CryptographicException("Key with invalid size has been specified");
        desProvider.Key = key;
        // desProvider.IV should be automatically filled with random bytes when DESCryptoServiceProvider instance is created
        desProvider.Mode = CipherMode.CBC;
        desProvider.Padding = PaddingMode.PKCS7;

        using (MemoryStream encryptedStream = new MemoryStream())
        {
            // Write IV at the beginning of memory stream
            encryptedStream.Write(desProvider.IV, 0, desProvider.IV.Length);

            // Perform encryption and append encrypted data to the memory stream
            using (ICryptoTransform encryptor = desProvider.CreateEncryptor())
            {
                byte[] encryptedData = encryptor.TransformFinalBlock(plainData, 0, plainData.Length);
                encryptedStream.Write(encryptedData, 0, encryptedData.Length);
            }

            return encryptedStream.ToArray();
        }
    }
}

private byte[] Decrypt(byte[] key, byte[] encryptedData)
{
    if (key == null)
        throw new ArgumentNullException("key");

    if (encryptedData == null)
        throw new ArgumentNullException("encryptedData");

    using (DESCryptoServiceProvider desProvider = new DESCryptoServiceProvider())
    {
        if (!desProvider.ValidKeySize(key.Length * 8))
            throw new CryptographicException("Key with invalid size has been specified");
        desProvider.Key = key;
        if (encryptedData.Length <= desProvider.IV.Length)
            throw new CryptographicException("Too short encrypted data has been specified");
        // Read IV from the beginning of encrypted data
        // Note: New byte array needs to be created because data written to desprovider.IV are ignored
        byte[] iv = new byte[desProvider.IV.Length];
        Array.Copy(encryptedData, 0, iv, 0, iv.Length);
        desProvider.IV = iv;
        desProvider.Mode = CipherMode.CBC;
        desProvider.Padding = PaddingMode.PKCS7;

        // Remove IV from the beginning of encrypted data and perform decryption
        using (ICryptoTransform decryptor = desProvider.CreateDecryptor())
            return decryptor.TransformFinalBlock(encryptedData, desProvider.IV.Length, encryptedData.Length - desProvider.IV.Length);
    }
}

It is really hard to tell what exactly was problem with your code because your decryption method did not work for me at all - most likely because it is using CryptoStream in write mode for decryption which seems a little odd to me.

So much for the code. Now let's get to encryption which is really really weak. It is more just an obfuscation that should protect the data from being accidentally displayed in plain text form (some people use BASE64 encoding for the same thing). The main cause of this is relatively old encryption algorithm and easily predictable encryption key. AFAIK every application running on the same device can read device ID without any privileges. That means any application can decrypt your data. Of course your SQLite database is probably accessible only to your application but that can no longer be true if you remove the SD card or root your phone. To make this a little better you could for example ask user to provide a password and then use it to derive unique encryption key but that is completely different problem. Anyway I am not really sure what you are trying to achieve with this encryption - it may be fully sufficient for your needs even if it can be considered to be weak.

Hope this helps.

jariq
  • 11,681
  • 3
  • 33
  • 52
  • Please see my edits above RE the methods I am using to encrypt/decrypt. Unfortunately I have not found a way to duplicate the error -it is being reported by our error reporting implementation. I tried deliberately messing with the padding and the encryption strings but it throws a different error -although my lack of knowledge with encryption could be taking its toll as well. – Le-roy Staines Feb 05 '15 at 23:33
  • @Le-roy implementation of those methods seems a little weird to me but that can be caused by a missing context. Could you please describe input parameters because I am not sure I understand what you are trying to encrypt and where is the encryption key and initialization vector coming from. It would also help if you could extend your code with the part that actually calls these two methods. – jariq Feb 06 '15 at 20:25
  • Thanks Jariq. I'm encrypting an API Key then storing it in a SQLite database on the device. The encryption key is the device ID and I'm not sure what you mean by Initialization Vector! (apart from a quick Google just now). I initially tailored this code from something I found elsewhere on the net (I know!). I'll edit my OP above with some extra code. – Le-roy Staines Feb 07 '15 at 22:39