Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AES Encryption in PowerShell and Python

My goal is to be able to AES encrypt a string in PowerShell, send it to a UNIX system with python available, and decrypt the string back to plain text. I would also like to be able to do the inverse. I am not a crypto guy or a PowerShell/python programmer, but this is what I have been able to do with the code so far:

function Create-AesManagedObject($key, $IV) {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    if ($IV) {
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)
        }
        else {
            $aesManaged.IV = $IV
        }
    }
    if ($key) {
        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        else {
            $aesManaged.Key = $key
        }
    }
    $aesManaged
}

function Encrypt-String($key, $unencryptedString) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($unencryptedString)
    $aesManaged = Create-AesManagedObject $key $IV
    $encryptor = $aesManaged.CreateEncryptor()
    $encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
    [byte[]] $fullData = $aesManaged.IV + $encryptedData
    $aesManaged.Dispose()
    [System.Convert]::ToBase64String($fullData)
}

function Decrypt-String($key, $encryptedStringWithIV) {
    $bytes = [System.Convert]::FromBase64String($encryptedStringWithIV)
    $IV = $bytes[0..15]
    $aesManaged = Create-AesManagedObject $key $IV
    $decryptor = $aesManaged.CreateDecryptor();
    $unencryptedData = $decryptor.TransformFinalBlock($bytes, 16, $bytes.Length - 16);
    $aesManaged.Dispose()
    [System.Text.Encoding]::UTF8.GetString($unencryptedData).Trim([char]0)
}

# key passphrase is a 16 byte string that is used to create the AES key.
$key_passphrase = "MypassphraseKey1"
# base64 encode the key.  The resulting key should be exactly 44 characters (43 characters with a single = of padding) (256 bits)
$Bytes = [System.Text.Encoding]::Ascii.GetBytes($key_passphrase)
$key =[Convert]::ToBase64String($Bytes)

# init is used to create the IV
$init = "This is an IV123"
# converts init to a byte array (e.g. T = 84, h = 104) and then sha1 hash it
$IV = (new-Object Security.Cryptography.SHA1Managed).ComputeHash( [Text.Encoding]::UTF8.GetBytes($init) )[0..15] 
write-output "IV is equal to $IV"


write-output "AES key is $key"
$unencryptedString = "testing"
$encryptedString = Encrypt-String $key $unencryptedString
$backToPlainText = Decrypt-String $key $encryptedString

write-output "Unencrypted string: $unencryptedString"
write-output "Encrypted string: $encryptedString"
write-output "Unencrytped string: $backToPlainText"

The PowerShell script seems to be working fine for encrypting and decrypting. For the python side, I can define the same AES key value since it is just base64 encoded of my key's passphrase. However, I do not get the same encrypted value of the string when executing (e.g. PowerShell outputs UXKWIhtaUgFOvN13bvA4tx4+2Hjkv4v6I1G3Xfl6zp0= and Python outputs BOJ3Ox4fJxR+jFZ0CBQ25Q==). I believe these would need to match in order to be able to decrypt but I could be mistaken. I know setting a static IV and key makes it insecure, but I am willing to do that in order to be able to encrypt and decrypt across platforms (unless there is a better method using AES). Any help would be appreciated.

Python code

import base64, array
import Crypto
import Crypto.Random
from Crypto.Cipher import AES

def pad_data(data):
    if len(data) % 16 == 0:
        return data
    databytes = bytearray(data)
    padding_required = 15 - (len(databytes) % 16)
    databytes.extend(b'\x80')
    databytes.extend(b'\x00' * padding_required)
    return bytes(databytes)

def unpad_data(data):
    if not data:
        return data

    data = data.rstrip(b'\x00')
    if data[-1] == 128: # b'\x80'[0]:
        return data[:-1]
    else:
        return data

def encrypt(key, iv, data):
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = pad_data(data)
    return aes.encrypt(data)

def decrypt(key, iv, data):
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = aes.decrypt(data)
    return unpad_data(data)

def test_crypto ():
    key = "MypassphraseKey1"
    # found using the debugger in the PowerShell ISE to get the value byte value which was converted to hex
    iv = "\x51\x72\x96\x22\x1b\x5a\x52\x01\x4e\xbc\xdd\x77\x6e\xf0\x38\xb7"
    msg = b"testing"

    # hex value of IV in powershell script is 51 72 96 22 1b 5a 52 01 4e bc dd 77 6e f0 38 b7
    print("Value of IV: " + iv)

    # base64 encode key
    b64key = base64.b64encode(key)
    print("AES key encoded: " + b64key)

    code = encrypt(key, iv, msg)
    # convert encrypted string to base64
    b64encoded = base64.b64encode(code)
    print("Encrypted string: " + b64encoded)

    decoded = decrypt(key, iv, code)
    print("Decoded: " + decoded)

if __name__ == '__main__':
    test_crypto()
like image 799
Michael Williams Avatar asked Jan 10 '17 19:01

Michael Williams


1 Answers

A couple suggestions:

  1. A 16 character ASCII string is 128^16 = 5.19229686e33 possible key inputs. Base64-encoding 16 bytes yields 24 bytes (4*ceil(16/3)). So even though you are using a 192 bit AES key (theoretically 6.27710174e57 key combinations), you can use only 1/1208925820422879877545683 [one over one trillion trillion] of them. In fact, you set the key size to be 256 bits, and apparently the code is ignoring that/allowing the 192 bit key without an error.

Use Rfc2898DeriveBytes to derive your AES key rather than a Base64 transformation of a raw string. RFC 2898 defines PBKDF2 (Password-Based Key Derivation Function 2), an HMAC-based Key Derivation Function to securely derive encryption keys from passwords, and provides for HMAC/SHA1 used with a high number of iterations to mitigate brute-force attacks against your key.

  1. You are only invoking TransformFinalBlock() on encrypt and decrypt in PowerShell. I imagine this will fail to encrypt or decrypt the complete message if the message is longer than one block (16 bytes). Try this with an input message like This is a plaintext message. (29 bytes). I believe you want to use both TransformBlock() and TransformFinalBlock().

  2. You are correct that a static IV is unsafe (defeats the purpose of an IV, which should be unique and non-predictable for every encryption operation with the same key). AesManaged already provides a method GenerateIV() to generate a satisfactory IV, which you can access from the IV property and prepend to the cipher text.

  3. Your PowerShell output of the Base64-encoded cipher text is 44 characters (16 byte IV + 16 byte ciphered message = 32 bytes -> 44 bytes in Base64). Your Python Base64 output is 24 characters (16 bytes -> 24 bytes in Base64). Either this output is not including the IV or the message (or some other less likely reason for the limited output). Looking at the code, your encrypt method does not prepend the IV onto the cipher text.

  4. Finally, at this point, your code should work and be both internally consistent and cross-compatible. Here you should revisit a couple design decisions:

  • Zero padding is non-standard and while you have implemented it manually, a well-defined padding scheme like PKCS #5/#7 is more desirable. There are bountiful implementations and code examples for implementing this in both Python and .NET.

  • You are using the CBC block cipher mode of operation. While CBC is fine for confidentiality, it does not provide integrity. You should use an authenticated encryption mode (AE/AEAD) like GCM or EAX. If you cannot, provide a message authentication code (MAC) over the cipher text by using an HMAC construction like HMAC/SHA-256 with a different shared secret than your encryption key and verify the MAC with a constant-time method before attempting decryption.

like image 188
Andy Avatar answered Oct 15 '22 11:10

Andy