|
@@ -1,7 +1,6 @@
|
|
|
package org.cryptomator.ui.keyloading.hub;
|
|
|
|
|
|
import com.google.common.base.Preconditions;
|
|
|
-import com.google.common.io.BaseEncoding;
|
|
|
import com.nimbusds.jose.EncryptionMethod;
|
|
|
import com.nimbusds.jose.JOSEException;
|
|
|
import com.nimbusds.jose.JWEAlgorithm;
|
|
@@ -13,19 +12,20 @@ import com.nimbusds.jose.crypto.ECDHEncrypter;
|
|
|
import com.nimbusds.jose.crypto.PasswordBasedDecrypter;
|
|
|
import com.nimbusds.jose.jwk.Curve;
|
|
|
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
|
|
|
-import com.nimbusds.jose.jwk.gen.JWKGenerator;
|
|
|
+import org.cryptomator.cryptolib.api.CryptoException;
|
|
|
import org.cryptomator.cryptolib.api.Masterkey;
|
|
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
|
+import java.security.Key;
|
|
|
import java.security.KeyFactory;
|
|
|
-import java.security.KeyPairGenerator;
|
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
import java.security.interfaces.ECPrivateKey;
|
|
|
import java.security.interfaces.ECPublicKey;
|
|
|
import java.security.spec.InvalidKeySpecException;
|
|
|
import java.security.spec.PKCS8EncodedKeySpec;
|
|
|
+import java.security.spec.X509EncodedKeySpec;
|
|
|
import java.util.Arrays;
|
|
|
import java.util.Base64;
|
|
|
import java.util.Map;
|
|
@@ -37,26 +37,16 @@ class JWEHelper {
|
|
|
private static final String JWE_PAYLOAD_KEY_FIELD = "key";
|
|
|
private static final String EC_ALG = "EC";
|
|
|
|
|
|
- private JWEHelper(){}
|
|
|
+ private JWEHelper() {}
|
|
|
+
|
|
|
public static JWEObject encryptUserKey(ECPrivateKey userKey, ECPublicKey deviceKey) {
|
|
|
- try {
|
|
|
- var encodedUserKey = Base64.getEncoder().encodeToString(userKey.getEncoded());
|
|
|
- var keyGen = new ECKeyGenerator(Curve.P_384);
|
|
|
- var ephemeralKeyPair = keyGen.generate();
|
|
|
- var header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM).ephemeralPublicKey(ephemeralKeyPair.toPublicJWK()).build();
|
|
|
- var payload = new Payload(Map.of(JWE_PAYLOAD_KEY_FIELD, encodedUserKey));
|
|
|
- var jwe = new JWEObject(header, payload);
|
|
|
- jwe.encrypt(new ECDHEncrypter(deviceKey));
|
|
|
- return jwe;
|
|
|
- } catch (JOSEException e) {
|
|
|
- throw new RuntimeException(e);
|
|
|
- }
|
|
|
+ return encryptKey(userKey, deviceKey);
|
|
|
}
|
|
|
|
|
|
public static ECPrivateKey decryptUserKey(JWEObject jwe, String setupCode) throws InvalidJweKeyException {
|
|
|
try {
|
|
|
jwe.decrypt(new PasswordBasedDecrypter(setupCode));
|
|
|
- return decodeUserKey(jwe);
|
|
|
+ return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, JWEHelper::decodeECPrivateKey);
|
|
|
} catch (JOSEException e) {
|
|
|
throw new InvalidJweKeyException(e);
|
|
|
}
|
|
@@ -65,17 +55,23 @@ class JWEHelper {
|
|
|
public static ECPrivateKey decryptUserKey(JWEObject jwe, ECPrivateKey deviceKey) throws InvalidJweKeyException {
|
|
|
try {
|
|
|
jwe.decrypt(new ECDHDecrypter(deviceKey));
|
|
|
- return decodeUserKey(jwe);
|
|
|
+ return readKey(jwe, JWE_PAYLOAD_KEY_FIELD, JWEHelper::decodeECPrivateKey);
|
|
|
} catch (JOSEException e) {
|
|
|
throw new InvalidJweKeyException(e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static ECPrivateKey decodeUserKey(JWEObject decryptedJwe) {
|
|
|
+ /**
|
|
|
+ * Attempts to decode a DER-encoded EC private key.
|
|
|
+ *
|
|
|
+ * @param encoded DER-encoded EC private key
|
|
|
+ * @return the decoded key
|
|
|
+ * @throws KeyDecodeFailedException On malformed input
|
|
|
+ */
|
|
|
+ public static ECPrivateKey decodeECPrivateKey(byte[] encoded) throws KeyDecodeFailedException {
|
|
|
try {
|
|
|
- var keySpec = readKey(decryptedJwe, JWE_PAYLOAD_KEY_FIELD, PKCS8EncodedKeySpec::new);
|
|
|
- var factory = KeyFactory.getInstance(EC_ALG);
|
|
|
- var privateKey = factory.generatePrivate(keySpec);
|
|
|
+ KeyFactory factory = KeyFactory.getInstance(EC_ALG);
|
|
|
+ var privateKey = factory.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
|
|
if (privateKey instanceof ECPrivateKey ecPrivateKey) {
|
|
|
return ecPrivateKey;
|
|
|
} else {
|
|
@@ -84,8 +80,49 @@ class JWEHelper {
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
|
throw new IllegalStateException(EC_ALG + " not supported");
|
|
|
} catch (InvalidKeySpecException e) {
|
|
|
- LOG.warn("Unexpected JWE payload: {}", decryptedJwe.getPayload());
|
|
|
- throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
|
|
|
+ throw new KeyDecodeFailedException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Attempts to decode a DER-encoded EC public key.
|
|
|
+ *
|
|
|
+ * @param encoded DER-encoded EC public key
|
|
|
+ * @return the decoded key
|
|
|
+ * @throws KeyDecodeFailedException On malformed input
|
|
|
+ */
|
|
|
+ public static ECPublicKey decodeECPublicKey(byte[] encoded) throws KeyDecodeFailedException {
|
|
|
+ try {
|
|
|
+ KeyFactory factory = KeyFactory.getInstance(EC_ALG);
|
|
|
+ var publicKey = factory.generatePublic(new X509EncodedKeySpec(encoded));
|
|
|
+ if (publicKey instanceof ECPublicKey ecPublicKey) {
|
|
|
+ return ecPublicKey;
|
|
|
+ } else {
|
|
|
+ throw new IllegalStateException(EC_ALG + " key factory not generating ECPublicKeys");
|
|
|
+ }
|
|
|
+ } catch (NoSuchAlgorithmException e) {
|
|
|
+ throw new IllegalStateException(EC_ALG + " not supported");
|
|
|
+ } catch (InvalidKeySpecException e) {
|
|
|
+ throw new KeyDecodeFailedException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public static JWEObject encryptVaultKey(Masterkey vaultKey, ECPublicKey userKey) {
|
|
|
+ return encryptKey(vaultKey, userKey);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static JWEObject encryptKey(Key key, ECPublicKey userKey) {
|
|
|
+ try {
|
|
|
+ var encodedVaultKey = Base64.getEncoder().encodeToString(key.getEncoded());
|
|
|
+ var keyGen = new ECKeyGenerator(Curve.P_384);
|
|
|
+ var ephemeralKeyPair = keyGen.generate();
|
|
|
+ var header = new JWEHeader.Builder(JWEAlgorithm.ECDH_ES, EncryptionMethod.A256GCM).ephemeralPublicKey(ephemeralKeyPair.toPublicJWK()).build();
|
|
|
+ var payload = new Payload(Map.of(JWE_PAYLOAD_KEY_FIELD, encodedVaultKey));
|
|
|
+ var jwe = new JWEObject(header, payload);
|
|
|
+ jwe.encrypt(new ECDHEncrypter(userKey));
|
|
|
+ return jwe;
|
|
|
+ } catch (JOSEException e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -108,12 +145,12 @@ class JWEHelper {
|
|
|
var keyBytes = new byte[0];
|
|
|
try {
|
|
|
if (fields.get(keyField) instanceof String key) {
|
|
|
- keyBytes = BaseEncoding.base64().decode(key);
|
|
|
+ keyBytes = Base64.getDecoder().decode(key);
|
|
|
return rawKeyFactory.apply(keyBytes);
|
|
|
} else {
|
|
|
throw new IllegalArgumentException("JWE payload doesn't contain field " + keyField);
|
|
|
}
|
|
|
- } catch (IllegalArgumentException e) {
|
|
|
+ } catch (IllegalArgumentException | KeyDecodeFailedException e) {
|
|
|
LOG.error("Unexpected JWE payload: {}", jwe.getPayload());
|
|
|
throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
|
|
|
} finally {
|
|
@@ -127,4 +164,11 @@ class JWEHelper {
|
|
|
super("Invalid key", cause);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ public static class KeyDecodeFailedException extends CryptoException {
|
|
|
+
|
|
|
+ public KeyDecodeFailedException(Throwable cause) {
|
|
|
+ super("Malformed key", cause);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|