-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #22 from cryptomator/feature/ecies
Added MasterkeyHubAccess
- Loading branch information
Showing
13 changed files
with
682 additions
and
2 deletions.
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
src/main/java/org/cryptomator/cryptolib/common/MasterkeyHubAccess.java
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package org.cryptomator.cryptolib.common; | ||
|
||
import com.google.common.io.BaseEncoding; | ||
import org.cryptomator.cryptolib.api.Masterkey; | ||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; | ||
import org.cryptomator.cryptolib.ecies.EncryptedMessage; | ||
import org.cryptomator.cryptolib.ecies.ECIntegratedEncryptionScheme; | ||
|
||
import javax.crypto.AEADBadTagException; | ||
import java.security.KeyFactory; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.PublicKey; | ||
import java.security.interfaces.ECPrivateKey; | ||
import java.security.interfaces.ECPublicKey; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.security.spec.X509EncodedKeySpec; | ||
import java.util.Arrays; | ||
|
||
public class MasterkeyHubAccess { | ||
|
||
private static final BaseEncoding BASE64_URL = BaseEncoding.base64Url().omitPadding(); | ||
|
||
private MasterkeyHubAccess() { | ||
} | ||
|
||
/** | ||
* Decrypts a masterkey retrieved from Cryptomator Hub | ||
* | ||
* @param devicePrivateKey Private key of the device this ciphertext is intended for | ||
* @param encodedCiphertext The encrypted masterkey | ||
* @param encodedEphPubKey The ephemeral public key to be used to derive a secret shared between message sender and this device | ||
* @return The decrypted masterkey | ||
* @throws MasterkeyLoadingFailedException If the parameters don't match and decryption fails | ||
*/ | ||
public static Masterkey decryptMasterkey(ECPrivateKey devicePrivateKey, String encodedCiphertext, String encodedEphPubKey) throws MasterkeyLoadingFailedException { | ||
byte[] cleartext = new byte[0]; | ||
try { | ||
EncryptedMessage message = decode(encodedCiphertext, encodedEphPubKey); | ||
cleartext = ECIntegratedEncryptionScheme.HUB.decrypt(devicePrivateKey, message); | ||
return new Masterkey(cleartext); | ||
} catch (IllegalArgumentException | AEADBadTagException e) { | ||
throw new MasterkeyLoadingFailedException("Key and ciphertext don't match", e); | ||
} finally { | ||
Arrays.fill(cleartext, (byte) 0x00); | ||
} | ||
} | ||
|
||
private static EncryptedMessage decode(String encodedCiphertext, String encodedEphPubKey) throws IllegalArgumentException { | ||
byte[] ciphertext = BASE64_URL.decode(encodedCiphertext); | ||
byte[] keyBytes = BASE64_URL.decode(encodedEphPubKey); | ||
try { | ||
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes)); | ||
if (key instanceof ECPublicKey) { | ||
return new EncryptedMessage((ECPublicKey) key, ciphertext); | ||
} else { | ||
throw new IllegalArgumentException("Key not an EC public key."); | ||
} | ||
} catch (InvalidKeySpecException e) { | ||
throw new IllegalArgumentException("Invalid license public key", e); | ||
} catch (NoSuchAlgorithmException e) { | ||
throw new IllegalStateException(e); | ||
} | ||
} | ||
|
||
} |
This file contains 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
This file contains 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
37 changes: 37 additions & 0 deletions
37
src/main/java/org/cryptomator/cryptolib/ecies/AuthenticatedEncryption.java
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package org.cryptomator.cryptolib.ecies; | ||
|
||
import javax.crypto.AEADBadTagException; | ||
|
||
public interface AuthenticatedEncryption { | ||
|
||
/** | ||
* AES-GCM with a 96 bit nonce taken from a the shared secret. | ||
* | ||
* Since the secret is derived via ECDH with an ephemeral key, the nonce is guaranteed to be unique. | ||
*/ | ||
AuthenticatedEncryption GCM_WITH_SECRET_NONCE = new GcmWithSecretNonce(); | ||
|
||
/** | ||
* @return number of bytes required during {@link #encrypt(byte[], byte[])} and {@link #decrypt(byte[], byte[])} | ||
*/ | ||
int requiredSecretBytes(); | ||
|
||
/** | ||
* Encrypts the given <code>plaintext</code> | ||
* | ||
* @param secret secret data required for encryption, such as (but not limited to) a key | ||
* @param plaintext The data to encrypt | ||
* @return The encrypted data, including all required information for authentication, such as a tag | ||
*/ | ||
byte[] encrypt(byte[] secret, byte[] plaintext); | ||
|
||
/** | ||
* Encrypts the given <code>ciphertext</code> | ||
* | ||
* @param secret secret data required for encryption, such as (but not limited to) a key | ||
* @param ciphertext The data to decrypt | ||
* @return The decrypted data | ||
* @throws AEADBadTagException In case of an authentication failure (including wrong <code>secret</code>) | ||
*/ | ||
byte[] decrypt(byte[] secret, byte[] ciphertext) throws AEADBadTagException; | ||
} |
90 changes: 90 additions & 0 deletions
90
src/main/java/org/cryptomator/cryptolib/ecies/ECIntegratedEncryptionScheme.java
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package org.cryptomator.cryptolib.ecies; | ||
|
||
import org.cryptomator.cryptolib.common.Destroyables; | ||
|
||
import javax.crypto.AEADBadTagException; | ||
import javax.crypto.KeyAgreement; | ||
import java.security.InvalidKeyException; | ||
import java.security.KeyPair; | ||
import java.security.KeyPairGenerator; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.interfaces.ECPrivateKey; | ||
import java.security.interfaces.ECPublicKey; | ||
import java.util.Arrays; | ||
|
||
public class ECIntegratedEncryptionScheme { | ||
|
||
/** | ||
* The ECIES used in Cryptomator Hub: | ||
* <ul> | ||
* <li>To be used with {@link org.cryptomator.cryptolib.common.P384KeyPair P-384 EC keys}</li> | ||
* <li>Use ANSI X9.63 KDF with SHA-256 to derive a 352 bit shared secret</li> | ||
* <li>Cut shared secret into 256 bit key + 96 bit nonce used for AES-GCM to encrypt/decrypt</li> | ||
* </ul> | ||
*/ | ||
public static final ECIntegratedEncryptionScheme HUB = new ECIntegratedEncryptionScheme(AuthenticatedEncryption.GCM_WITH_SECRET_NONCE, KeyDerivationFunction.ANSI_X963_SHA256_KDF); | ||
|
||
private final AuthenticatedEncryption ae; | ||
private final KeyDerivationFunction kdf; | ||
|
||
public ECIntegratedEncryptionScheme(AuthenticatedEncryption ae, KeyDerivationFunction kdf) { | ||
this.ae = ae; | ||
this.kdf = kdf; | ||
} | ||
|
||
public EncryptedMessage encrypt(KeyPairGenerator ephKeyGen, ECPublicKey receiverPublicKey, byte[] plaintext) { | ||
KeyPair ephKeyPair = ephKeyGen.generateKeyPair(); | ||
try { | ||
if (ephKeyPair.getPrivate() instanceof ECPrivateKey) { | ||
assert ephKeyPair.getPublic() instanceof ECPublicKey; | ||
byte[] ciphertext = encrypt((ECPrivateKey) ephKeyPair.getPrivate(), receiverPublicKey, plaintext); | ||
return new EncryptedMessage((ECPublicKey) ephKeyPair.getPublic(), ciphertext); | ||
} else { | ||
throw new IllegalArgumentException("key generator didn't create EC key pair"); | ||
} | ||
} finally { | ||
Destroyables.destroySilently(ephKeyPair.getPrivate()); | ||
} | ||
} | ||
|
||
public byte[] decrypt(ECPrivateKey receiverPrivateKey, EncryptedMessage encryptedMessage) throws AEADBadTagException { | ||
return decrypt(receiverPrivateKey, encryptedMessage.getEphPublicKey(), encryptedMessage.getCiphertext()); | ||
} | ||
|
||
// visible for testing | ||
byte[] encrypt(ECPrivateKey ephPrivateKey, ECPublicKey receiverPublicKey, byte[] plaintext) { | ||
byte[] secret = ecdhAndKdf(ephPrivateKey, receiverPublicKey, ae.requiredSecretBytes()); | ||
return ae.encrypt(secret, plaintext); | ||
} | ||
|
||
// visible for testing | ||
byte[] decrypt(ECPrivateKey receiverPrivateKey, ECPublicKey ephPublicKey, byte[] plaintext) throws AEADBadTagException { | ||
byte[] secret = ecdhAndKdf(receiverPrivateKey, ephPublicKey, ae.requiredSecretBytes()); | ||
return ae.decrypt(secret, plaintext); | ||
} | ||
|
||
private byte[] ecdhAndKdf(ECPrivateKey privateKey, ECPublicKey publicKey, int numBytes) { | ||
byte[] sharedSecret = new byte[0]; | ||
try { | ||
KeyAgreement keyAgreement = createKeyAgreement(); | ||
keyAgreement.init(privateKey); | ||
keyAgreement.doPhase(publicKey, true); | ||
sharedSecret = keyAgreement.generateSecret(); | ||
return kdf.deriveKey(sharedSecret, numBytes); | ||
} catch (InvalidKeyException e) { | ||
throw new IllegalArgumentException("Invalid keys", e); | ||
} finally { | ||
Arrays.fill(sharedSecret, (byte) 0x00); | ||
} | ||
} | ||
|
||
private static KeyAgreement createKeyAgreement() { | ||
try { | ||
return KeyAgreement.getInstance("ECDH"); | ||
} catch (NoSuchAlgorithmException e) { | ||
throw new IllegalStateException("ECDH not supported"); | ||
} | ||
} | ||
|
||
|
||
} |
23 changes: 23 additions & 0 deletions
23
src/main/java/org/cryptomator/cryptolib/ecies/EncryptedMessage.java
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package org.cryptomator.cryptolib.ecies; | ||
|
||
import java.security.interfaces.ECPublicKey; | ||
|
||
public class EncryptedMessage { | ||
|
||
private final ECPublicKey ephPublicKey; | ||
private final byte[] ciphertext; | ||
|
||
public EncryptedMessage(ECPublicKey ephPublicKey, byte[] ciphertext) { | ||
this.ephPublicKey = ephPublicKey; | ||
this.ciphertext = ciphertext; | ||
} | ||
|
||
public ECPublicKey getEphPublicKey() { | ||
return ephPublicKey; | ||
} | ||
|
||
public byte[] getCiphertext() { | ||
return ciphertext; | ||
} | ||
|
||
} |
47 changes: 47 additions & 0 deletions
47
src/main/java/org/cryptomator/cryptolib/ecies/GcmWithSecretNonce.java
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package org.cryptomator.cryptolib.ecies; | ||
|
||
import com.google.common.base.Throwables; | ||
import org.cryptomator.cryptolib.common.CipherSupplier; | ||
import org.cryptomator.cryptolib.common.DestroyableSecretKey; | ||
|
||
import javax.crypto.AEADBadTagException; | ||
import javax.crypto.BadPaddingException; | ||
import javax.crypto.Cipher; | ||
import javax.crypto.IllegalBlockSizeException; | ||
import javax.crypto.spec.GCMParameterSpec; | ||
import java.util.Arrays; | ||
|
||
class GcmWithSecretNonce implements AuthenticatedEncryption { | ||
|
||
private static final int GCM_KEY_SIZE = 32; | ||
private static final int GCM_TAG_SIZE = 16; | ||
private static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM | ||
|
||
@Override | ||
public int requiredSecretBytes() { | ||
return GCM_KEY_SIZE + GCM_NONCE_SIZE; | ||
} | ||
|
||
@Override | ||
public byte[] encrypt(byte[] secret, byte[] plaintext) { | ||
try (DestroyableSecretKey key = new DestroyableSecretKey(secret, 0, GCM_KEY_SIZE, "AES")) { | ||
byte[] nonce = Arrays.copyOfRange(secret, GCM_KEY_SIZE, GCM_KEY_SIZE + GCM_NONCE_SIZE); | ||
Cipher cipher = CipherSupplier.AES_GCM.forEncryption(key, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); | ||
return cipher.doFinal(plaintext); | ||
} catch (IllegalBlockSizeException | BadPaddingException e) { | ||
throw new IllegalStateException("Unexpected exception during GCM decryption.", e); | ||
} | ||
} | ||
|
||
@Override | ||
public byte[] decrypt(byte[] secret, byte[] ciphertext) throws AEADBadTagException { | ||
try (DestroyableSecretKey key = new DestroyableSecretKey(secret, 0, GCM_KEY_SIZE, "AES")) { | ||
byte[] nonce = Arrays.copyOfRange(secret, GCM_KEY_SIZE, GCM_KEY_SIZE + GCM_NONCE_SIZE); | ||
Cipher cipher = CipherSupplier.AES_GCM.forDecryption(key, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); | ||
return cipher.doFinal(ciphertext); | ||
} catch (IllegalBlockSizeException | BadPaddingException e) { | ||
Throwables.throwIfInstanceOf(e, AEADBadTagException.class); | ||
throw new IllegalStateException("Unexpected exception during GCM decryption.", e); | ||
} | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
src/main/java/org/cryptomator/cryptolib/ecies/KeyDerivationFunction.java
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package org.cryptomator.cryptolib.ecies; | ||
|
||
import org.cryptomator.cryptolib.common.MessageDigestSupplier; | ||
|
||
import java.math.BigInteger; | ||
import java.nio.ByteBuffer; | ||
import java.nio.ByteOrder; | ||
import java.security.DigestException; | ||
import java.security.MessageDigest; | ||
import java.util.Arrays; | ||
|
||
@FunctionalInterface | ||
public interface KeyDerivationFunction { | ||
|
||
KeyDerivationFunction ANSI_X963_SHA256_KDF = (sharedSecret, keyDataLen) -> ansiX963sha256Kdf(sharedSecret, new byte[0], keyDataLen); | ||
|
||
/** | ||
* Derives a key of desired length | ||
* | ||
* @param sharedSecret A shared secret | ||
* @param keyDataLen Desired key length (in bytes) | ||
* @return key data | ||
*/ | ||
byte[] deriveKey(byte[] sharedSecret, int keyDataLen); | ||
|
||
/** | ||
* Performs <a href="https://www.secg.org/sec1-v2.pdf">ANSI-X9.63-KDF</a> with SHA-256 | ||
* | ||
* @param sharedSecret A shared secret | ||
* @param sharedInfo Additional authenticated data | ||
* @param keyDataLen Desired key length (in bytes) | ||
* @return key data | ||
*/ | ||
static byte[] ansiX963sha256Kdf(byte[] sharedSecret, byte[] sharedInfo, int keyDataLen) { | ||
MessageDigest digest = MessageDigestSupplier.SHA256.get(); // max input length is 2^64 - 1, see https://doi.org/10.6028/NIST.SP.800-56Cr2, Table 1 | ||
int hashLen = digest.getDigestLength(); | ||
|
||
// These two checks must be performed according to spec. However with 32 bit integers, we can't exceed any limits anyway: | ||
assert BigInteger.valueOf(4L + sharedSecret.length + sharedInfo.length).compareTo(BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE)) < 0 : "input larger than hashmaxlen"; | ||
assert keyDataLen < (1L << 32 - 1) * hashLen : "keyDataLen larger than hashLen × (2^32 − 1)"; | ||
|
||
ByteBuffer counter = ByteBuffer.allocate(Integer.BYTES); | ||
assert ByteOrder.BIG_ENDIAN.equals(counter.order()); | ||
int n = (keyDataLen + hashLen - 1) / hashLen; | ||
byte[] buffer = new byte[n * hashLen]; | ||
try { | ||
for (int i = 0; i < n; i++) { | ||
digest.update(sharedSecret); | ||
counter.clear(); | ||
counter.putInt(i + 1); | ||
counter.flip(); | ||
digest.update(counter); | ||
digest.update(sharedInfo); | ||
digest.digest(buffer, i * hashLen, hashLen); | ||
} | ||
return Arrays.copyOf(buffer, keyDataLen); | ||
} catch (DigestException e) { | ||
throw new IllegalStateException("Invalid digest output buffer offset", e); | ||
} finally { | ||
Arrays.fill(buffer, (byte) 0x00); | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.