diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/EciesHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/EciesHelper.java index 8823a22ff..a1636ea3a 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/EciesHelper.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/EciesHelper.java @@ -3,12 +3,20 @@ package org.cryptomator.ui.keyloading.hub; import com.google.common.base.Preconditions; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -import org.cryptomator.cryptolib.common.AesKeyWrap; +import org.cryptomator.cryptolib.common.CipherSupplier; import org.cryptomator.cryptolib.common.DestroyableSecretKey; +import javax.crypto.AEADBadTagException; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyAgreement; +import javax.crypto.spec.GCMParameterSpec; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.DigestException; import java.security.InvalidKeyException; import java.security.KeyPair; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; @@ -18,32 +26,96 @@ import java.util.Arrays; class EciesHelper { + private static final int GCM_TAG_SIZE = 16; + private static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM + private EciesHelper() {} public static Masterkey decryptMasterkey(KeyPair deviceKey, EciesParams eciesParams) throws MasterkeyLoadingFailedException { - // TODO: include a KDF between key agreement and KEK to conform to ECIES? - try (var kek = ecdh(deviceKey.getPrivate(), eciesParams.getEphemeralPublicKey()); // - var rawMasterkey = AesKeyWrap.unwrap(kek, eciesParams.getCiphertext(), "HMAC")) { - return new Masterkey(rawMasterkey.getEncoded()); - } catch (InvalidKeyException e) { + var sharedSecret = ecdhAndKdf(deviceKey.getPrivate(), eciesParams.getEphemeralPublicKey(), 44); + var cleartext = new byte[0]; + try (var kek = new DestroyableSecretKey(sharedSecret, 0, 32, "AES")) { + var nonce = Arrays.copyOfRange(sharedSecret, 32, GCM_NONCE_SIZE); + var cipher = CipherSupplier.AES_GCM.forDecryption(kek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + cleartext = cipher.doFinal(eciesParams.getCiphertext()); + return new Masterkey(cleartext); + } catch (AEADBadTagException e) { throw new MasterkeyLoadingFailedException("Unsuitable KEK to decrypt encrypted masterkey", e); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new IllegalStateException("Unexpected exception during GCM decryption.", e); + } finally { + Arrays.fill(sharedSecret, (byte) 0x00); + Arrays.fill(cleartext, (byte) 0x00); } } - private static DestroyableSecretKey ecdh(PrivateKey privateKey, PublicKey publicKey) { + /** + * Computes a shared secret using ECDH key agreement and derives a key. + * + * @param privateKey Recipient's EC private key + * @param publicKey Sender's EC public key + * @param numBytes Number of bytes requested form KDF + * @return A derived secret key + */ + // visible for testing + static byte[] ecdhAndKdf(PrivateKey privateKey, PublicKey publicKey, int numBytes) { Preconditions.checkArgument(privateKey instanceof ECPrivateKey, "expected ECPrivateKey"); Preconditions.checkArgument(publicKey instanceof ECPublicKey, "expected ECPublicKey"); - byte[] keyBytes = new byte[0]; + byte[] sharedSecret = new byte[0]; try { var keyAgreement = createKeyAgreement(); keyAgreement.init(privateKey); keyAgreement.doPhase(publicKey, true); - keyBytes = keyAgreement.generateSecret(); - return new DestroyableSecretKey(keyBytes, "AES"); + sharedSecret = keyAgreement.generateSecret(); + return kdf(sharedSecret, new byte[0], numBytes); } catch (InvalidKeyException e) { throw new IllegalArgumentException("Invalid keys", e); } finally { - Arrays.fill(keyBytes, (byte) 0x00); + Arrays.fill(sharedSecret, (byte) 0x00); + } + } + + /** + * Performs ANSI-X9.63-KDF with SHA-256 + * @param sharedSecret A shared secret + * @param keyDataLen Desired key length (in bytes) + * @return key data + */ + // visible for testing + static byte[] kdf(byte[] sharedSecret, byte[] sharedInfo, int keyDataLen) { + MessageDigest digest = sha256(); // 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(sharedSecret.length + sharedInfo.length + 4).compareTo(BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE)) < 0: "input larger than hashmaxlen"; + assert keyDataLen < (2L << 32 - 1) * hashLen : "keyDataLen larger than hashLen × (2^32 − 1)"; + + ByteBuffer counter = ByteBuffer.allocate(Integer.BYTES); + 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); + } + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Every implementation of the Java platform is required to support SHA-256."); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java index 631bf14fc..99bd7d3fe 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/P12AccessHelper.java @@ -23,7 +23,7 @@ import java.security.spec.ECGenParameterSpec; class P12AccessHelper { private static final String EC_ALG = "EC"; - private static final String EC_CURVE_NAME = "secp256r1"; // TODO switch to secp384r1 + private static final String EC_CURVE_NAME = "secp384r1"; private static final String SIGNATURE_ALG = "SHA256withECDSA"; private static final String KEYSTORE_ALIAS_KEY = "key"; private static final String KEYSTORE_ALIAS_CERT = "crt"; diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/package-info.java b/src/main/java/org/cryptomator/ui/keyloading/hub/package-info.java new file mode 100644 index 000000000..54bd6ae0e --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/package-info.java @@ -0,0 +1,24 @@ +/** + * This {@link org.cryptomator.ui.keyloading.KeyLoadingStrategy strategy} retrieves the vault key from a web application, similar to + * RFC 8252 but with an encrypted masterkey instead of an authorization code. + *
+ * If the kid of the vault config starts with either {@value org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy#SCHEME_HUB_HTTP}
+ * or {@value org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy#SCHEME_HUB_HTTPS}, the included http address is amended by three parameters and opened
+ * in a browser. These parameters are:
+ *
+ * The callback address points to a embedded web server waiting to receive the masterkey encrypted specifically for this device, using the device-specific public key. + *
+ * The vault key can be decrypted using this ECIES: + *