I'm trying RSA encrypt text with JSEncrypt(javascript) and decrypt with python crypto (python3.7). Most of the time, it works. But sometimes, python cannot decrypt.
const encrypt = new JSEncrypt()
encrypt.setPublicKey(publicKey)
encrypt.encrypt(data)
from base64 import b64decode
from Crypto.Cipher import PKCS1_v1_5 as Cipher_PKCS1_v1_5
from Crypto.PublicKey import RSA
crypt_text = "J9I/IdsSGZqrQ5XBTlDrze5+U3otrGEGn7J7f330/tbIpdPNwu9k5gCh35HJHuRF6tXhbOD9XbHS6dGXwRdj0KNSWa43tDQMyGp/ZSewCd4wWkqIx83YzDKnYTVc9zWYbg2iYrmR03AqtWMysl8vZDUSmQn7gNdYEJGxSUzVng=="
private_key = "MIICXQIBAAKBgQClFImg7N+5ziGtjrMDwN7frootgwrLUmbE9YFBtecnjchCRjAn1wqq69XiWynEv0q3/U91N5g0nJxeMuolSM8cwdQbT3KZFwQF6vreSzDNhfEYOsFVZknILLPiJpUYm5w3Gi34UeM60iHGH9EUnmQeVwKSG0WF2nK2SCU6EyfoJwIDAQABAoGAHHk2Y/N3g2zykiUS64rQ5nQMkV0Q95D2+PH/oX3mqQPjjsrcc4K77E9RTQG8aps0IBgpJGa6chixP+44RMYSMvRIK0wqgX7s6AFIkFIIM+v+bP9pd3kKaVKTcNIjfnKJZokgAnU0QVdf0zeSNElZC+2qe1FbblsSQ6sqaFmHaMECQQC4oZO+w0q2smQh7VZbM0fSIbdZEimX/4y9KN4VYzPQZkDzQcEQX1Al2YAP8eqlzB4r7QcpRJgvUQDODhzMUtP9AkEA5ORFhPVK5slpqYP7pj2F+D2xAoL9XkgBKmhVppD/Sje/vg4yEKCTQ7fRlIzSvtwAvbDJi3ytYqXQWVdaD/Eb8wJAdYC3k8ecTCu6WHFA7Wf0hIJausA5YngMLPLObFQnTLFXErm9UlsmmgATZZJz4LLIXPJMBXKXXD20Qm9u2oa4TQJBAKxBopP6KiFfSNabDkLAoFb+znzuaZGPrNjmZjcRfh6zr+hvNHxQ7CMVbnNWO7AJT8FyD2ubK71GvnLOC2hd8sMCQQCT70B5EpFqULt7RBvCa7wwJsmwaMZLhBcfNmbry/J9SZG3FVrfYf15r0SBRug7mT2gRmH+tvt/mFafjG50VCnw"
decode_data = b64decode(crypt_text)
other_private_key = RSA.importKey(b64decode(private_key))
cipher = Cipher_PKCS1_v1_5.new(other_private_key)
decrypt_text = cipher.decrypt(decode_data, None).decode()
print(decrypt_text)
this is a example text that python can't decrypt, but js can decrypt it well.
python throws the error:
File "/usr/local/lib/python3.7/site-packages/Crypto/Cipher/PKCS1_v1_5.py", line 165, in decrypt
raise ValueError("Ciphertext with incorrect length.")
ValueError: Ciphertext with incorrect length.
If the ciphertext is Base64-decoded, the reason becomes clearer: The ciphertext doesn't have the length of the modulus (128 byte), but only 127 byte, i.e. it isn't padded to the length of the modulus with leading 0x00 values. This ciphertext is invalid (see RFC8017, step 1) and the decryption in the Python code fails with the error message Ciphertext with incorrect length. In contrast, the decryption in the JavaScript code works, i.e. JSEncrypt#decrypt obviously adjusts the ciphertext to the length of the modulus by stealthily padding with 0x00 values. If the ciphertext was created with JSEncrypt#encrypt, this method doesn't seem to work properly.
In detail: The modulus can be determined with:
openssl rsa -modulus -noout -in <path to private key>
and is (as hex-string):
A51489A0ECDFB9CE21AD8EB303C0DEDFAE8A2D830ACB5266C4F58141B5E7278DC842463027D70AAAEBD5E25B29C4BF4AB7FD4F753798349C9C5E32EA2548CF1CC1D41B4F7299170405EAFADE4B30CD85F1183AC1556649C82CB3E22695189B9C371A2DF851E33AD221C61FD1149E641E5702921B4585DA72B648253A1327E827
The length is 128 byte. The Base64-decoded ciphertext is (as hex-string):
27d23f21db12199aab4395c14e50ebcdee7e537a2dac61069fb27b7f7df4fed6c8a5d3cdc2ef64e600a1df91c91ee445ead5e16ce0fd5db1d2e9d197c11763d0a35259ae37b4340cc86a7f6527b009de305a4a88c7cdd8cc32a761355cf735986e0da262b991d3702ab56332b25f2f6435129909fb80d7581091b1494cd59e
The length is 127 byte. If the ciphertext is padded manually to the length of the modulus with 0x00-values, it can also be decrypted in the Python code:
0027d23f21db12199aab4395c14e50ebcdee7e537a2dac61069fb27b7f7df4fed6c8a5d3cdc2ef64e600a1df91c91ee445ead5e16ce0fd5db1d2e9d197c11763d0a35259ae37b4340cc86a7f6527b009de305a4a88c7cdd8cc32a761355cf735986e0da262b991d3702ab56332b25f2f6435129909fb80d7581091b1494cd59e
The decrypted data are:
Mzg4MDE1NDU4MTI1ODI0OA==NDQyODYwNjI1MjU4NTM2MA==
which are two valid Base64-encoded strings.
Thanks to Topaco, it solved.
from base64 import b64decode, b16decode
from Crypto.Cipher import PKCS1_v1_5 as Cipher_PKCS1_v1_5
from Crypto.PublicKey import RSA
crypt_text = \
"R247QGAFEeSW1wwXQuNf/cm/K/tnW5xwXLb5MuHW6/Fr8SRklM0n6Rmj07TgFwApeN72j/avXAvpoR70U92ehOJsDnnZguYN4u2bMXHDyTNmAXuJw9xPm59bSGcvgRm1X+V0Zq1FLzGEsPG6tOYEIX+wnIuH3P7QMd02XJfj0w0="
private_key = "MIICXQIBAAKBgQClFImg7N+5ziGtjrMDwN7frootgwrLUmbE9YFBtecnjchCRjAn1wqq69XiWynEv0q3/U91N5g0nJxeMuolSM8cwdQbT3KZFwQF6vreSzDNhfEYOsFVZknILLPiJpUYm5w3Gi34UeM60iHGH9EUnmQeVwKSG0WF2nK2SCU6EyfoJwIDAQABAoGAHHk2Y/N3g2zykiUS64rQ5nQMkV0Q95D2+PH/oX3mqQPjjsrcc4K77E9RTQG8aps0IBgpJGa6chixP+44RMYSMvRIK0wqgX7s6AFIkFIIM+v+bP9pd3kKaVKTcNIjfnKJZokgAnU0QVdf0zeSNElZC+2qe1FbblsSQ6sqaFmHaMECQQC4oZO+w0q2smQh7VZbM0fSIbdZEimX/4y9KN4VYzPQZkDzQcEQX1Al2YAP8eqlzB4r7QcpRJgvUQDODhzMUtP9AkEA5ORFhPVK5slpqYP7pj2F+D2xAoL9XkgBKmhVppD/Sje/vg4yEKCTQ7fRlIzSvtwAvbDJi3ytYqXQWVdaD/Eb8wJAdYC3k8ecTCu6WHFA7Wf0hIJausA5YngMLPLObFQnTLFXErm9UlsmmgATZZJz4LLIXPJMBXKXXD20Qm9u2oa4TQJBAKxBopP6KiFfSNabDkLAoFb+znzuaZGPrNjmZjcRfh6zr+hvNHxQ7CMVbnNWO7AJT8FyD2ubK71GvnLOC2hd8sMCQQCT70B5EpFqULt7RBvCa7wwJsmwaMZLhBcfNmbry/J9SZG3FVrfYf15r0SBRug7mT2gRmH+tvt/mFafjG50VCnw"
decode_data = b64decode(crypt_text)
if len(decode_data) == 127:
hex_fixed = '00' + decode_data.hex()
decode_data = b16decode(hex_fixed.upper())
other_private_key = RSA.importKey(b64decode(private_key))
cipher = Cipher_PKCS1_v1_5.new(other_private_key)
decrypt_text = cipher.decrypt(decode_data, None).decode()
print(decrypt_text)
Related
I have been struggling with this for a couple of days and was wondering if anyone would have the experience to know these two encryption libraries well enough to help.
I am currently creating a SSO payload according to instructions given to me by a vendor. The steps to have this created are highlighted as follows:
Create an AES 256 CBC cypher of the payload
i. The key will be a SHA256 digest of the site token.
2. Base64 encode the initialization vector (IV) and encrypted payload from above
3. CGI-escape the output from step 2.
4. Your final payload would look something like ikUbqiutwMhi%2Bjg6WwUHyeZB76g6LdLGcrKrEV4YpvQ%3D%0A.
SHA256 will always generate a 32-byte hash, but it can’t be displayed nicely in Base64. When it’s displayed as Hex, it is 32 pairs of Hex values (a total of 64 characters on the screen) but representing only 32 bytes.
I was able to get it to work on Ruby with Open SSL, the code is:
require 'digest'
require 'openssl'
require "base64"
require 'cgi'
require 'json'
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.encrypt
cipher.key = Digest::SHA256.digest(siteToken)
iv = cipher.random_iv
data= unencryptedPayload
encrypted = cipher.update(JSON.generate(data)) + cipher.final
encoded = CGI::escape(Base64.encode64(iv + encrypted))
puts encoded
However, I have not yet had luck with Node.js's Crypto library. This is what I have so far:
const crypto = require('crypto');
// Defining algorithm
const algorithm = 'aes-256-cbc';
// Defining key
//'key' variable is defined and equal to siteToken in the OpenSSL version
//const key = siteToken;
// Defining iv
const iv = crypto.randomBytes(16);
// An encrypt function
function encrypt(text) {
// Creating Cipheriv with its parameter
let cipher = crypto.createCipheriv(
'aes-256-cbc', Buffer.from(key), iv);
// Updating text
let encrypted = cipher.update(text);
// Using concatenation
encrypted = Buffer.concat([encrypted, cipher.final()]);
// Returning iv and encrypted data
return { iv: iv.toString('hex'),
encryptedData: encrypted.toString('hex') };
}
// Displays output
var output = encrypt(unencryptedPayload);
I think my code has so far covered almost all of these except for the SHA256 digest of the site token. Does anyone know how I might achieve this in Node.js terms?
Thanks!
I’d like to decrypt an AES-encrypted string (CCM mode) in Python 3.
The following JavaScript code which is using the sjcl library is working correctly:
const sjcl = require('sjcl');
const key = "ef530e1d82c154170296467bfe40cdb47b9ad77e685bbf8336b145dfa0e85640";
const keyArray = sjcl.codec.hex.toBits(key);
const iv = sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(key.substr(0,16)));
const params = {
"iv": iv,
"v": 1,
"iter": 1000,
"ks": 256,
"ts": 128,
"mode": "ccm",
"adata": "",
"cipher": "aes",
"salt": "",
};
function encrypt(data) {
const ct = JSON.parse(sjcl.encrypt(keyArray, data, params)).ct;
return sjcl.codec.hex.fromBits(sjcl.codec.base64.toBits(ct));
}
function decrypt(data) {
const ct = sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(data));
const paramsWithCt = JSON.stringify({ ...params, ...{ "ct": ct } });
return sjcl.decrypt(keyArray, paramsWithCt);
}
let ct = encrypt("my secret string");
console.log("Cipher Text: " + ct);
let plain = decrypt(ct);
console.log("Plain Text: " + plain);
Output:
$ npm i sjcl
$ node index.js
Cipher Text: fa90bcdedbfe7ba89b69216e352a90fa57a63871fc4da7e69ab7f897f427f8e3
Plain Text: my secret string
Which library can I use to do the same in Python?
I tried using the pycryptodome library, but it accepts a different set of parameters:
key (bytes) – the cryptographic key
mode – the constant Crypto.Cipher.<algorithm>.MODE_CCM
nonce (bytes) – the value of the fixed nonce. It must be unique for the combination message/key. For AES, its length varies from 7 to 13 bytes. The longer the nonce, the smaller the allowed message size (with a nonce of 13 bytes, the message cannot exceed 64KB). If not present, the library creates a 11 bytes random nonce (the maximum message size is 8GB).
mac_len (integer) – the desired length of the MAC tag (default if not present: 16 bytes).
msg_len (integer) – pre-declaration of the length of the message to encipher. If not specified, encrypt() and decrypt() can only be called once.
assoc_len (integer) – pre-declaration of the length of the associated data. If not specified, some extra buffering will take place internally.
The sjcl operates on arrays of 4 byte words. With sjcl.codec.hex.toBits() the hex encoded key is converted into such an array. The first 8 bytes (16 hexdigits) of the key are used as nonce.
Key size, tag size, algorithm and mode are determined from the params object. The params object further contains parameters for the key derivation, e.g. iter, salt, etc.), but these are ignored here since the key is passed as an array and not as a string.
Nonce and ciphertext are passed Base64 encoded within the params object.
The ciphertext is the concatenation of the actual ciphertext and the tag in this order, which must also be passed to the decryption in this format.
While the sjcl processes ciphertext and tag concatenated, PyCryptodome handles both separately. Apart from that, encryption and decryption in Python is straightforward with PyCryptodome:
from Crypto.Cipher import AES
data = b'my secret string'
key = bytes.fromhex('ef530e1d82c154170296467bfe40cdb47b9ad77e685bbf8336b145dfa0e85640')
nonce = bytes.fromhex('ef530e1d82c154170296467bfe40cdb47b9ad77e685bbf8336b145dfa0e85640')[:8]
# Encryption
cipher = AES.new(key, AES.MODE_CCM, nonce)
ciphertext, tag = cipher.encrypt_and_digest(data)
ciphertextTagHex = ciphertext.hex() + tag.hex()
print(ciphertextTagHex) # fa90bcdedbfe7ba89b69216e352a90fa57a63871fc4da7e69ab7f897f427f8e3
# Decryption
ciphertextTag = bytes.fromhex(ciphertextTagHex)
ciphertext = ciphertextTag[:-16]
tag = ciphertextTag[-16:]
cipher = AES.new(key, AES.MODE_CCM, nonce)
try:
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
print(decrypted.decode('utf-8')) # my secret string
except ValueError:
print('Decryption failed')
Note that it is insecure to derive the nonce from the key. This is especially true for CCM, s. e.g. RFC4309, p. 3, last section:
AES CCM employs counter mode for encryption. As with any stream
cipher, reuse of the same IV value with the same key is catastrophic.
Instead, the nonce should be randomly generated for each encryption. The nonce is not secret and is usually concatenated with the ciphertext at byte level, typically nonce|ciphertext|tag.
I want to decrypt a string that has been encrypted with openssl on the server like this:
openssl enc -e -aes-256-cbc -pbkdf2 -a -S 0123456789ABCDEF -A -k mypassword
Note this is done providing only a salt and password, and openssl should handle key and IV automatically. Am I too optimistic that this can happen when the browser decrypts too? If at all possible, I want to do it with only those encryption settings, or the bare minimum of increased complexity. In the browser, I'm trying to decrypt with CryptoJS like this:
import * as CryptoJS from 'crypto-js'
const encrypted = <ENCRYPTED_STRING_FROM_SERVER>
const password = 'mypassword'
const salt = '0123456789ABCDEF'
const key = CryptoJS.PBKDF2(password, salt) // Generate key
const bytes = CryptoJS.AES.decrypt(encrypted, key)
const decrypted = bytes.toString(CryptoJS.enc.Utf8)
console.log(decrypted)
But the call to CryptoJS.AES.decrypt errors with Cannot read property '0' of undefined, crypto-js/cipher-core.js:371. The docs for CryptoJS.AES.decrypt are quite thin, and any settings I am aware of to change when calling that func seem to give the same error. Thanks to anyone who can shine light!
In the OpenSSL statement, the iteration count and digest are not specified, so the default values 10000 and SHA256 are used. This is relevant because CryptoJS uses different default values (1 and SHA1).
CryptoJS applies the OpenSSL format for the ciphertext, i.e. the encrypted data starts with the ASCII encoding of Salted__ followed by the salt and then the ciphertext. Therefore the beginning of the Base64 encoded ciphertext starts always with U2FsdGVkX1.
CryptoJS uses the WordArray data type, which encapsulates an array of words. A word consists of 4 bytes.
During decryption, ciphertext and salt must first be separated. Then, key and IV must be determined using PBKDF2. Due to the different default values, iteration count and digest must be specified explicitly. Finally it can be decrypted:
// 1. Separate ciphertext and salt
var encrypted = "U2FsdGVkX18BI0VniavN78vlhR6fryIan0VvUrdIr+YeLkDYhO2xyA+/oVXJj/c35swVVkCqHPh9VdRbNQG6NQ=="
var encryptedWA = CryptoJS.enc.Base64.parse(encrypted);
var prefixWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(0, 8/4)); // Salted__ prefix
var saltWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(8/4, 16/4)); // 8 bytes salt: 0x0123456789ABCDEF
var ciphertextWA = CryptoJS.lib.WordArray.create(encryptedWA.words.slice(16/4, encryptedWA.words.length)); // ciphertext
// 2. Determine key and IV using PBKDF2
var password = 'mypassword'
var keyIvWA = CryptoJS.PBKDF2(
password,
saltWA,
{
keySize: (32+16)/4, // key and IV
iterations: 10000,
hasher: CryptoJS.algo.SHA256
}
);
var keyWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(0, 32/4));
var ivWA = CryptoJS.lib.WordArray.create(keyIvWA.words.slice(32/4, (32+16)/4));
// 3. Decrypt
var decryptedWA = CryptoJS.AES.decrypt(
{ciphertext: ciphertextWA},
keyWA,
{iv: ivWA}
);
var decrypted = decryptedWA.toString(CryptoJS.enc.Utf8)
console.log(decrypted)
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
More details can be found in the CryptoJS documentation.
try this lib in browser
https://www.npmjs.com/package/cryptojs2
More details can be found in the documentation.
In communication between two applications, I'd like to encrypt a piece of information in JavaScript and decrypt the message from an Objective-C client using a fixed key (just for basic security).
Encryption works well:
var command = "mjallo";
var crypto_key = CryptoJS.enc.Base64.parse('280f8bb8c43d532f389ef0e2a5321220');
var crypto_iv = CryptoJS.enc.Base64.parse("CC0A69779E15780A");
// Encrypt and encode
var encrypted = CryptoJS.AES.encrypt(command, crypto_key, {iv: crypto_iv}).toString();
var encrypted_and_encoded = btoa(encrypted);
// encrypted_and_encoded => 'dFBQVDZZS3dGSktoa0J3Y1NQOElpZz09'
// Confirms that decrypt works with CryptoJS:
// Decode and decrypt
var decrypted = CryptoJS.AES.decrypt(atob(encrypted_and_encoded), crypto_key, {iv: crypto_iv});
// decrypted => 'mjallo'
How would you go about decoding and decrypting the message in Objective-c after it was encrypted by CryptoJS?
I've attempted to decrypt using CocoaSecurity, but with no luck. Following is RubyMotion syntax:
begin
res = CocoaSecurity.aesDecryptWithBase64('dFBQVDZZS3dGSktoa0J3Y1NQOElpZz09', hexKey: '280f8bb8c43d532f389ef0e2a5321220', hexIv: 'CC0A69779E15780A')
rescue NSException => e
p e.reason # => "Length of iv is wrong. Length of iv should be 16(128bits)"
end
AES supports a block size of 128 bit and key sizes of 128, 192 and 256 bit. The IV for CBC mode (which is the default) should be 128 bit.
Your encoded key consists of 32 characters. In CryptoJS you're parsing it as Base64 which results in a 192 bit key, but in CocoaSecurity you're assuming that it is Hex encoded. Since it only contains digits and the letters a to f, it's likely Hex encoded and not Base64 encoded. If one would assume that it is Hex encoded, then one would get a valid AES key size of 128 bit:
var crypto_key = CryptoJS.enc.Hex.parse('280f8bb8c43d532f389ef0e2a5321220');
Your IV on the other hand doesn't have a valid size under the same assumption. An IV should be 16 bytes long for AES in CBC mode. Additionally, an IV should never be fixed at a static value. You would need to generate a random IV for every encryption. Since the IV doesn't have to be secret, you can send it along with the ciphertext.
var crypto_iv = CryptoJS.lib.WordArray.random(128/8);
console.log("IV: " + crypto_iv.toString()); // hex encoded
The result of CryptoJS.<Cipher>.encrypt() is a special formattable object. If you call toString() on that object, you will get a Base64 encoded ciphertext (optionally with a salt when password-based encryption was used). But then you're encoding it again with Base64 by calling btoa(). You don't need to encode it twice.
var encrypted = CryptoJS.AES.encrypt(command, crypto_key, {iv: crypto_iv}).toString();
console.log("Ciphertext (Base64): " + encrypted.toString());
console.log("Ciphertext (Hex): " + encrypted.ciphertext.toString());
As far I can judge, your RubyMotion code looks fine.
If you can only change the CocoaSecurity code, then you will need to
re-encode the key by decoding it as Base64 and encoding it as Hex,
append 16 "0" characters to the IV hex string, because CryptoJS fills the IV up to the next valid IV with 0x00 bytes,
decode the ciphertext once from Base64.
You should always authenticate the ciphertexts. This can either be done with an authenticated mode like GCM or with an HMAC over the ciphertext.
Here is a JavaScript part which decodes a string with AES encryption
var p = 'some large string'
var s = 'Q05WTmhPSjlXM1BmeFd0UEtiOGg='
var y = CryptoJS.AES.decrypt({
ciphertext: CryptoJS.enc.Base64.parse(p)
}, CryptoJS.enc.Base64.parse(s), {
iv CryptoJS.enc.Hex.parse("random")
});
var v = y.toString(CryptoJS.enc.Utf8)
I am trying to code a similar decoding function in python with importing AES.
Could anyone help me with this one. I can't figure out all equivalent code for js to python.
I looked up this page
Python AES Decryption Routine (Code Help)
and
AES - Encryption with Crypto (node-js) / decryption with Pycrypto (python)
Not sure if they have the code similar to the js I have here
"y.toString(CryptoJS.enc.Utf8)"
This in python what it means
I have tried something like this from another source
from base64 import b64decode
from Crypto.Cipher import AES
iv = 'random'
key = 'Q05WTmhPSjlXM1BmeFd0UEtiOGg='
encoded = b64decode('some large string')
dec = AES.new(key=key, mode=AES.MODE_CBC, IV=iv)
value = dec.decrypt(encoded)
There are multiple problems with your CryptoJS code and Python code.
Wrong key size
Your key s contains only 20 bytes (160 bit) which doesn't constitute any of the valid key sizes for AES which are 128 (10), 192 (12) and 256 bit (14 rounds). CryptoJS will silently run the key schedule for a 160 bit key with 11 rounds which PyCrypto doesn't support (see AES.c).
You can reduce the key to 128 bit like this in CryptoJS:
var key = CryptoJS.enc.Base64.parse('Q05WTmhPSjlXM1BmeFd0UEtiOGg=');
key.sigBytes = 16;
key.clamp();
or in Python:
key = b64decode('Q05WTmhPSjlXM1BmeFd0UEtiOGg=')[:16]
Wrong character encoding
You forgot to decode the key from a Base64 string in Python and you forgot to decode the IV from hex. The character '0' and the byte 0x00 are entirely different. There's an easier way to define an all zero IV:
iv = "\0"*16
No unpadding
CryptoJS uses PKCS#7 padding by default, but PyCrypto doesn't implement any padding and only handles data as a multiple of the block size. After you decrypt something, you need to remove the padding yourself in Python:
value = value[:value[-1]]
(the last byte determines how many bytes are padding bytes). More on that here.
Other considerations:
You really shouldn't be setting the IV to a static value. The IV should be randomly generated for every encryption using the same key. Otherwise, you will lose semantic security. Since the IV doesn't have to be secret, you can put it in front of the ciphertext and slice it off before decryption.