Created
December 17, 2021 16:39
-
-
Save alipha/2cd77bfd1a7a46f38341617019c7b652 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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