- Using RFC AES 3394 Key Wrap algorithm for storing master keys

- Storing HMac key and encryption key separately
- Thanks to key wrap, simplified keyfile (no more IV needed)
This commit is contained in:
Sebastian Stenzel
2015-01-04 16:32:50 +01:00
parent e90e001718
commit 3f2ef3a83a
3 changed files with 105 additions and 127 deletions

View File

@@ -11,7 +11,6 @@ package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SeekableByteChannel;
@@ -30,7 +29,6 @@ import java.util.Random;
import java.util.UUID;
import java.util.zip.CRC32;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
@@ -40,10 +38,11 @@ import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
@@ -81,7 +80,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction
* Policy Files isn't installed. Those files can be downloaded here: http://www.oracle.com/technetwork/java/javase/downloads/.
*/
private static final int AES_KEY_LENGTH;
private static final int AES_KEY_LENGTH_IN_BITS;
/**
* Jackson JSON-Mapper.
@@ -89,15 +88,15 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* The decrypted master key. Its lifecycle starts with {@link #randomData(int)} or {@link #encryptMasterKey(Path, CharSequence)}. Its
* lifecycle ends with {@link #swipeSensitiveData()}.
* The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or
* {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with {@link #swipeSensitiveData()}.
*/
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
private SecretKey primaryMasterKey;
/**
* If certain cryptographic operations need a second key, which is distinct to the masterKey
* Decrypted secondary key used for hmac operations.
*/
private final byte[] secondaryKey = new byte[MASTER_KEY_LENGTH];
private SecretKey hMacMasterKey;
private static final int SIZE_OF_LONG = Long.BYTES;
@@ -105,8 +104,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
try {
PBKDF2_FACTORY = SecretKeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
SECURE_PRNG = SecureRandom.getInstance(PRNG_ALGORITHM);
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(CRYPTO_ALGORITHM);
AES_KEY_LENGTH = (maxKeyLen >= 256) ? 256 : maxKeyLen;
final int maxKeyLength = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
AES_KEY_LENGTH_IN_BITS = (maxKeyLength >= MAX_MASTER_KEY_LENGTH_IN_BITS) ? MAX_MASTER_KEY_LENGTH_IN_BITS : maxKeyLength;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Algorithm should exist.", e);
}
@@ -117,8 +116,16 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/
public Aes256Cryptor() {
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
SECURE_PRNG.nextBytes(this.masterKey);
SECURE_PRNG.nextBytes(this.secondaryKey);
byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
try {
SECURE_PRNG.nextBytes(bytes);
this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
SECURE_PRNG.nextBytes(bytes);
this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
} finally {
Arrays.fill(bytes, (byte) 0);
}
}
/**
@@ -128,8 +135,16 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* @param prng Fast, possibly insecure PRNG.
*/
Aes256Cryptor(Random prng) {
prng.nextBytes(this.masterKey);
prng.nextBytes(this.secondaryKey);
byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
try {
prng.nextBytes(bytes);
this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
prng.nextBytes(bytes);
this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
} finally {
Arrays.fill(bytes, (byte) 0);
}
}
/**
@@ -137,31 +152,26 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/
@Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
final ByteBuffer combinedKey = ByteBuffer.allocate(this.masterKey.length + this.secondaryKey.length);
combinedKey.put(this.masterKey);
combinedKey.put(this.secondaryKey);
try {
// derive key:
final byte[] userSalt = randomData(SALT_LENGTH);
final SecretKey userKey = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH);
final SecretKey kek = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH_IN_BITS);
// encrypt:
final byte[] iv = randomData(AES_BLOCK_LENGTH);
final Cipher encCipher = aesGcmCipher(userKey, iv, Cipher.ENCRYPT_MODE);
byte[] encryptedCombinedKey = encCipher.doFinal(combinedKey.array());
final Cipher encCipher = aesKeyWrapCipher(kek, Cipher.WRAP_MODE);
byte[] wrappedPrimaryKey = encCipher.wrap(primaryMasterKey);
byte[] wrappedSecondaryKey = encCipher.wrap(hMacMasterKey);
// save encrypted masterkey:
final Key key = new Key();
final KeyFile key = new KeyFile();
key.setIterations(PBKDF2_PW_ITERATIONS);
key.setIv(iv);
key.setKeyLength(AES_KEY_LENGTH);
key.setMasterkey(encryptedCombinedKey);
key.setKeyLength(AES_KEY_LENGTH_IN_BITS);
key.setPrimaryMasterKey(wrappedPrimaryKey);
key.setHMacMasterKey(wrappedSecondaryKey);
key.setSalt(userSalt);
objectMapper.writeValue(out, key);
} catch (IllegalBlockSizeException | BadPaddingException ex) {
throw new IllegalStateException("Block size hard coded. Padding irrelevant in ENCRYPT_MODE. IV must exist in CTR mode.", ex);
} finally {
Arrays.fill(combinedKey.array(), (byte) 0);
} catch (InvalidKeyException | IllegalBlockSizeException ex) {
throw new IllegalStateException("Invalid hard coded configuration.", ex);
}
}
@@ -176,57 +186,56 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/
@Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
byte[] combinedKey = new byte[0];
try {
// load encrypted masterkey:
final Key key = objectMapper.readValue(in, Key.class);
final KeyFile key = objectMapper.readValue(in, KeyFile.class);
// check, whether the key length is supported:
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(CRYPTO_ALGORITHM);
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
if (key.getKeyLength() > maxKeyLen) {
throw new UnsupportedKeyLengthException(key.getKeyLength(), maxKeyLen);
}
// derive key:
final SecretKey userKey = pbkdf2(password, key.getSalt(), key.getIterations(), key.getKeyLength());
final SecretKey kek = pbkdf2(password, key.getSalt(), key.getIterations(), key.getKeyLength());
// decrypt and check password by catching AEAD exception
final Cipher decCipher = aesGcmCipher(userKey, key.getIv(), Cipher.DECRYPT_MODE);
combinedKey = decCipher.doFinal(key.getMasterkey());
final Cipher decCipher = aesKeyWrapCipher(kek, Cipher.UNWRAP_MODE);
SecretKey primary = (SecretKey) decCipher.unwrap(key.getPrimaryMasterKey(), AES_KEY_ALGORITHM, Cipher.SECRET_KEY);
SecretKey secondary = (SecretKey) decCipher.unwrap(key.getPrimaryMasterKey(), HMAC_KEY_ALGORITHM, Cipher.SECRET_KEY);
// everything ok, split decrypted data to masterkey and secondary key:
final ByteBuffer combinedKeyBuffer = ByteBuffer.wrap(combinedKey);
combinedKeyBuffer.get(this.masterKey);
combinedKeyBuffer.get(this.secondaryKey);
} catch (AEADBadTagException e) {
throw new WrongPasswordException();
} catch (IllegalBlockSizeException | BadPaddingException | BufferOverflowException ex) {
throw new DecryptFailedException(ex);
// everything ok, assign decrypted keys:
this.primaryMasterKey = primary;
this.hMacMasterKey = secondary;
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("Algorithm should exist.", ex);
} finally {
Arrays.fill(combinedKey, (byte) 0);
} catch (InvalidKeyException e) {
throw new WrongPasswordException();
}
}
/**
* Overwrites the {@link #masterKey} with zeros. As masterKey is a final field, this operation is ensured to work on its actual data.
* Otherwise developers could accidentally just assign a new object to the variable.
*/
@Override
public void swipeSensitiveDataInternal() {
Arrays.fill(this.masterKey, (byte) 0);
Arrays.fill(this.secondaryKey, (byte) 0);
destroyQuietly(primaryMasterKey);
destroyQuietly(hMacMasterKey);
}
private Cipher aesGcmCipher(SecretKey key, byte[] iv, int cipherMode) {
private void destroyQuietly(Destroyable d) {
try {
final Cipher cipher = Cipher.getInstance(AES_GCM_CIPHER);
cipher.init(cipherMode, key, new GCMParameterSpec(AES_GCM_TAG_LENGTH, iv));
d.destroy();
} catch (DestroyFailedException e) {
// ignore
}
}
private Cipher aesKeyWrapCipher(SecretKey key, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_KEYWRAP_CIPHER);
cipher.init(cipherMode, key);
return cipher;
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
throw new IllegalStateException("Algorithm/Padding should exist and accept GCM specs.", ex);
}
}
@@ -250,24 +259,14 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
return result;
}
private SecretKey deriveSecretKeyFromMasterKey() {
final char[] pw = new char[masterKey.length];
try {
byteToChar(masterKey, pw);
return pbkdf2(CharBuffer.wrap(pw), EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
} finally {
Arrays.fill(pw, (char) 0);
}
}
private SecretKey pbkdf2(CharSequence password, byte[] salt, int iterations, int keyLength) {
private SecretKey pbkdf2(CharSequence password, byte[] salt, int iterations, int keyLengthInBits) {
final int pwLen = password.length();
final char[] pw = new char[pwLen];
CharBuffer.wrap(password).get(pw, 0, pwLen);
try {
final KeySpec specs = new PBEKeySpec(pw, salt, iterations, keyLength);
final KeySpec specs = new PBEKeySpec(pw, salt, iterations, keyLengthInBits);
final SecretKey pbkdf2Key = PBKDF2_FACTORY.generateSecret(specs);
final SecretKey aesKey = new SecretKeySpec(pbkdf2Key.getEncoded(), CRYPTO_ALGORITHM);
final SecretKey aesKey = new SecretKeySpec(pbkdf2Key.getEncoded(), AES_KEY_ALGORITHM);
return aesKey;
} catch (InvalidKeySpecException ex) {
throw new IllegalStateException("Specs are hard-coded.", ex);
@@ -276,26 +275,16 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
private void byteToChar(byte[] source, char[] destination) {
if (source.length != destination.length) {
throw new IllegalArgumentException("char[] needs to be the same length as byte[]");
}
for (int i = 0; i < source.length; i++) {
destination[i] = (char) (source[i] & 0xFF);
}
}
private long crc32Sum(byte[] source) {
final CRC32 crc32 = new CRC32();
crc32.update(source);
return crc32.getValue();
}
private byte[] hmacSha256(byte[] key, byte[] data) {
private byte[] hmacSha256(byte[] data) {
try {
final SecretKeySpec secretKey = new SecretKeySpec(key, "HmacSHA256");
final Mac mac = Mac.getInstance("HmacSHA256");
mac.init(secretKey);
final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
mac.init(hMacMasterKey);
return mac.doFinal(data);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.", e);
@@ -307,11 +296,10 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try {
final SecretKey key = this.deriveSecretKeyFromMasterKey();
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, key, ioSupport);
final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, ioSupport);
encryptedPathComps.add(encrypted);
}
return StringUtils.join(encryptedPathComps, encryptedPathSep);
@@ -336,7 +324,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final byte[] mac = hmacSha256(secondaryKey, cleartext.getBytes());
final byte[] mac = hmacSha256(cleartext.getBytes());
final byte[] partialIv = ArrayUtils.subarray(mac, 0, 10);
final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
iv.put(partialIv);
@@ -360,11 +348,10 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try {
final SecretKey key = this.deriveSecretKeyFromMasterKey();
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, key, ioSupport);
final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, ioSupport);
cleartextPathComps.add(new String(cleartext));
}
return StringUtils.join(cleartextPathComps, cleartextPathSep);
@@ -438,9 +425,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
throw new IOException("Failed to read encrypted file header.");
}
// derive secret key and generate cipher:
final SecretKey key = this.deriveSecretKeyFromMasterKey();
final Cipher cipher = this.aesCtrCipher(key, countingIv.array(), Cipher.DECRYPT_MODE);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
@@ -469,9 +455,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// fast forward stream:
encryptedFile.position(SIZE_OF_LONG + AES_BLOCK_LENGTH + beginOfFirstRelevantBlock);
// derive secret key and generate cipher:
final SecretKey key = this.deriveSecretKeyFromMasterKey();
final Cipher cipher = this.aesCtrCipher(key, countingIv.array(), Cipher.DECRYPT_MODE);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
@@ -489,9 +474,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, 0l);
countingIv.position(0);
// derive secret key and generate cipher:
final SecretKey key = this.deriveSecretKeyFromMasterKey();
final Cipher cipher = this.aesCtrCipher(key, countingIv.array(), Cipher.ENCRYPT_MODE);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE);
// 8 bytes (file size: temporarily -1):
final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG);

View File

@@ -18,7 +18,7 @@ interface AesCryptographicConfiguration {
/**
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
*/
int MASTER_KEY_LENGTH = 256;
int MAX_MASTER_KEY_LENGTH_IN_BITS = 256;
/**
* Number of bytes used as salt, where needed.
@@ -48,19 +48,19 @@ interface AesCryptographicConfiguration {
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#AlgorithmParameters
*/
String CRYPTO_ALGORITHM = "AES";
String AES_KEY_ALGORITHM = "AES";
/**
* Cipher specs for masterkey encryption.
* Key algorithm for keyed MAC.
*/
String HMAC_KEY_ALGORITHM = "HmacSHA256";
/**
* Cipher specs for RFC 3394 masterkey encryption.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/
String AES_GCM_CIPHER = "AES/GCM/NoPadding";
/**
* Length of authentication tag.
*/
int AES_GCM_TAG_LENGTH = 128;
String AES_KEYWRAP_CIPHER = "AESWrap";
/**
* Cipher specs for file name and file content encryption. Using CTR-mode for random access.
@@ -86,10 +86,4 @@ interface AesCryptographicConfiguration {
*/
int PBKDF2_PW_ITERATIONS = 1000;
/**
* Number of iterations for key derived from masterkey. Low iteration count for better performance. No additional security is added by
* high values.
*/
int PBKDF2_MASTERKEY_ITERATIONS = 1;
}

View File

@@ -4,15 +4,15 @@ import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder(value = {"salt", "iv", "iterations", "keyLength", "masterkey", "secondaryKey"})
public class Key implements Serializable {
@JsonPropertyOrder(value = {"salt", "iv", "iterations", "keyLength", "primaryMasterKey", "hMacMasterKey"})
public class KeyFile implements Serializable {
private static final long serialVersionUID = 8578363158959619885L;
private byte[] salt;
private byte[] iv;
private int iterations;
private int keyLength;
private byte[] masterkey;
private byte[] primaryMasterKey;
private byte[] hMacMasterKey;
public byte[] getSalt() {
return salt;
@@ -22,14 +22,6 @@ public class Key implements Serializable {
this.salt = salt;
}
public byte[] getIv() {
return iv;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
public int getIterations() {
return iterations;
}
@@ -46,12 +38,20 @@ public class Key implements Serializable {
this.keyLength = keyLength;
}
public byte[] getMasterkey() {
return masterkey;
public byte[] getPrimaryMasterKey() {
return primaryMasterKey;
}
public void setMasterkey(byte[] masterkey) {
this.masterkey = masterkey;
public void setPrimaryMasterKey(byte[] primaryMasterKey) {
this.primaryMasterKey = primaryMasterKey;
}
public byte[] getHMacMasterKey() {
return hMacMasterKey;
}
public void setHMacMasterKey(byte[] hMacMasterKey) {
this.hMacMasterKey = hMacMasterKey;
}
}