Ver Fonte

Switching to P-384 + X9.63 KDF SHA-256 + AES-GCM

Sebastian Stenzel há 4 anos atrás
pai
commit
01b2b47823

+ 83 - 11
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 <a href="https://www.secg.org/sec1-v2.pdf">ANSI-X9.63-KDF</a> 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.");
 		}
 	}
 

+ 1 - 1
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";

+ 24 - 0
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
+ * <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-7.3">RFC 8252</a> but with an encrypted masterkey instead of an authorization code.
+ * <p>
+ * If the <code>kid</code> 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:
+ * <ul>
+ *     <li>A device-specific public key (generated by this application and stored among its settings</li>
+ *     <li>A unique device ID (stored in settings)</li>
+ *     <li>A loopback callback address</li>
+ * </ul>
+ * <p>
+ * 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.
+ * <p>
+ * The vault key can be decrypted using this ECIES:
+ * <ol>
+ *     <li>Generate shared secret using ECDH without cofactor</li>
+ *     <li>Derive 44 bytes using ANSI X9.63 KDF with SHA256</li>
+ *     <li>Decrypt payload via AES-GCM, using first 32 bytes as key, last 12 bytes as IV</li>
+ *     <li>No MAC check required, as AES-GCM includes a tag already</li>
+ * </ol>
+ */
+package org.cryptomator.ui.keyloading.hub;

+ 60 - 0
src/test/java/org/cryptomator/ui/keyloading/hub/EciesHelperTest.java

@@ -0,0 +1,60 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.google.common.io.BaseEncoding;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.converter.ArgumentConversionException;
+import org.junit.jupiter.params.converter.ArgumentConverter;
+import org.junit.jupiter.params.converter.ConvertWith;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+
+public class EciesHelperTest {
+
+	@DisplayName("ECDH + KDF")
+	@ParameterizedTest
+	@ValueSource(ints = {16, 32, 44, 128})
+	public void testEcdhAndKdf(int len) throws NoSuchAlgorithmException {
+		var alice = KeyPairGenerator.getInstance("EC").generateKeyPair();
+		var bob = KeyPairGenerator.getInstance("EC").generateKeyPair();
+
+		byte[] result1 = EciesHelper.ecdhAndKdf(alice.getPrivate(), bob.getPublic(), len);
+		byte[] result2 = EciesHelper.ecdhAndKdf(bob.getPrivate(), alice.getPublic(), len);
+
+		Assertions.assertArrayEquals(result1, result2);
+	}
+
+	@DisplayName("ANSI-X9.63-KDF")
+	@ParameterizedTest
+	@CsvSource(value = {
+			"96c05619d56c328ab95fe84b18264b08725b85e33fd34f08, , 16, 443024c3dae66b95e6f5670601558f71",
+			"96f600b73ad6ac5629577eced51743dd2c24c21b1ac83ee4, , 16, b6295162a7804f5667ba9070f82fa522",
+			"22518b10e70f2a3f243810ae3254139efbee04aa57c7af7d, 75eef81aa3041e33b80971203d2c0c52, 128, c498af77161cc59f2962b9a713e2b215152d139766ce34a776df11866a69bf2e52a13d9c7c6fc878c50c5ea0bc7b00e0da2447cfd874f6cf92f30d0097111485500c90c3af8b487872d04685d14c8d1dc8d7fa08beb0ce0ababc11f0bd496269142d43525a78e5bc79a17f59676a5706dc54d54d4d1f0bd7e386128ec26afc21",
+			"7e335afa4b31d772c0635c7b0e06f26fcd781df947d2990a, d65a4812733f8cdbcdfb4b2f4c191d87, 128, c0bd9e38a8f9de14c2acd35b2f3410c6988cf02400543631e0d6a4c1d030365acbf398115e51aaddebdc9590664210f9aa9fed770d4c57edeafa0b8c14f93300865251218c262d63dadc47dfa0e0284826793985137e0a544ec80abf2fdf5ab90bdaea66204012efe34971dc431d625cd9a329b8217cc8fd0d9f02b13f2f6b0b",
+	})
+	// test vectors from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/components/800-135testvectors/ansx963_2001.zip
+	public void testKdf(@ConvertWith(HexConverter.class) byte[] sharedSecret, @ConvertWith(HexConverter.class) byte[] sharedInfo, int outLen, @ConvertWith(HexConverter.class) byte[] expectedResult) {
+		byte[] result = EciesHelper.kdf(sharedSecret, sharedInfo, outLen);
+		Assertions.assertArrayEquals(expectedResult, result);
+	}
+
+	public static class HexConverter implements ArgumentConverter {
+
+		@Override
+		public byte[] convert(Object source, ParameterContext context) throws ArgumentConversionException {
+			if (source == null) {
+				return new byte[0];
+			} else if (source instanceof String s) {
+				return BaseEncoding.base16().lowerCase().decode(s);
+			} else {
+				return null;
+			}
+		}
+	}
+
+}