Skip to content

Commit

Permalink
Merge pull request #22 from cryptomator/feature/ecies
Browse files Browse the repository at this point in the history
Added MasterkeyHubAccess
  • Loading branch information
overheadhunter authored Aug 20, 2021
2 parents eeb44ef + 3f73eba commit 127d0fd
Show file tree
Hide file tree
Showing 13 changed files with 682 additions and 2 deletions.
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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
public final class MessageDigestSupplier {

public static final MessageDigestSupplier SHA1 = new MessageDigestSupplier("SHA-1");
public static final MessageDigestSupplier SHA256 = new MessageDigestSupplier("SHA-256");

private final String digestAlgorithm;
private final ThreadLocal<MessageDigest> threadLocal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static P384KeyPair generate() {
*
* @param p12File A .p12 file
* @param passphrase The password to protect the key material
* @return
* @return loaded key pair
* @throws IOException In case of I/O errors
* @throws Pkcs12PasswordException If the supplied password is incorrect
* @throws Pkcs12Exception If any cryptographic operation fails
Expand All @@ -49,7 +49,7 @@ public static P384KeyPair load(Path p12File, char[] passphrase) throws IOExcepti
*
* @param in An input stream providing PKCS#12 formatted data
* @param passphrase The password to protect the key material
* @return
* @return loaded key pair
* @throws IOException In case of I/O errors
* @throws Pkcs12PasswordException If the supplied password is incorrect
* @throws Pkcs12Exception If any cryptographic operation fails
Expand Down
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;
}
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");
}
}


}
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;
}

}
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);
}
}
}
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);
}
}

}
Loading

0 comments on commit 127d0fd

Please sign in to comment.