CTR Mode in AES
In its purest form, AES is a block cipher of 128 bits, but most current applications convert it into a stream cipher mode, where we just need to use an XOR with the plaintext stream to create our ciphertext. Two of the most popular stream modes for AES are GCM and CTR.
It was in 1995 that Diffie and Hellman created the CTR mode [here]:
Basically it converts the block cipher into a stream by creating a keystream using counter. This keystream should not repeat for a long time, as if it does, the ciphertext stream can be cracked. We can use any method to create the counter, but we mostly just increment it by one each time:
In the above, we can see we use a simple XOR operation between the plaintext and the block cipher. This makes the cipher fast, and easy to implement in hardware. Overall, though, we need to add a MAC (Message Authentication Code) to the ciphertext, as Eve can flip bits in the cipher:
Normally, this is an HMAC signature. GCM mode also suffers from bit-flipping, and has a GMAC code attached to the ciphertext. It should be noted that the counter value needs to be attached to the ciphertext, in order for the receiver to be able to decrypt the ciphertext with the correct key stream.
Coding
So let’s code with JavaScript. The following is the code [here]:
<script>
function buf2hex(buffer) { // buffer is an ArrayBuffer
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
(() => {
let ciphertext;
let iv;
function getMessage() {
let message = document.getElementById('message').value;
let enc = new TextEncoder();
return enc.encode(message);
}
async function encryptMessage(key) {
let encoded = getMessage();
iv = window.crypto.getRandomValues(new Uint8Array(16));
ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-CTR",
counter,
length: 64
},
key,
encoded
);
let buffer = new Uint8Array(ciphertext);
let k = await exportKey(key);
document.getElementById('cipher').value = "Key: " + k;
document.getElementById('cipher').value += "\nIV: " + buf2hex(iv);
document.getElementById('cipher').value += "\nCipher: " + buf2hex(buffer);
}
async function exportKey(k) {
const exported = await window.crypto.subtle.exportKey("jwk", k);
console.log("Exported Public Key: ", exported.k);
return exported.k;
}
async function decryptMessage(key) {
let decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter,
length: 64
},
key,
ciphertext
);
let dec = await new TextDecoder();
document.getElementById('decipher').value = await dec.decode(decrypted);
}
window.crypto.subtle.generateKey(
{
name: "AES-CTR",
length: 256,
},
true,
["encrypt", "decrypt"]
).then((key) => {
exportKey(key).then((k) => {
document.getElementById('cipher').value = "Key: " + k;
});
const encryptButton = document.getElementById('button-encrypt');
encryptButton.addEventListener("click", () => {
encryptMessage(key);
});
const decryptButton = document.getElementById('button-decrypt');
decryptButton.addEventListener("click", () => {
decryptMessage(key);
});
});
})();
</script>
<div class="indented">
<table width="100%">
<tr>
<th width="15%">Method</th>
<td style="text-align:left">
<p>
<input id="genkey" class="btn btn-large btn-primary" onclick="location.reload(true)" type="button" value="Gen AES CTR Key" />
<input id="button-encrypt" class="btn btn-large btn-danger" type="button" value="Encrypt" />
<input id="button-decrypt" class="btn btn-large btn-success" type="button" value="Decrypt" />
</p>
</td>
</tr>
</table>
<h2>Signature</h2>
<table width="100%">
<tr>
<th width="15%">Message)</th>
<td>
<input type="text" id="message" size="40">
</td>
</tr>
<tr>
<th width="15%">Cipher</th>
<td>
<input type="text" id="cipher" size="40">
</td>
</tr>
<tr>
<th>Decipher</th>
<td>
<input type="text" id="decipher" size="40">
</td>
</tr>
</table>
</div>
<script>
document.getElementById('message').value = "Hello 123";
</script>
The JavaScript coding used is [here]:
function buf2hex(buffer) { // buffer is an ArrayBuffer
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
(() => {
let ciphertext;
let counter;
function getMessage() {
let message = document.getElementById('message').value;
let enc = new TextEncoder();
return enc.encode(message);
}
async function encryptMessage(key) {
let encoded = getMessage();
counter = window.crypto.getRandomValues(new Uint8Array(16));
ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-CTR",
counter,
length: 64
},
key,
encoded
);
let buffer = new Uint8Array(ciphertext);
let k = await exportKey(key);
document.getElementById('cipher').value = "Key: " + k;
document.getElementById('cipher').value += "\nCounter: " + buf2hex(counter);
document.getElementById('cipher').value += "\nCipher: " + buf2hex(buffer);
}
async function exportKey(k) {
const exported = await window.crypto.subtle.exportKey("jwk", k);
console.log("Exported Public Key: ", exported.k);
return exported.k;
}
async function decryptMessage(key) {
let decrypted = await window.crypto.subtle.decrypt(
{
name: "AES-CTR",
counter,
length: 64
},
key,
ciphertext
);
let dec = await new TextDecoder();
document.getElementById('decipher').value = await dec.decode(decrypted);
}
window.crypto.subtle.generateKey(
{
name: "AES-CTR",
length: 256,
},
true,
["encrypt", "decrypt"]
).then((key) => {
exportKey(key).then((k) => {
document.getElementById('cipher').value = "Key: " + k;
});
const encryptButton = document.getElementById('button-encrypt');
encryptButton.addEventListener("click", () => {
encryptMessage(key);
});
const decryptButton = document.getElementById('button-decrypt');
decryptButton.addEventListener("click", () => {
decryptMessage(key);
});
});
})();
In this case, we compute a random 128-bit counter value (16 bytes) for the start of the counter:
counter = window.crypto.getRandomValues(new Uint8Array(16));
It is highly unlikely that the same counter value will return in any of our communications. A sample run is:
Plaintext: Hello 123
Key: ZHwoxHOjCWvWph_-47NrATmhXCVuAemME4yp9SOpKAo
Counter: 79d3a83371802f282caf61cd8bc34787
Cipher: 177f5a95969edb3f09
Conclusions
With AES, there are really only three main modes that you should use. If you want a block cipher, then select CBC mode. For a stream cipher, select GCM or CTR.
With a 128-bit counter value, it is highly unlikely that the same counter value will repeat within a long time periond. But, in WEP communications in wi-fi, the counter value was only 24 bits long, and which meant that the same counter value would repeat after a day or so. This meant that WEP was easily cracked.
References
[1] Diffie, W., & Hellman, M. E. (1979). Privacy and authentication: An introduction to cryptography. Proceedings of the IEEE, 67(3), 397–427.