mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 08:41:28 +00:00
- fixes #8: Using Scrypt key derivation function now
This commit is contained in:
@@ -17,12 +17,23 @@
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<name>Cryptomator cryptographic module (AES)</name>
|
||||
<description>Provides stream ciphers and filename pseudonymization functions.</description>
|
||||
|
||||
<properties>
|
||||
<bouncycastle.version>1.51</bouncycastle.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Bouncycastle -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Commons -->
|
||||
<dependency>
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
@@ -20,8 +19,6 @@ import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -37,9 +34,7 @@ import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
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;
|
||||
@@ -48,6 +43,7 @@ import org.apache.commons.io.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
import org.cryptomator.crypto.AbstractCryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
@@ -68,14 +64,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
*/
|
||||
private static final SecureRandom SECURE_PRNG;
|
||||
|
||||
/**
|
||||
* Factory for deriveing keys. Defaults to PBKDF2/HMAC-SHA1.
|
||||
*
|
||||
* @see PKCS #5, defined in RFC 2898
|
||||
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecretKeyFactory
|
||||
*/
|
||||
private static final SecretKeyFactory PBKDF2_FACTORY;
|
||||
|
||||
/**
|
||||
* 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/.
|
||||
@@ -102,10 +90,9 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
|
||||
static {
|
||||
try {
|
||||
PBKDF2_FACTORY = SecretKeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
|
||||
SECURE_PRNG = SecureRandom.getInstance(PRNG_ALGORITHM);
|
||||
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;
|
||||
AES_KEY_LENGTH_IN_BITS = (maxKeyLength >= PREF_MASTER_KEY_LENGTH_IN_BITS) ? PREF_MASTER_KEY_LENGTH_IN_BITS : maxKeyLength;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Algorithm should exist.", e);
|
||||
}
|
||||
@@ -154,8 +141,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
||||
try {
|
||||
// derive key:
|
||||
final byte[] userSalt = randomData(SALT_LENGTH);
|
||||
final SecretKey kek = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH_IN_BITS);
|
||||
final byte[] kekSalt = randomData(SCRYPT_SALT_LENGTH);
|
||||
final SecretKey kek = scrypt(password, kekSalt, SCRYPT_COST_PARAM, SCRYPT_BLOCK_SIZE, AES_KEY_LENGTH_IN_BITS);
|
||||
|
||||
// encrypt:
|
||||
final Cipher encCipher = aesKeyWrapCipher(kek, Cipher.WRAP_MODE);
|
||||
@@ -163,13 +150,14 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
byte[] wrappedSecondaryKey = encCipher.wrap(hMacMasterKey);
|
||||
|
||||
// save encrypted masterkey:
|
||||
final KeyFile key = new KeyFile();
|
||||
key.setIterations(PBKDF2_PW_ITERATIONS);
|
||||
key.setKeyLength(AES_KEY_LENGTH_IN_BITS);
|
||||
key.setPrimaryMasterKey(wrappedPrimaryKey);
|
||||
key.setHMacMasterKey(wrappedSecondaryKey);
|
||||
key.setSalt(userSalt);
|
||||
objectMapper.writeValue(out, key);
|
||||
final KeyFile keyfile = new KeyFile();
|
||||
keyfile.setScryptSalt(kekSalt);
|
||||
keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
|
||||
keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
|
||||
keyfile.setKeyLength(AES_KEY_LENGTH_IN_BITS);
|
||||
keyfile.setPrimaryMasterKey(wrappedPrimaryKey);
|
||||
keyfile.setHMacMasterKey(wrappedSecondaryKey);
|
||||
objectMapper.writeValue(out, keyfile);
|
||||
} catch (InvalidKeyException | IllegalBlockSizeException ex) {
|
||||
throw new IllegalStateException("Invalid hard coded configuration.", ex);
|
||||
}
|
||||
@@ -188,21 +176,21 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
try {
|
||||
// load encrypted masterkey:
|
||||
final KeyFile key = objectMapper.readValue(in, KeyFile.class);
|
||||
final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class);
|
||||
|
||||
// check, whether the key length is supported:
|
||||
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
|
||||
if (key.getKeyLength() > maxKeyLen) {
|
||||
throw new UnsupportedKeyLengthException(key.getKeyLength(), maxKeyLen);
|
||||
if (keyfile.getKeyLength() > maxKeyLen) {
|
||||
throw new UnsupportedKeyLengthException(keyfile.getKeyLength(), maxKeyLen);
|
||||
}
|
||||
|
||||
// derive key:
|
||||
final SecretKey kek = pbkdf2(password, key.getSalt(), key.getIterations(), key.getKeyLength());
|
||||
final SecretKey kek = scrypt(password, keyfile.getScryptSalt(), keyfile.getScryptCostParam(), keyfile.getScryptBlockSize(), AES_KEY_LENGTH_IN_BITS);
|
||||
|
||||
// decrypt and check password by catching AEAD exception
|
||||
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);
|
||||
SecretKey primary = (SecretKey) decCipher.unwrap(keyfile.getPrimaryMasterKey(), AES_KEY_ALGORITHM, Cipher.SECRET_KEY);
|
||||
SecretKey secondary = (SecretKey) decCipher.unwrap(keyfile.getPrimaryMasterKey(), HMAC_KEY_ALGORITHM, Cipher.SECRET_KEY);
|
||||
|
||||
// everything ok, assign decrypted keys:
|
||||
this.primaryMasterKey = primary;
|
||||
@@ -259,19 +247,19 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
return result;
|
||||
}
|
||||
|
||||
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);
|
||||
private SecretKey scrypt(CharSequence password, byte[] salt, int costParam, int blockSize, int keyLengthInBits) {
|
||||
// use sb, as password.toString's implementation is unknown
|
||||
final StringBuilder sb = new StringBuilder(password);
|
||||
final byte[] pw = sb.toString().getBytes();
|
||||
try {
|
||||
final KeySpec specs = new PBEKeySpec(pw, salt, iterations, keyLengthInBits);
|
||||
final SecretKey pbkdf2Key = PBKDF2_FACTORY.generateSecret(specs);
|
||||
final SecretKey aesKey = new SecretKeySpec(pbkdf2Key.getEncoded(), AES_KEY_ALGORITHM);
|
||||
return aesKey;
|
||||
} catch (InvalidKeySpecException ex) {
|
||||
throw new IllegalStateException("Specs are hard-coded.", ex);
|
||||
final byte[] key = SCrypt.generate(pw, salt, costParam, blockSize, 1, keyLengthInBits / Byte.SIZE);
|
||||
return new SecretKeySpec(key, AES_KEY_ALGORITHM);
|
||||
} finally {
|
||||
Arrays.fill(pw, (char) 0);
|
||||
// destroy copied bytes of the plaintext password:
|
||||
Arrays.fill(pw, (byte) 0);
|
||||
for (int i = 0; i < password.length(); i++) {
|
||||
sb.setCharAt(i, (char) 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,32 +11,29 @@ package org.cryptomator.crypto.aes256;
|
||||
interface AesCryptographicConfiguration {
|
||||
|
||||
/**
|
||||
* Number of bytes used as seed for the PRNG.
|
||||
* Number of bytes used as salt, where needed.
|
||||
*/
|
||||
int PRNG_SEED_LENGTH = 16;
|
||||
int SCRYPT_SALT_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* Scrypt CPU/Memory cost parameter.
|
||||
*/
|
||||
int SCRYPT_COST_PARAM = 1 << 14;
|
||||
|
||||
/**
|
||||
* Scrypt block size (affects memory consumption)
|
||||
*/
|
||||
int SCRYPT_BLOCK_SIZE = 8;
|
||||
|
||||
/**
|
||||
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
|
||||
*/
|
||||
int MAX_MASTER_KEY_LENGTH_IN_BITS = 256;
|
||||
int PREF_MASTER_KEY_LENGTH_IN_BITS = 256;
|
||||
|
||||
/**
|
||||
* Number of bytes used as salt, where needed.
|
||||
* Number of bytes used as seed for the PRNG.
|
||||
*/
|
||||
int SALT_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* 0-filled salt.
|
||||
*/
|
||||
byte[] EMPTY_SALT = new byte[SALT_LENGTH];
|
||||
|
||||
/**
|
||||
* Algorithm used for key derivation as defined in RFC 2898 / PKCS #5.
|
||||
*
|
||||
* SHA1 will deprecate soon, but the main purpose of PBKDF2 is waisting CPU cycles, so cryptographically strong hash algorithms are not
|
||||
* necessary here. See also http://crypto.stackexchange.com/a/11017
|
||||
*/
|
||||
String KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA1";
|
||||
int PRNG_SEED_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Algorithm used for random number generation.
|
||||
@@ -81,9 +78,4 @@ interface AesCryptographicConfiguration {
|
||||
*/
|
||||
int FILE_NAME_IV_LENGTH = 5;
|
||||
|
||||
/**
|
||||
* Number of iterations for key derived from user pw. High iteration count for better resistance to bruteforcing.
|
||||
*/
|
||||
int PBKDF2_PW_ITERATIONS = 1000;
|
||||
|
||||
}
|
||||
|
||||
@@ -4,30 +4,39 @@ import java.io.Serializable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
@JsonPropertyOrder(value = {"salt", "iv", "iterations", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
||||
@JsonPropertyOrder(value = {"scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
||||
public class KeyFile implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
private byte[] salt;
|
||||
private int iterations;
|
||||
private byte[] scryptSalt;
|
||||
private int scryptCostParam;
|
||||
private int scryptBlockSize;
|
||||
private int keyLength;
|
||||
private byte[] primaryMasterKey;
|
||||
private byte[] hMacMasterKey;
|
||||
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
public byte[] getScryptSalt() {
|
||||
return scryptSalt;
|
||||
}
|
||||
|
||||
public void setSalt(byte[] salt) {
|
||||
this.salt = salt;
|
||||
public void setScryptSalt(byte[] scryptSalt) {
|
||||
this.scryptSalt = scryptSalt;
|
||||
}
|
||||
|
||||
public int getIterations() {
|
||||
return iterations;
|
||||
public int getScryptCostParam() {
|
||||
return scryptCostParam;
|
||||
}
|
||||
|
||||
public void setIterations(int iterations) {
|
||||
this.iterations = iterations;
|
||||
public void setScryptCostParam(int scryptCostParam) {
|
||||
this.scryptCostParam = scryptCostParam;
|
||||
}
|
||||
|
||||
public int getScryptBlockSize() {
|
||||
return scryptBlockSize;
|
||||
}
|
||||
|
||||
public void setScryptBlockSize(int scryptBlockSize) {
|
||||
this.scryptBlockSize = scryptBlockSize;
|
||||
}
|
||||
|
||||
public int getKeyLength() {
|
||||
|
||||
Reference in New Issue
Block a user