Program
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AesCtr256 {
// sBox is pre-computed multiplicative inverse in GF(2^8) used in subBytes and keyExpansion [§5.1.1]
private static final int[] sBox = new int[] { 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e,
0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 };
// rCon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [§5.2]
private static final int[][] rCon = new int[][] { new int[] { 0x00, 0x00, 0x00, 0x00 }, // 0
new int[] { 0x01, 0x00, 0x00, 0x00 }, // 1
new int[] { 0x02, 0x00, 0x00, 0x00 }, // 2
new int[] { 0x04, 0x00, 0x00, 0x00 }, // 3
new int[] { 0x08, 0x00, 0x00, 0x00 }, // 4
new int[] { 0x10, 0x00, 0x00, 0x00 }, // 5
new int[] { 0x20, 0x00, 0x00, 0x00 }, // 6
new int[] { 0x40, 0x00, 0x00, 0x00 }, // 7
new int[] { 0x80, 0x00, 0x00, 0x00 }, // 8
new int[] { 0x1b, 0x00, 0x00, 0x00 }, // 9
new int[] { 0x36, 0x00, 0x00, 0x00 }// 10
};
private static final String UTF8 = "UTF-8";
public static void main(String[] args) throws Exception {
// Security.setProperty("crypto.policy", "unlimited");
final int maxKeySize = javax.crypto.Cipher.getMaxAllowedKeyLength("AES");
System.out.println("Max Key Size for AES : " + maxKeySize);
final String data = "My Encrypted String";
final String key = "3e6cf9b346544bbb46ec99e33a9e0bcc";
final String encrypted = encrypt(data, key, 256);
System.out.println("Encrypted string=" + encrypted);
System.out.println();
final String decrypted = decrypt(encrypted, key);
System.out.println("Decrypted string=" + decrypted);
}
/**
* AES Cipher function: encrypt 'input' state with Rijndael algorithm [§5.1]; applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage.
*
* @param {number[]} input - 16-byte (128-bit) input state array.
* @param {number[][]} w - Key schedule as 2D byte-array (Nr+1 x Nb bytes).
* @returns {number[]} Encrypted output state array.
*/
private static int[] cipher(int[] input, int[][] w) {
final int Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
final int Nr = w.length / Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys
int[][] state = new int[4][];// [[],[],[],[]]; // initialise 4xNb byte-array 'state' with input [§3.4]
for (int i = 0; i < 4 * Nb; i++) {
if (state[i % 4] == null) {
state[i % 4] = new int[4];
}
state[i % 4][(int) Math.floor((double) i / 4)] = input[i];
}
state = addRoundKey(state, w, 0, Nb);
for (int round = 1; round < Nr; round++) {
state = subBytes(state, Nb);
state = shiftRows(state, Nb);
state = mixColumns(state, Nb);
state = addRoundKey(state, w, round, Nb);
}
state = subBytes(state, Nb);
state = shiftRows(state, Nb);
state = addRoundKey(state, w, Nr, Nb);
final int[] output = new int[4 * Nb]; // convert state to 1-d array before returning [§3.4]
for (int i = 0; i < 4 * Nb; i++) {
output[i] = state[i % 4][(int) Math.floor((double) i / 4)];
}
return output;
}
/**
* Perform key expansion to generate a key schedule from a cipher key [§5.2].
*
* @param {number[]} key - Cipher key as 16/24/32-byte array.
* @returns {number[][]} Expanded key schedule as 2D byte-array (Nr+1 x Nb bytes).
*/
private static int[][] keyExpansion(int[] key) {
final int Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
final int Nk = key.length / 4; // key length (in words): 4/6/8 for 128/192/256-bit keys
final int Nr = Nk + 6; // no of rounds: 10/12/14 for 128/192/256-bit keys
final int[][] w = new int[Nb * (Nr + 1)][Nb];
int[] temp = new int[4];
// initialise first Nk words of expanded key with cipher key
for (int i = 0; i < Nk; i++) {
final int[] r = new int[] { key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3] };
w[i] = r;
}
// expand the key into the remainder of the schedule
for (int i = Nk; i < (Nb * (Nr + 1)); i++) {
w[i] = new int[4];
for (int t = 0; t < 4; t++) {
temp[t] = w[i - 1][t];
}
// each Nk'th word has extra transformation
if (i % Nk == 0) {
temp = subWord(rotWord(temp));
for (int t = 0; t < 4; t++) {
temp[t] ^= rCon[i / Nk][t];
}
}
// 256-bit key has subWord applied every 4th word
else if (Nk > 6 && i % Nk == 4) {
temp = subWord(temp);
}
// xor w[i] with w[i-1] and w[i-Nk]
for (int t = 0; t < 4; t++) {
w[i][t] = (w[i - Nk][t] ^ temp[t]);
}
}
return w;
}
/**
* Apply SBox to state S [§5.1.1]
*
* @private
*/
private static int[][] subBytes(int[][] s, int Nb) {
for (int r = 0; r < 4; r++) {
for (int c = 0; c < Nb; c++) {
s[r][c] = sBox[s[r][c]];
}
}
return s;
}
/**
* Shift row r of state S left by r bytes [§5.1.2]
*
* @private
*/
private static int[][] shiftRows(int[][] s, int Nb) {
final int[] t = new int[4];
for (int r = 1; r < 4; r++) {
for (int c = 0; c < 4; c++) {
t[c] = s[r][(c + r) % Nb]; // shift into temp copy
}
// and copy back
System.arraycopy(t, 0, s[r], 0, 4);
} // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES):
return s; // see asmaes.sourceforge.net/rijndael/rijndaelImplementation.pdf
}
/**
* Combine bytes of each col of state S [§5.1.3]
*
* @private
*/
private static int[][] mixColumns(int[][] s, int Nb) {
for (int c = 0; c < 4; c++) {
final int[] a = new int[4]; // 'a' is a copy of the current column from 's'
final int[] b = new int[4]; // 'b' is a•{02} in GF(2^8)
for (int i = 0; i < 4; i++) {
a[i] = s[i][c];
b[i] = (s[i][c] & 0x80) > 0 ? (s[i][c] << 1 ^ 0x011b) : (s[i][c] << 1);
}
// a[n] ^ b[n] is a•{03} in GF(2^8)
s[0][c] = (b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3]); // {02}•a0 + {03}•a1 + a2 + a3
s[1][c] = (a[0] ^ b[1] ^ a[2] ^ b[2] ^ a[3]); // a0 • {02}•a1 + {03}•a2 + a3
s[2][c] = (a[0] ^ a[1] ^ b[2] ^ a[3] ^ b[3]); // a0 + a1 + {02}•a2 + {03}•a3
s[3][c] = (a[0] ^ b[0] ^ a[1] ^ a[2] ^ b[3]); // {03}•a0 + a1 + a2 + {02}•a3
}
return s;
}
/**
* Xor Round Key into state S [§5.1.4]
*
* @private
*/
private static int[][] addRoundKey(int[][] state, int[][] w, int rnd, int Nb) {
for (int r = 0; r < 4; r++) {
for (int c = 0; c < Nb; c++) {
state[r][c] ^= w[rnd * 4 + c][r];
}
}
return state;
}
/**
* Apply SBox to 4-byte word w
*
* @private
*/
private static int[] subWord(int[] w) {
for (int i = 0; i < 4; i++) {
w[i] = sBox[w[i]];
}
return w;
}
/**
* Rotate 4-byte word w left by one byte
*
* @private
*/
private static int[] rotWord(int[] w) {
final int tmp = w[0];
System.arraycopy(w, 1, w, 0, 3);
w[3] = tmp;
return w;
}
public static String encrypt(String plaintext, String password, int nBits) throws Exception {
final int blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
if (!(nBits == 128 || nBits == 192 || nBits == 256)) {
return ""; // standard allows 128/192/256 bit keys
}
// note PHP (5) gives us plaintext and password in UTF8 encoding!
password = UTF8Encode(password);
plaintext = UTF8Encode(plaintext);
final int nBytes = nBits / 8; // no bytes in key
final int[] pwBytes = new int[nBytes];
for (int i = 0; i < nBytes; i++) {
pwBytes[i] = Float.isNaN(password.charAt(i)) ? 0 : password.charAt(i);
}
int[] key = cipher(pwBytes, keyExpansion(pwBytes));
// expand key to 16/24/32 bytes long
final int bytesExpand = nBytes - 16;
if (bytesExpand > 0) {
final int keyOriginalLength = key.length;
final int[] expandKey = new int[bytesExpand];
final int[] endKey = new int[keyOriginalLength + bytesExpand];
System.arraycopy(key, 0, expandKey, 0, bytesExpand);// initial expandKey
System.arraycopy(key, 0, endKey, 0, key.length);// copy all from key to endKey
System.arraycopy(expandKey, 0, endKey, key.length, expandKey.length);
key = endKey;
}
final int[] counterBlock = new int[16];
final long nonce = 1543699; // System.currentTimeMillis();
final long nonceMs = nonce % 1000;
final long nonceSec = (long) Math.floor(nonce / 1000);
final long nonceRnd = 27096; // (long)Math.floor(Math.random() * 0xffff);
for (int i = 0; i < 2; i++) {
counterBlock[i] = (int) ((nonceMs >>> i * 8) & 0xff);
}
for (int i = 0; i < 2; i++) {
counterBlock[i + 2] = (int) ((nonceRnd >>> i * 8) & 0xff);
}
for (int i = 0; i < 4; i++) {
counterBlock[i + 4] = (int) ((nonceSec >>> i * 8) & 0xff);
}
final StringBuilder ctrTxt = new StringBuilder();
for (int i = 0; i < 8; i++) {
ctrTxt.append((char) counterBlock[i]);
}
// generate key schedule
final int[][] keySchedule = keyExpansion(key);
// separate ciphertext into blocks (skipping past initial 8 bytes)
final int nBlocks = (int) Math.ceil((plaintext.length()) / (float) blockSize);
final String[] ciphertxt = new String[nBlocks];
final byte[][] bytearray = new byte[nBlocks + 1][];
bytearray[0] = ctrTxt.toString().getBytes();
for (int b = 0; b < nBlocks; b++) {
// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
// done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB)
for (int c = 0; c < 4; c++) {
counterBlock[15 - c] = (b >>> c * 8) & 0xff;
}
for (int c = 0; c < 4; c++) {
counterBlock[15 - c - 4] = b / 0x10000000 >>> c * 8; /* 0x100000000 */
}
final int[] cipherCntr = cipher(counterBlock, keySchedule);
// block size is reduced on final block
final int blockLength = (b < nBlocks - 1) ? blockSize : (plaintext.length() - 1) % blockSize + 1;
final byte[] cipherByte = new byte[blockLength];
for (int i = 0; i < blockLength; i++) { // -- xor plaintext with ciphered counter byte-by-byte --
cipherByte[i] = (byte) (cipherCntr[i] ^ ord(substr(plaintext, b * blockSize + i, 1)));
}
ciphertxt[b] = new String(cipherByte);
bytearray[b + 1] = cipherByte;
}
int bigArraySize = 0;
for (final byte[] bytes : bytearray) {
bigArraySize += bytes.length;
}
final byte[] bigArray = new byte[bigArraySize];
copySmallArraysToBigArray(bytearray, bigArray);
return Base64.getEncoder().withoutPadding().encodeToString(bigArray);
// return new String(bigArray, StandardCharsets.UTF_8);
}
private static void copySmallArraysToBigArray(byte[][] smallArrays, byte[] bigArray) {
int currentOffset = 0;
for (final byte[] currentArray : smallArrays) {
System.arraycopy(currentArray, 0, bigArray, currentOffset, currentArray.length);
currentOffset += currentArray.length;
}
}
public static int ord(String s) {
// return s.length() > 0 ? (s.getBytes(StandardCharsets.UTF_8)[0] & 0xff) : 0;
return s.length() > 0 ? (s.getBytes()[0] & 0xff) : 0;
}
public static int ord(char c) {
return c < 0x80 ? c : ord(Character.toString(c));
}
private static String substr(String string, int from, int to) {
final String substring = string.substring(string.length() - Math.abs(from));
if (from < 0 && to < 0) {
if (Math.abs(from) > Math.abs(to)) {
return substring.substring(substring.length() - Math.abs(to));
}
else {
return "";
}
}
else if (from >= 0 && to < 0) {
final String s = string.substring(from);
if (Math.abs(to) >= s.length()) {
return "";
}
else {
return s.substring(0, s.length() - Math.abs(to));
}
}
else if (from < 0) {
if (to >= substring.length()) {
return substring;
}
return substring.substring(0, to);
}
else {
final String s = string.substring(Math.abs(from));
if (to >= s.length()) {
return s;
}
else {
return s.substring(0, Math.abs(to));
}
}
}
/**
* Decrypt a text encrypted by AES in counter mode of operation
*
* @param {string} ciphertext - Source text to be encrypted.
* @param {string} password - Password to use to generate a key.
* @param {number} nBits - Number of bits to be used in the key; 128 / 192 / 256.
* @returns {string} Decrypted text
* @example
*/
private static String decrypt(String ciphertext, String password) throws Exception {
String plaintext = "";
// try {
final int blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES
ciphertext = base64Decoder(ciphertext);
password = UTF8Encode(password);
// use AES to encrypt password (mirroring encrypt routine)
final int nBytes = 256 / 8; // no bytes in key
final int[] pwBytes = new int[nBytes];
for (int i = 0; i < nBytes; i++) {
pwBytes[i] = Float.isNaN(password.charAt(i)) ? 0 : password.charAt(i);
}
int[] key = cipher(pwBytes, keyExpansion(pwBytes));
// expand key to 16/24/32 bytes long
final int bytesExpand = nBytes - 16;
final int keyOriginalLength = key.length;
final int[] expandKey = new int[bytesExpand];
final int[] endKey = new int[keyOriginalLength + bytesExpand];
System.arraycopy(key, 0, expandKey, 0, bytesExpand);// initial expandKey
System.arraycopy(key, 0, endKey, 0, key.length);// copy all from key to endKey
System.arraycopy(expandKey, 0, endKey, key.length, expandKey.length);
key = endKey;
// recover nonce from 1st 8 bytes of ciphertext
final int[] counterBlock = new int[16];
final String ctrTxt = ciphertext.substring(0, 8);
for (int i = 0; i < 8; i++) {
counterBlock[i] = ctrTxt.charAt(i);
}
// generate key schedule
final int[][] keySchedule = keyExpansion(key);
// separate ciphertext into blocks (skipping past initial 8 bytes)
final int nBlocks = (int) Math.ceil((ciphertext.length() - 8) / (float) blockSize);
final String[] cipherArr = new String[nBlocks];
for (int b = 0; b < nBlocks; b++) {
final int start = 8 + b * blockSize;
final int end = 8 + b * blockSize + blockSize;
if (end >= ciphertext.length()) {
cipherArr[b] = UTF8Encode(ciphertext.substring(start));
}
else {
cipherArr[b] = UTF8Encode(ciphertext.substring(start, end));
}
}
// ciphertext is now array of block-length strings, ³F.àiþ±wãì¿,ß°d
// plaintext will get generated block-by-block into "³F.àiþ±wãì¿,ß°" array of block-length strings
final String[] plaintxt = new String[cipherArr.length];
// Expand CounterBlock
for (int b = 0; b < nBlocks; b++) {
// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes)
for (int c = 0; c < 4; c++) {
counterBlock[15 - c] = (b >> c * 8) & 0xff;
}
for (int c = 0; c < 4; c++)
// counterBlock[15 - c - 4] = (b / 0x100000000 >>> c * 8);
{
counterBlock[15 - c - 4] = 0;
}
final int[] cipherCntr = cipher(counterBlock, keySchedule); // encrypt counter block
final char[] plaintxtByte = new char[cipherArr[b].length()];
for (int i = 0; i < cipherArr[b].length(); i++) {
plaintxtByte[i] = (char) (cipherCntr[i] ^ cipherArr[b].charAt(i));
}
plaintxt[b] = String.copyValueOf(plaintxtByte);
}
// join array of blocks into single plaintext string
plaintext = joinArray(plaintxt);// plaintxt.Join('');
// join array of blocks into single plaintext string
plaintext = UTF8Decode(plaintext);// decode from UTF8 back to Unicode multi-byte chars
return plaintext;
}
private static String joinArray(Object[] source) {
final StringBuilder dest = new StringBuilder();
for (final Object o : source) {
dest.append((String) o);
}
return dest.toString();
}
private static String UTF8Decode(String s) throws Exception {
final byte[] utf8Bytes = new byte[s.length()];
for (int i = 0; i < s.length(); ++i) {
// Debug.Assert( 0 <= utf8String[i] && utf8String[i] <= 255, "the char must be in byte's range");
utf8Bytes[i] = (byte) s.charAt(i);
}
return new String(utf8Bytes, StandardCharsets.UTF_8);
}
private static String base64Decoder(String data) throws Exception {
final byte[] b = Base64.getDecoder().decode(data.getBytes(StandardCharsets.ISO_8859_1));
// byte[] b = DatatypeConverter.parseBase64Binary(data);
return new String(b, StandardCharsets.ISO_8859_1);
}
private static String UTF8Encode(String s) throws Exception {
return new String(s.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
}
}
Expected Output (It runs correctly with cmd)
Actual output on IDEs (Intellij,Eclipse)
Max Key Size for AES : 2147483647
Encrypted string=wrsCw5hpBwYAAGNJ2u4oXMphxxzlyt+hHAm5tVc
Decrypted string=��\��ʶ����r?3�cd�^
JDK that I'm using
java version "11.0.5" 2019-10-15 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.5+10-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.5+10-LTS, mixed mode)
The place where the error is taking place is in the encrypt-method where you generate the initialization vector (necessary for AES CTR mode). The iv is placed at the beginning of the encrypted string/byte array in bytearray[0]. Just change the line from
bytearray[0] = ctrTxt.toString().getBytes();
to
bytearray[0] = ctrTxt.toString().getBytes(StandardCharsets.ISO_8859_1);
and your program is working within your IDE or on command line.
Check the encoding your files (Status Bar) and the project (Settings | Editor | File Encodings) have.
Try adding -Dfile.encoding=UTF-8
into Help | Edit Custom VM Options file and restart IDE.
Also check that the console has Font set (Settings (Preferences on macOS) | Editor | Color Scheme | Console Font) which is capable to display all the glyphs.
User contributions licensed under CC BY-SA 3.0