You just have to xor-ing the first 16 bytes of the plaintext and the first 16 bytes of the ciphertext. The result is the IV encrypted with the AES primitive, which must therefore be decrypted with the AES primitive (which is functionally identical to using the ECB mode with padding disabled).
You can see this most easily with the CTR flowchart.
Example:
Key (hex): 01ae3fd52761ebe55ebae2d33ff7e380ef32e264fabc32890079ca8037eed254
IV (hex): 4eb334a2ebcdcbe46399a5e445c61ac0
Plaintext: The quick brown fox jumps over the lazy dog
Plaintext (hex): 54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67
Ciphertext (hex): a2b6bd79e3aa8709f081ca15e513d548336a84ca205d8a9ca3955bb00ac8d70b68beabe5658cdab543a5bb
which can be checked with CyberChef here.
Since you tagged your question with CryptoJS, the following solution uses CryptoJS:
var ptFirst16bytes = hex2ab("54686520717569636b2062726f776e20"); // first 16 bytes of plaintext
var ctFirst16bytes = hex2ab("a2b6bd79e3aa8709f081ca15e513d548"); // first 16 bytes of ciphertext
var encIv = xor(ptFirst16bytes, ctFirst16bytes); // xor both values, gives the encrypted IV
var encIvWA = CryptoJS.lib.WordArray.create(encIv); // convert to WordArray (for processing with CryptoJS)
var keyWA = CryptoJS.enc.Hex.parse("01ae3fd52761ebe55ebae2d33ff7e380ef32e264fabc32890079ca8037eed254");
var ivWA = CryptoJS.AES.decrypt( // decrypt with AES primitive (i.e. ECB, no padding)
{ciphertext: encIvWA},
keyWA,
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding});
console.log("IV (hex): " + ivWA.toString());
function xor(aBuff, bBuff){
var cBuff = new Uint8Array(aBuff.length);
for (var i = 0; i < aBuff.length; i++){
cBuff[i] = aBuff[i] ^ bBuff[i];
}
return cBuff;
}
function hex2ab(hex){
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {
return parseInt(h, 16)}));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
The result is equal to the IV used in encryption.
Edit:
Regarding your comment that you apply NodeJS' crypto module instead of CryptoJS: In NodeJS, the determination of the IV can be implemented a bit more compactly:
var crypto = require("crypto")
var ptFirst16bytes = Buffer.from("54686520717569636b2062726f776e20", "hex");
var ctFirst16bytes = Buffer.from("a2b6bd79e3aa8709f081ca15e513d548", "hex");
var encIv = xor(ptFirst16bytes, ctFirst16bytes);
var key = Buffer.from("01ae3fd52761ebe55ebae2d33ff7e380ef32e264fabc32890079ca8037eed254", "hex");
var decipher = crypto.createDecipheriv("aes-256-ecb", key, null);
decipher.setAutoPadding(false);
var iv = Buffer.concat([decipher.update(encIv), decipher.final()]);
console.log("IV (hex): " + iv.toString("hex"));
function xor(a, b) {
var result = Buffer.alloc(a.length);
for (var i = 0; i < a.length; i++) {
result[i] = a[i] ^ b[i];
}
return result;
}
which gives the same IV for the same input data as the CryptoJS code: 0x4eb334a2ebcdcbe46399a5e445c61ac0
.
The IV determined in this way can then be used to decrypt the entire ciphertext by performing a decryption with CTR:
const crypto = require("crypto")
const ciphertext = Buffer.from("a2b6bd79e3aa8709f081ca15e513d548336a84ca205d8a9ca3955bb00ac8d70b68beabe5658cdab543a5bb", "hex");
const iv = Buffer.from("4eb334a2ebcdcbe46399a5e445c61ac0", "hex");
const secretKey = Buffer.from("01ae3fd52761ebe55ebae2d33ff7e380ef32e264fabc32890079ca8037eed254", "hex");
const decipher = crypto.createDecipheriv("aes-256-ctr", secretKey, iv);
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
console.log(decrypted.toString('utf8'));
which gives the original plaintext: The quick brown fox jumps over the lazy dog
.
If this decryption fails, then it means that the premises were wrong, i.e. when determining the IV, plaintext and ciphertext are not related, and/or the key is wrong, and/or it was not encrypted with CTR, etc. Then, an incorrect IV would be determined and the decryption of the remaining ciphertext would fail.