mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-18 02:31:27 +00:00
Switching to P-384 + X9.63 KDF SHA-256 + AES-GCM
This commit is contained in:
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user