Skip to content

Commit

Permalink
- fixes #6, simplifies password verification
Browse files Browse the repository at this point in the history
- improves filename IV -> SIV using substring from sha256(secondaryKey + plaintextFilename). References #7
  • Loading branch information
Sebastian Stenzel committed Dec 31, 2014
1 parent 4cb9da7 commit 9fe135e
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 60 deletions.
Expand Up @@ -19,6 +19,7 @@
import java.nio.file.Path; import java.nio.file.Path;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
Expand All @@ -30,6 +31,7 @@
import java.util.UUID; import java.util.UUID;
import java.util.zip.CRC32; import java.util.zip.CRC32;


import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.CipherInputStream; import javax.crypto.CipherInputStream;
Expand All @@ -38,12 +40,14 @@
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory; import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;


import org.apache.commons.io.Charsets; import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.AbstractCryptor; import org.cryptomator.crypto.AbstractCryptor;
import org.cryptomator.crypto.CryptorIOSupport; import org.cryptomator.crypto.CryptorIOSupport;
Expand Down Expand Up @@ -90,6 +94,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/ */
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH]; private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];


/**
* If certain cryptographic operations need a second key, which is distinct to the masterKey
*/
private final byte[] secondaryKey = new byte[MASTER_KEY_LENGTH];

private static final int SIZE_OF_LONG = Long.BYTES; private static final int SIZE_OF_LONG = Long.BYTES;


static { static {
Expand All @@ -109,6 +118,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
public Aes256Cryptor() { public Aes256Cryptor() {
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH)); SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
SECURE_PRNG.nextBytes(this.masterKey); SECURE_PRNG.nextBytes(this.masterKey);
SECURE_PRNG.nextBytes(this.secondaryKey);
} }


/** /**
Expand All @@ -119,35 +129,39 @@ public Aes256Cryptor() {
*/ */
Aes256Cryptor(Random prng) { Aes256Cryptor(Random prng) {
prng.nextBytes(this.masterKey); prng.nextBytes(this.masterKey);
prng.nextBytes(this.secondaryKey);
} }


/** /**
* Encrypts the current masterKey with the given password and writes the result to the given output stream. * Encrypts the current masterKey with the given password and writes the result to the given output stream.
*/ */
@Override @Override
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException { 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 { try {
// derive key: // derive key:
final byte[] userSalt = randomData(SALT_LENGTH); final byte[] userSalt = randomData(SALT_LENGTH);
final SecretKey userKey = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH); final SecretKey userKey = pbkdf2(password, userSalt, PBKDF2_PW_ITERATIONS, AES_KEY_LENGTH);


// encrypt: // encrypt:
final byte[] iv = randomData(AES_BLOCK_LENGTH); final byte[] iv = randomData(AES_BLOCK_LENGTH);
final Cipher encCipher = this.cipher(MASTERKEY_CIPHER, userKey, iv, Cipher.ENCRYPT_MODE); final Cipher encCipher = aesGcmCipher(userKey, iv, Cipher.ENCRYPT_MODE);
byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded()); byte[] encryptedCombinedKey = encCipher.doFinal(combinedKey.array());
byte[] encryptedMasterKey = encCipher.doFinal(this.masterKey);


// save encrypted masterkey: // save encrypted masterkey:
final Key key = new Key(); final Key key = new Key();
key.setIterations(PBKDF2_PW_ITERATIONS); key.setIterations(PBKDF2_PW_ITERATIONS);
key.setIv(iv); key.setIv(iv);
key.setKeyLength(AES_KEY_LENGTH); key.setKeyLength(AES_KEY_LENGTH);
key.setMasterkey(encryptedMasterKey); key.setMasterkey(encryptedCombinedKey);
key.setSalt(userSalt); key.setSalt(userSalt);
key.setPwVerification(encryptedUserKey);
objectMapper.writeValue(out, key); objectMapper.writeValue(out, key);
} catch (IllegalBlockSizeException | BadPaddingException ex) { } catch (IllegalBlockSizeException | BadPaddingException ex) {
throw new IllegalStateException("Block size hard coded. Padding irrelevant in ENCRYPT_MODE. IV must exist in CBC mode.", 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);
} }
} }


Expand All @@ -162,7 +176,7 @@ public void encryptMasterKey(OutputStream out, CharSequence password) throws IOE
*/ */
@Override @Override
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException { public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
byte[] decrypted = new byte[0]; byte[] combinedKey = new byte[0];
try { try {
// load encrypted masterkey: // load encrypted masterkey:
final Key key = objectMapper.readValue(in, Key.class); final Key key = objectMapper.readValue(in, Key.class);
Expand All @@ -176,26 +190,22 @@ public void decryptMasterKey(InputStream in, CharSequence password) throws Decry
// derive key: // derive key:
final SecretKey userKey = pbkdf2(password, key.getSalt(), key.getIterations(), key.getKeyLength()); final SecretKey userKey = pbkdf2(password, key.getSalt(), key.getIterations(), key.getKeyLength());


// check password: // decrypt and check password by catching AEAD exception
final Cipher encCipher = this.cipher(MASTERKEY_CIPHER, userKey, key.getIv(), Cipher.ENCRYPT_MODE); final Cipher decCipher = aesGcmCipher(userKey, key.getIv(), Cipher.DECRYPT_MODE);
byte[] encryptedUserKey = encCipher.doFinal(userKey.getEncoded()); combinedKey = decCipher.doFinal(key.getMasterkey());
if (!Arrays.equals(key.getPwVerification(), encryptedUserKey)) {
throw new WrongPasswordException();
}

// decrypt:
final Cipher decCipher = this.cipher(MASTERKEY_CIPHER, userKey, key.getIv(), Cipher.DECRYPT_MODE);
decrypted = decCipher.doFinal(key.getMasterkey());


// everything ok, move decrypted data to masterkey: // everything ok, split decrypted data to masterkey and secondary key:
final ByteBuffer masterKeyBuffer = ByteBuffer.wrap(this.masterKey); final ByteBuffer combinedKeyBuffer = ByteBuffer.wrap(combinedKey);
masterKeyBuffer.put(decrypted); combinedKeyBuffer.get(this.masterKey);
combinedKeyBuffer.get(this.secondaryKey);
} catch (AEADBadTagException e) {
throw new WrongPasswordException();
} catch (IllegalBlockSizeException | BadPaddingException | BufferOverflowException ex) { } catch (IllegalBlockSizeException | BadPaddingException | BufferOverflowException ex) {
throw new DecryptFailedException(ex); throw new DecryptFailedException(ex);
} catch (NoSuchAlgorithmException ex) { } catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("Algorithm should exist.", ex); throw new IllegalStateException("Algorithm should exist.", ex);
} finally { } finally {
Arrays.fill(decrypted, (byte) 0); Arrays.fill(combinedKey, (byte) 0);
} }
} }


Expand All @@ -206,11 +216,24 @@ public void decryptMasterKey(InputStream in, CharSequence password) throws Decry
@Override @Override
public void swipeSensitiveDataInternal() { public void swipeSensitiveDataInternal() {
Arrays.fill(this.masterKey, (byte) 0); Arrays.fill(this.masterKey, (byte) 0);
Arrays.fill(this.secondaryKey, (byte) 0);
} }


private Cipher cipher(String cipherTransformation, SecretKey key, byte[] iv, int cipherMode) { private Cipher aesGcmCipher(SecretKey key, byte[] iv, int cipherMode) {
try { try {
final Cipher cipher = Cipher.getInstance(cipherTransformation); final Cipher cipher = Cipher.getInstance(AES_GCM_CIPHER);
cipher.init(cipherMode, key, new GCMParameterSpec(AES_GCM_TAG_LENGTH, iv));
return cipher;
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
throw new IllegalStateException("Algorithm/Padding should exist and accept GCM specs.", ex);
}
}

private Cipher aesCtrCipher(SecretKey key, byte[] iv, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_CTR_CIPHER);
cipher.init(cipherMode, key, new IvParameterSpec(iv)); cipher.init(cipherMode, key, new IvParameterSpec(iv));
return cipher; return cipher;
} catch (InvalidKeyException ex) { } catch (InvalidKeyException ex) {
Expand Down Expand Up @@ -268,6 +291,15 @@ private long crc32Sum(byte[] source) {
return crc32.getValue(); return crc32.getValue();
} }


private byte[] sha256(byte[] data) {
try {
final MessageDigest md = MessageDigest.getInstance("SHA-256");
return md.digest(data);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support SHA-256.", e);
}
}

@Override @Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) { public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try { try {
Expand Down Expand Up @@ -300,13 +332,14 @@ public String encryptPath(String cleartextPath, char encryptedPathSep, char clea
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}. * {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/ */
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException { private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final long cleartextHash = crc32Sum(cleartext.getBytes()); final byte[] mac = sha256(ArrayUtils.addAll(secondaryKey, cleartext.getBytes()));
final byte[] partialIv = ArrayUtils.subarray(mac, 0, 10);
final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH); final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
iv.putLong(cleartextHash); iv.put(partialIv);
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, iv.array(), Cipher.ENCRYPT_MODE); final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.ENCRYPT_MODE);
final byte[] cleartextBytes = cleartext.getBytes(Charsets.UTF_8); final byte[] cleartextBytes = cleartext.getBytes(Charsets.UTF_8);
final byte[] encryptedBytes = cipher.doFinal(cleartextBytes); final byte[] encryptedBytes = cipher.doFinal(cleartextBytes);
final String ivAndCiphertext = Long.toHexString(cleartextHash) + IV_PREFIX_SEPARATOR + ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes); final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(partialIv) + IV_PREFIX_SEPARATOR + ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);


if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) { if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String crc32 = Long.toHexString(crc32Sum(ivAndCiphertext.getBytes())); final String crc32 = Long.toHexString(crc32Sum(ivAndCiphertext.getBytes()));
Expand Down Expand Up @@ -354,12 +387,12 @@ private String decryptPathComponent(final String encrypted, final SecretKey key,
throw new IllegalArgumentException("Unsupported path component: " + encrypted); throw new IllegalArgumentException("Unsupported path component: " + encrypted);
} }


final String cleartextHash = StringUtils.substringBefore(ivAndCiphertext, IV_PREFIX_SEPARATOR); final String partialIvStr = StringUtils.substringBefore(ivAndCiphertext, IV_PREFIX_SEPARATOR);
final String ciphertext = StringUtils.substringAfter(ivAndCiphertext, IV_PREFIX_SEPARATOR); final String ciphertext = StringUtils.substringAfter(ivAndCiphertext, IV_PREFIX_SEPARATOR);
final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH); final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
iv.putLong(Long.parseLong(cleartextHash, 16)); iv.put(ENCRYPTED_FILENAME_CODEC.decode(partialIvStr));


final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, iv.array(), Cipher.DECRYPT_MODE); final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.DECRYPT_MODE);
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext); final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
final byte[] cleartextBytes = cipher.doFinal(encryptedBytes); final byte[] cleartextBytes = cipher.doFinal(encryptedBytes);
return new String(cleartextBytes, Charsets.UTF_8); return new String(cleartextBytes, Charsets.UTF_8);
Expand Down Expand Up @@ -403,7 +436,7 @@ public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaint


// derive secret key and generate cipher: // derive secret key and generate cipher:
final SecretKey key = this.deriveSecretKeyFromMasterKey(); final SecretKey key = this.deriveSecretKeyFromMasterKey();
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE); final Cipher cipher = this.aesCtrCipher(key, countingIv.array(), Cipher.DECRYPT_MODE);


// read content // read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile); final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
Expand Down Expand Up @@ -434,7 +467,7 @@ public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plainte


// derive secret key and generate cipher: // derive secret key and generate cipher:
final SecretKey key = this.deriveSecretKeyFromMasterKey(); final SecretKey key = this.deriveSecretKeyFromMasterKey();
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE); final Cipher cipher = this.aesCtrCipher(key, countingIv.array(), Cipher.DECRYPT_MODE);


// read content // read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile); final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
Expand All @@ -454,7 +487,7 @@ public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encrypted


// derive secret key and generate cipher: // derive secret key and generate cipher:
final SecretKey key = this.deriveSecretKeyFromMasterKey(); final SecretKey key = this.deriveSecretKeyFromMasterKey();
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.ENCRYPT_MODE); final Cipher cipher = this.aesCtrCipher(key, countingIv.array(), Cipher.ENCRYPT_MODE);


// 8 bytes (file size: temporarily -1): // 8 bytes (file size: temporarily -1):
final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG); final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG);
Expand Down
Expand Up @@ -55,21 +55,19 @@ interface AesCryptographicConfiguration {
* *
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/ */
String MASTERKEY_CIPHER = "AES/CTR/NoPadding"; String AES_GCM_CIPHER = "AES/GCM/NoPadding";


/** /**
* Cipher specs for file name encryption. * Length of authentication tag.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/ */
String FILE_NAME_CIPHER = "AES/CTR/NoPadding"; int AES_GCM_TAG_LENGTH = 128;


/** /**
* Cipher specs for content encryption. Using CTR-mode for random access. * Cipher specs for file name and file content encryption. Using CTR-mode for random access.
* *
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/ */
String FILE_CONTENT_CIPHER = "AES/CTR/NoPadding"; String AES_CTR_CIPHER = "AES/CTR/NoPadding";


/** /**
* AES block size is 128 bit or 16 bytes. * AES block size is 128 bit or 16 bytes.
Expand Down
Expand Up @@ -4,21 +4,20 @@


import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.annotation.JsonPropertyOrder;


@JsonPropertyOrder(value = { "salt", "iv", "iterations", "keyLength", "masterkey" }) @JsonPropertyOrder(value = {"salt", "iv", "iterations", "keyLength", "masterkey", "secondaryKey"})
public class Key implements Serializable { public class Key implements Serializable {


private static final long serialVersionUID = 8578363158959619885L; private static final long serialVersionUID = 8578363158959619885L;
private byte[] salt; private byte[] salt;
private byte[] iv; private byte[] iv;
private int iterations; private int iterations;
private int keyLength; private int keyLength;
private byte[] pwVerification;
private byte[] masterkey; private byte[] masterkey;

public byte[] getSalt() { public byte[] getSalt() {
return salt; return salt;
} }

public void setSalt(byte[] salt) { public void setSalt(byte[] salt) {
this.salt = salt; this.salt = salt;
} }
Expand Down Expand Up @@ -47,14 +46,6 @@ public void setKeyLength(int keyLength) {
this.keyLength = keyLength; this.keyLength = keyLength;
} }


public byte[] getPwVerification() {
return pwVerification;
}

public void setPwVerification(byte[] pwVerification) {
this.pwVerification = pwVerification;
}

public byte[] getMasterkey() { public byte[] getMasterkey() {
return masterkey; return masterkey;
} }
Expand All @@ -63,5 +54,4 @@ public void setMasterkey(byte[] masterkey) {
this.masterkey = masterkey; this.masterkey = masterkey;
} }



} }
Expand Up @@ -47,21 +47,29 @@ public void testCorrectPassword() throws IOException, WrongPasswordException, De
IOUtils.closeQuietly(in); IOUtils.closeQuietly(in);
} }


@Test(expected = WrongPasswordException.class) @Test
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
final String pw = "asd"; final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG); final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw); cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData(); cryptor.swipeSensitiveData();
IOUtils.closeQuietly(out);


final String wrongPw = "foo"; // all these passwords are expected to fail.
final String[] wrongPws = {"a", "as", "asdf", "sdf", "das", "dsa", "foo", "bar", "baz"};
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG); final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final InputStream in = new ByteArrayInputStream(out.toByteArray()); for (final String wrongPw : wrongPws) {
decryptor.decryptMasterKey(in, wrongPw); final InputStream in = new ByteArrayInputStream(out.toByteArray());

try {
IOUtils.closeQuietly(out); decryptor.decryptMasterKey(in, wrongPw);
IOUtils.closeQuietly(in); Assert.fail("should not succeed.");
} catch (WrongPasswordException e) {
continue;
} finally {
IOUtils.closeQuietly(in);
}
}
} }


@Test @Test
Expand Down

0 comments on commit 9fe135e

Please sign in to comment.