Skip to content

Instantly share code, notes, and snippets.

@alipha
Created December 17, 2021 16:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alipha/2cd77bfd1a7a46f38341617019c7b652 to your computer and use it in GitHub Desktop.
Save alipha/2cd77bfd1a7a46f38341617019c7b652 to your computer and use it in GitHub Desktop.
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/* Creates an authenticated symmetric cipher using only AES-CBC (no hash or one-time authenticator algorithm is used).
Concerns:
- The MAC generation requires a keystream to be generated which is over 512 times longer than the plaintext to encrypt.
- The MAC generation, as written, is not time-constant nor has consistent memory-access patterns.
- It is the responsibility of the user of this class to prevent out-of-order attacks or replay attacks.
- A single AuthCipher object should not be used by multiple threads.
- Keys should be rotated well prior to 2^64 blocks of data being encrypted.
*/
public class AuthCipher {
private static final int MAC_SIZE = 32;
private static final int IV_POS = MAC_SIZE;
private static final int IV_SIZE = 16;
private static final int CIPHERTEXT_POS = IV_POS + IV_SIZE;
private final Cipher cipher;
private final SecretKey key;
private final SecretKey macKey;
private final SecureRandom random;
public AuthCipher(byte[] keyBytes) throws NoSuchAlgorithmException, NoSuchPaddingException {
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
this.key = new SecretKeySpec(keyBytes, "AES");
// make the MAC key be different from the encryption key
for(int i = 0; i < keyBytes.length; ++i)
keyBytes[i] ^= 0x35;
this.macKey = new SecretKeySpec(keyBytes, "AES");
this.random = new SecureRandom();
}
/* The result of encrypt will be a message in the format of:
--------------------------------------------------------------------------
| MAC (32 bytes) | IV (16 bytes) | Ciphertext ... |
--------------------------------------------------------------------------
The IV is randomly-generated.
*/
public byte[] encrypt(byte[] plaintext) throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, ShortBufferException {
byte[] iv = new byte[IV_SIZE];
random.nextBytes(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
int ciphertextSize = cipher.getOutputSize(plaintext.length);
byte[] ciphertext = new byte[CIPHERTEXT_POS + ciphertextSize];
cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, CIPHERTEXT_POS);
System.arraycopy(iv, 0, ciphertext, IV_POS, IV_SIZE);
computeMac(ciphertext, ciphertext);
return ciphertext;
}
/* decrypt expects a message in the same format that encrypt() produces */
public byte[] decrypt(byte[] ciphertext) throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, ShortBufferException {
if(ciphertext.length < CIPHERTEXT_POS)
throw new ShortBufferException("ciphertext array is shorter than any valid ciphertext (" + ciphertext.length + " < " + CIPHERTEXT_POS + ")");
byte[] providedMac = new byte[MAC_SIZE];
System.arraycopy(ciphertext, 0, providedMac, 0, MAC_SIZE);
byte[] computedMac = new byte[MAC_SIZE];
computeMac(ciphertext, computedMac);
if(!MessageDigest.isEqual(computedMac, providedMac))
throw new AEADBadTagException();
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ciphertext, IV_POS, IV_SIZE));
int plaintextSize = cipher.getOutputSize(ciphertext.length - CIPHERTEXT_POS);
byte[] plaintext = new byte[plaintextSize];
cipher.doFinal(ciphertext, CIPHERTEXT_POS, ciphertext.length - CIPHERTEXT_POS, plaintext, 0);
return plaintext;
}
/* For each BIT of the IV + ciphertext, computeMac will select between two different 32-byte chunks, depending upon
whether the BIT is 0 or 1. Each selected 32-byte chunk is xor'd together to produce the MAC.
These 32-byte chunks are deterministically created from a AES-CBC keystream using the macKey.
Altering any bit of the IV or ciphertext will cause a different 32-byte chunk to be selected corresponding to that
bit, which will completely alter the resulting MAC.
*/
private void computeMac(byte[] ciphertext, byte[] out) throws InvalidKeyException, InvalidAlgorithmParameterException {
cipher.init(Cipher.ENCRYPT_MODE, macKey, new IvParameterSpec(ciphertext, IV_POS, IV_SIZE));
byte[] zeros = new byte[2 * MAC_SIZE * 8]; // we need 2 different 32-byte chunks for each of the 8 bits in a byte of the ciphertext
for(int byteIndex = IV_POS; byteIndex < ciphertext.length; ++byteIndex) {
// generate enough 32-byte chunks for processing one byte of ciphertext.
// Note this means 512 bytes of keystream is generated for every single byte of ciphertext.
byte[] keyStream = cipher.update(zeros);
for(int bitIndex = 0; bitIndex < 8; ++bitIndex) {
boolean bitSet = (ciphertext[byteIndex] & 0xff & (1 << bitIndex)) != 0;
xorBytes(out, keyStream, bitIndex + (bitSet ? 0 : 8));
}
}
}
private void xorBytes(byte[] mac, byte[] keyStream, int chunkIndex) {
int chunkPos = chunkIndex * MAC_SIZE;
for(int i = 0; i < MAC_SIZE; ++i)
mac[i] ^= keyStream[chunkPos + i];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment