- always check HMAC before decryption

- separating AES and CMAC key during SIV mode
This commit is contained in:
Sebastian Stenzel
2015-02-20 19:47:45 +01:00
parent 5569ecbfc7
commit b68cf71494
16 changed files with 107 additions and 157 deletions
@@ -293,7 +293,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
final List<String> encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
for (final String cleartext : cleartextPathComps) {
final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, ioSupport);
final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, hMacMasterKey, ioSupport);
encryptedPathComps.add(encrypted);
}
return StringUtils.join(encryptedPathComps, encryptedPathSep);
@@ -317,11 +317,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* These alternative names consist of the checksum, a unique id and a special file extension defined in
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException {
private String encryptPathComponent(final String cleartext, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException {
final byte[] cleartextBytes = cleartext.getBytes(StandardCharsets.UTF_8);
// encrypt:
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(key.getEncoded(), cleartextBytes);
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(aesKey, macKey, cleartextBytes);
final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
@@ -342,7 +342,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
final List<String> cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
for (final String encrypted : encryptedPathComps) {
final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, ioSupport);
final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, hMacMasterKey, ioSupport);
cleartextPathComps.add(new String(cleartext));
}
return StringUtils.join(cleartextPathComps, cleartextPathSep);
@@ -354,7 +354,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
/**
* @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
*/
private String decryptPathComponent(final String encrypted, final SecretKey key, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException, DecryptFailedException {
private String decryptPathComponent(final String encrypted, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException, DecryptFailedException {
final String ciphertext;
if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
@@ -371,7 +371,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// decrypt:
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(key.getEncoded(), encryptedBytes);
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(aesKey, macKey, encryptedBytes);
return new String(cleartextBytes, StandardCharsets.UTF_8);
}
@@ -389,8 +389,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
}
@Override
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
private void authenticateContent(SeekableByteChannel encryptedFile) throws IOException, DecryptFailedException {
// init mac:
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
@@ -411,7 +410,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
return MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
if (!macMatches) {
throw new DecryptFailedException("MAC authentication failed.");
}
}
@Override
@@ -440,7 +443,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
// read iv:
encryptedFile.position(0);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
@@ -454,6 +457,9 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
throw new IOException("Failed to read file header.");
}
// check MAC:
this.authenticateContent(encryptedFile);
// go to begin of content:
encryptedFile.position(64);
@@ -467,7 +473,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
// read iv:
encryptedFile.position(0);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
@@ -478,6 +484,9 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
throw new IOException("Failed to read file header.");
}
// check MAC:
this.authenticateContent(encryptedFile);
// seek relevant position and update iv:
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
@@ -13,6 +13,8 @@ import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.apache.commons.lang3.ArrayUtils;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
@@ -31,15 +33,26 @@ final class AesSivCipherUtil {
private static final byte[] BYTES_ZERO = new byte[16];
private static final byte DOUBLING_CONST = (byte) 0x87;
static byte[] sivEncrypt(byte[] key, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
if (key.length != 32 && key.length != 48 && key.length != 64) {
throw new InvalidKeyException("Invalid key length " + key.length);
static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
final byte[] aesKeyBytes = aesKey.getEncoded();
final byte[] macKeyBytes = macKey.getEncoded();
if (aesKeyBytes == null || macKeyBytes == null) {
throw new IllegalArgumentException("Can't get bytes of given key.");
}
try {
return sivEncrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
} finally {
Arrays.fill(aesKeyBytes, (byte) 0);
Arrays.fill(macKeyBytes, (byte) 0);
}
}
static byte[] sivEncrypt(byte[] aesKey, byte[] macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
}
final byte[] k1 = Arrays.copyOf(key, key.length / 2);
final byte[] k2 = Arrays.copyOfRange(key, k1.length, key.length);
final byte[] iv = s2v(k1, plaintext, additionalData);
final byte[] iv = s2v(macKey, plaintext, additionalData);
final int numBlocks = (plaintext.length + 15) / 16;
@@ -52,7 +65,7 @@ final class AesSivCipherUtil {
final byte[] x = new byte[numBlocks * 16];
final BlockCipher aes = new AESFastEngine();
aes.init(true, new KeyParameter(k2));
aes.init(true, new KeyParameter(aesKey));
for (int i = 0; i < numBlocks; i++) {
final long ctrVal = initialCtrVal + i;
ctrBuf.putLong(8, ctrVal);
@@ -65,13 +78,24 @@ final class AesSivCipherUtil {
return ArrayUtils.addAll(iv, ciphertext);
}
static byte[] sivDecrypt(byte[] key, byte[] ciphertext, byte[]... additionalData) throws DecryptFailedException, InvalidKeyException {
if (key.length != 32 && key.length != 48 && key.length != 64) {
throw new InvalidKeyException("Invalid key length " + key.length);
static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException, DecryptFailedException {
final byte[] aesKeyBytes = aesKey.getEncoded();
final byte[] macKeyBytes = macKey.getEncoded();
if (aesKeyBytes == null || macKeyBytes == null) {
throw new IllegalArgumentException("Can't get bytes of given key.");
}
try {
return sivDecrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
} finally {
Arrays.fill(aesKeyBytes, (byte) 0);
Arrays.fill(macKeyBytes, (byte) 0);
}
}
final byte[] k1 = Arrays.copyOf(key, key.length / 2);
final byte[] k2 = Arrays.copyOfRange(key, k1.length, key.length);
static byte[] sivDecrypt(byte[] aesKey, byte[] macKey, byte[] ciphertext, byte[]... additionalData) throws DecryptFailedException, InvalidKeyException {
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
}
final byte[] iv = Arrays.copyOf(ciphertext, 16);
@@ -87,7 +111,7 @@ final class AesSivCipherUtil {
final byte[] x = new byte[numBlocks * 16];
final BlockCipher aes = new AESFastEngine();
aes.init(true, new KeyParameter(k2));
aes.init(true, new KeyParameter(aesKey));
for (int i = 0; i < numBlocks; i++) {
final long ctrVal = initialCtrVal + i;
ctrBuf.putLong(8, ctrVal);
@@ -97,7 +121,7 @@ final class AesSivCipherUtil {
final byte[] plaintext = xor(actualCiphertext, x);
final byte[] control = s2v(k1, plaintext, additionalData);
final byte[] control = s2v(macKey, plaintext, additionalData);
if (MessageDigest.isEqual(control, iv)) {
return plaintext;
@@ -106,8 +130,8 @@ final class AesSivCipherUtil {
}
}
static byte[] s2v(byte[] key, byte[] plaintext, byte[]... additionalData) {
final CipherParameters params = new KeyParameter(key);
static byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... additionalData) {
final CipherParameters params = new KeyParameter(macKey);
final BlockCipher aes = new AESFastEngine();
final CMac mac = new CMac(aes);
mac.init(params);
@@ -14,26 +14,12 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
@@ -86,8 +72,8 @@ public class Aes256CryptorTest {
}
}
@Test
public void testIntegrityAuthentication() throws IOException {
@Test(expected = DecryptFailedException.class)
public void testIntegrityAuthentication() throws IOException, DecryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
@@ -104,11 +90,6 @@ public class Aes256CryptorTest {
encryptedData.position(0);
// authenticate unmodified content:
final SeekableByteChannel encryptedIn1 = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean isContentUnmodified1 = cryptor.authenticateContent(encryptedIn1);
Assert.assertTrue(isContentUnmodified1);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
@@ -117,30 +98,10 @@ public class Aes256CryptorTest {
encryptedData.position(0);
// authenticate modified content:
final SeekableByteChannel encryptedIn2 = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean isContentUnmodified2 = cryptor.authenticateContent(encryptedIn2);
Assert.assertFalse(isContentUnmodified2);
}
@Test
public void foo() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, NoSuchProviderException {
Security.addProvider(new BouncyCastleProvider());
final byte[] iv = new byte[16];
final byte[] keyBytes = new byte[16];
final SecretKey key = new SecretKeySpec(keyBytes, "AES");
final Cipher pkcs5PaddedCipher = Cipher.getInstance("AES/CTR/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME);
pkcs5PaddedCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
final Cipher unpaddedCipher = Cipher.getInstance("AES/CTR/NoPadding");
unpaddedCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
// test data:
final byte[] plaintextData = "Hello World".getBytes();
final byte[] pkcs5PaddedCiphertext = pkcs5PaddedCipher.doFinal(plaintextData);
final byte[] unpaddedCiphertext = unpaddedCipher.doFinal(plaintextData);
Assert.assertFalse(Arrays.equals(pkcs5PaddedCiphertext, unpaddedCiphertext));
// decrypt modified content (should fail with DecryptFailedException):
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
cryptor.decryptedFile(encryptedIn, plaintextOut);
}
@Test
@@ -14,7 +14,7 @@ public class AesSivCipherUtilTest {
@Test
public void testS2v() throws DecoderException {
final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
@@ -36,17 +36,18 @@ public class AesSivCipherUtilTest {
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93};
final byte[] result = AesSivCipherUtil.s2v(key, plaintext, ad);
final byte[] result = AesSivCipherUtil.s2v(macKey, plaintext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testSivEncrypt() throws InvalidKeyException {
final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0, //
(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
@@ -72,17 +73,18 @@ public class AesSivCipherUtilTest {
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] result = AesSivCipherUtil.sivEncrypt(key, plaintext, ad);
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testSivDecrypt() throws DecryptFailedException, InvalidKeyException {
final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0, //
(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
@@ -108,17 +110,18 @@ public class AesSivCipherUtilTest {
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] result = AesSivCipherUtil.sivDecrypt(key, ciphertext, ad);
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test(expected = DecryptFailedException.class)
public void testSivDecryptWithInvalidKey() throws DecryptFailedException, InvalidKeyException {
final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0, //
(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0x00};
@@ -144,7 +147,7 @@ public class AesSivCipherUtilTest {
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] result = AesSivCipherUtil.sivDecrypt(key, ciphertext, ad);
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
Assert.assertArrayEquals(expected, result);
}
@@ -153,12 +156,12 @@ public class AesSivCipherUtilTest {
*/
@Test
public void testNonceBasedAuthenticatedEncryption() throws InvalidKeyException {
final byte[] key = {(byte) 0x7f, (byte) 0x7e, (byte) 0x7d, (byte) 0x7c, //
final byte[] macKey = {(byte) 0x7f, (byte) 0x7e, (byte) 0x7d, (byte) 0x7c, //
(byte) 0x7b, (byte) 0x7a, (byte) 0x79, (byte) 0x78, //
(byte) 0x77, (byte) 0x76, (byte) 0x75, (byte) 0x74, //
(byte) 0x73, (byte) 0x72, (byte) 0x71, (byte) 0x70, //
(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, //
(byte) 0x73, (byte) 0x72, (byte) 0x71, (byte) 0x70};
final byte[] aesKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, //
(byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, //
(byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, //
(byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f};
@@ -196,7 +199,7 @@ public class AesSivCipherUtilTest {
(byte) 0x53, (byte) 0x49, (byte) 0x56, (byte) 0x2d, //
(byte) 0x41, (byte) 0x45, (byte) 0x53};
final byte[] result = AesSivCipherUtil.sivEncrypt(key, plaintext, ad1, ad2, nonce);
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad1, ad2, nonce);
final byte[] expected = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, //
(byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, //