I have written some code using the pycrypt library and I must be doing something wrong, but I can't figure out what it is that I am doing wrong. I can (nearly) decrypt messages with the wrong initialization vector even though I believe I am following their examples.
from Crypto.Cipher import AES
import os
from string import ascii_letters
key, iv = os.urandom(32), os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = ascii_letters
plaintext += ' ' * (-len(plaintext) % 16) # Padding
ciphertext = cipher.encrypt(plaintext)
cipher = AES.new(key, AES.MODE_CBC, os.urandom(16))
text = cipher.decrypt(ciphertext)
# text[16:] = b'qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ '
Now aside from the first 16 bytes of the decrypted text, you have everything decrypted correctly with a totally random choice of IV. Can someone help me figure out where I am going wrong?
That's normal for cipher block chaining. In CBC decryption, the IV is only necessary to reconstruct the first block of plaintext. The computation of other blocks of plaintext doesn't actually involve the IV. Here's a diagram (source: Wikimedia user WhiteTimberwolf, public domain):
Related
To keep it short: I am sending an encrypted message (AES with CBC) to another service, and it returns me an encrypted response, but I can't decrypt it because they are not using padding? (to be honest, I don't know much about encryption and its mechanisms).
This is my implementation (based on the documentation) of a class used to encrypt and decrypt messages.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
class AESCipher:
def __init__(self, key, iv):
self.key = bytes.fromhex(key)
self.iv = bytes.fromhex(iv)
def encrypt(self, msg):
msg = pad(msg.encode(), AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
return cipher.encrypt(msg)
def decrypt(self, msg):
cipher = AES.new(self.key, AES.MODE_CFB, self.iv)
return unpad(cipher.decrypt(msg), AES.block_size)
I can easily encrypt (and even decrypt my own message) using this implementation, but the actual response from the host always fails to decrypt. First I thought it was a problem with the host, but when I use an AES online tool, it kinda works (I used this one)
To reproduce the issue, I send a malformed message and receive an encrypted error:
msg = b"X\xb4\xc6\xc9j\x92\x8f\xe5\x84\xe5\\N7\x8bv\xb8\x02\x0e\xed*\xe7\x92\xdd/\xf1\xff\xdfj 5\x00\x91\xb5;\xb6Q\x08\xc8\xf1PFF\x1aw\x93\xa7\xbe\xa7\xafD\xe7:=\x8b\x1d\x86i\xa8\x95\x107\xf2\xbcF1\x80D\x8c\x98\x1f\xfc\x80\xc3\xd6\x81'\xf3\xd98\x93\x8bv\xf7P\xc9\xb1L,\x8aJ\x05\xd8\xd0P\x10\rQ\xba\xf5&4\x0e\xf0\x97\xf5\xa5B\xb7\xbda_?\xcbk~\xe6\xfe\xf6\x8f\x92\x1b;#\xd2\x87\xc6^\n"
The key and iv are:
key = AEC273769C9C4E9830D5FA3929BE1F5115E4BF085BCBA6ACCBAEF63E654D8AE3
iv = ACE499278E5FDC6849DDF23A8966D7CF
I get this error:
File "/home/richter/Code/Test_encryption/encryptation.py", line 22, in decrypt
return unpad(uncrypt, AES.block_size)
File "/home/richter/Code/Test_encryption/.venv/lib64/python3.10/site-packages/Crypto/Util/Padding.py", line 92, in unpad
raise ValueError("Padding is incorrect.")
ValueError: Padding is incorrect.
I also tried a different library, one written entirely in python (and boy, it's slow) but got the same issue with the size of the package.
I've got a encrypt/decrypt class setup based on this SO answer. I've tested it and it works fine. It's not working for a new API I'm pulling information from. The new API is built with PHP and is using the following package to encrypt information: https://laravel.com/docs/8.x/encryption using Laravel Crypt() command. All encrypted values are encrypted using OpenSSL and the AES-256-CBC cipher.
The enc value after the first line of the decrypt method
def decrypt(self, enc):
enc = base64.b64decode(enc)
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')
looks like this (Don't worry, this is just non-sensitive demo data):
b"{'iv': 'Ld2pjRJqoKcWnW1P3tiO9Q==', 'value': 'M9QeHtbybeUxAVuIRcQ3bA==', 'mac': 'xxxxxx......'}"
, which basically looks like a byte-string JSON. The testing encryption key is base64:69GnnXWsW1qnW+soLXOVd8Mi4AdXKBFfkw88/7F2kSg=.
I know I can turn it into a dictionary like this
import json
d = json.loads(enc)
How should I manipulate values from this dictionary to prepare it to be decrypted like other encrypted text this class can successfully decrypt?
Update:
Based on comments I've tried to modify the method to look like this:
def decrypt(self, encrypted):
enc = base64.b64decode(encrypted)
if b'{"iv":' == enc[:6]:
d = json.loads(enc)
iv = base64.b64decode(d['iv'])
val = base64.b64decode(d['value'])
else:
iv = enc[:AES.block_size]
val = enc[AES.block_size:]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(val)).decode('utf-8')
This still does not work. It doesn't crash, but I'm getting a blank string back ('') and I know that's not what was encrypted. The answer should be 'Demo'
The code in the "Update" section of the question will work without any changes. You just need to make sure to remove the "base64:" prefix in the encryption key provided. Once that is removed, it will work as expected.
Based on the Golang documentation on CFB decryption I wrote a minimal working example to decrypt a string that was encrypted with AES CFB and then base 64 encoded in python3.
The golang decryption works fine when the message was encrypted within Golang (with the encryption function from the Golang doc example).
However when I encrypt the message in a python script using the python crypto package, I am unable to decrypt it in the golang script successfully. I don't get the right bytes back.
$ python3 stack.py
Going to encrypt and base64 "This is not encrypted" result:
b'jf9A5LCxKWPuNb1XiH+G3APAgR//'
Now going to call the Golang script:
b'Hello from Golang, going to decrypt: jf9A5LCxKWPuNb1XiH+G3APAgR//
Result: Tl!\xca/\xf1\xc0\xb2\xd01Y\x02V\xec\xdf\xecy\xd38&\xd9\n'
Blocksize is 16 by default for both AES implementations.
So the question: What is going wrong?
Golang script:
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"os"
)
func main() {
key := []byte("TfvY7I358yospfWKcoviZizOShpm5hyH")
iv := []byte("mb13KcoviZizvYhp")
payload_python := os.Args[1]
fmt.Println("Hello from Golang, going to decrypt: "+payload_python+" Result: "+string(decrypt(key, payload_python, iv)))
}
func decrypt(key []byte, cryptoText string, iv []byte) []byte {
ciphertext, _ := base64.StdEncoding.DecodeString(cryptoText) //decode base64 coding
//prepare decryption based on key and iv
block, _ := aes.NewCipher(key)
stream := cipher.NewCFBDecrypter(block, iv)
//decrypt
stream.XORKeyStream(ciphertext, ciphertext)
return ciphertext
}
Python script:
#!/usr/bin/env python3
import base64
from Crypto.Cipher import AES
from subprocess import check_output
original_message = 'This is not encrypted'
key = 'TfvY7I358yospfWKcoviZizOShpm5hyH'
iv = 'mb13KcoviZizvYhp'
#prepare encryption
cfb_cipher_encrypt = AES.new(key, AES.MODE_CFB, iv)
#encrypt and base64 encode
encryptedpayload = base64.b64encode(cfb_cipher_encrypt.encrypt(original_message))
print('Going to encrypt and base64 "{}" result:\n{}\n'.format(original_message,encryptedpayload))
print('Now going to call the Golang script:')
print(check_output('go run stack.go {}'.format(encryptedpayload.decode()),shell=True))
Try encrypting from Python like this.
The result can then be unencrypted from Go successfully.
#!/usr/bin/env python3
import base64
from Crypto.Cipher import AES
MODE = AES.MODE_CFB
BLOCK_SIZE = 16
SEGMENT_SIZE = 128
def _pad_string(value):
length = len(value)
pad_size = BLOCK_SIZE - (length % BLOCK_SIZE)
return value.ljust(length + pad_size, '\x00')
def encrypt(key, iv, plaintext):
aes = AES.new(key, MODE, iv, segment_size=SEGMENT_SIZE)
plaintext = _pad_string(plaintext)
encrypted_text = aes.encrypt(plaintext)
return encrypted_text
key = 'TfvY7I358yospfWKcoviZizOShpm5hyH'
iv = 'mb13KcoviZizvYhp'
original_message = 'This is not encrypted'
encryptedpayload = base64.b64encode(encrypt(key, iv, original_message))
print('Going to encrypt and base64 "{}" result:\n{}\n'.format(original_message,encryptedpayload))
Source: http://chase-seibert.github.io/blog/2016/01/29/cryptojs-pycrypto-ios-aes256.html
I'm using python to encrypt and decrypt files. When file encrypted, then try to decrypt like this:
from Crypto.Cipher import AES
from Crypto import Random
def decrypt(in_file, out_file, pwd, key_len=32):
bs = AES.block_size
salt = in_file.read(bs)[len('Salted__'):]
key, iv = derive_keyiv(pwd, salt, key_len, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
next_chunk = ''
finished = False
try:
while not finished:
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024*bs))
if len(next_chunk) == 0:
padding_len = ord(chunk[-1])
chunk = chunk[:-padding_len]
finished = True
out_file.write(chunk)
return True, None
except Exception as e:
return False, e
But if the password input error, the decrypt still decrypt in_file and write to out_file and no exception throw.
How to check the password error during decrypt?
AES by itself cannot check if the key is "correct". It is simply a pure function that transforms some bytes to some other bytes.
To achieve what you want, you need to implement it yourself. One way to do is to add a fixed header (like 16 bytes of zero) to the plaintext before encryption. Upon decryption, check and discard the said header if it matches or raise an error if the header mismatches.
A side note: you are doing encryption without any authentication, which is probably insecure.
Edit
First of all, you should add authentication. Encryption without authentication easily leads to many security flaws, many not obvious to the untrained. Especially since you are using AES in CBC mode, you may open yourself to padding oracle attacks without authentication.
When you do authenticated encryption the right way (encrypt-then-mac), you will get an authentication error if the user input the wrong password. If you want to further distinguish a wrong password from tampered data, you have to devise your own method, like prepending a ciphertext of magic number.
I got a problem with aes in python 2.7
import pyelliptic
iv = pyelliptic.Cipher.gen_IV('aes-256-cfb')
ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb')
ciphertext = ctx.update('test1')
ciphertext += ctx.final()
ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb')
Now I don't know how to send this msg to server, and decrypt it on server, because I don't know the IV and my server can't decrypt it. The server has the secret key.
The IV does not need to be kept secret, but it needs to unique (random) for every encrypt operation with the same key.
Many implementations just add the IV bytes to the front of the ciphertext. You have to know how long the IV is for your implementation so that you can slice it off before decrypting.
# encrypt
ciphertext = iv + ciphertext
# decrypt
blocksize = pyelliptic.Cipher.get_blocksize('aes-256-cfb')
iv = ciphertext[0:blocksize]
ciphertext = ciphertext[blocksize:]
From the code it is apparent that the IV is generated in the same size as the cipher blocksize, so it is safe to slice a block from the ciphertext to get the IV.