mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-11-27 14:00:38 +00:00
use application-global device key that requires a system keychain
This commit is contained in:
parent
5922743f19
commit
fa86d890fe
15
pom.xml
15
pom.xml
@ -27,7 +27,8 @@
|
||||
<nonModularGroupIds>com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
|
||||
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>2.1.0-beta11</cryptomator.cryptofs.version>
|
||||
<cryptomator.cryptolib.version>2.1.0-beta2</cryptomator.cryptolib.version>
|
||||
<cryptomator.cryptofs.version>2.1.0-beta12</cryptomator.cryptofs.version>
|
||||
<cryptomator.integrations.version>1.0.0-rc1</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
|
||||
<cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>
|
||||
@ -58,6 +59,11 @@
|
||||
|
||||
<dependencies>
|
||||
<!-- Cryptomator Libs -->
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>cryptolib</artifactId>
|
||||
<version>${cryptomator.cryptolib.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>cryptofs</artifactId>
|
||||
@ -130,13 +136,6 @@
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- BouncyCastle -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
|
@ -4,6 +4,7 @@ import org.cryptomator.integrations.tray.TrayIntegrationProvider;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
|
||||
|
||||
module org.cryptomator.desktop {
|
||||
requires org.cryptomator.cryptolib;
|
||||
requires org.cryptomator.cryptofs;
|
||||
requires org.cryptomator.frontend.dokany;
|
||||
requires org.cryptomator.frontend.fuse;
|
||||
@ -25,8 +26,6 @@ module org.cryptomator.desktop {
|
||||
requires com.tobiasdiez.easybind;
|
||||
requires dagger;
|
||||
requires org.slf4j;
|
||||
requires org.bouncycastle.provider;
|
||||
requires org.bouncycastle.pkix;
|
||||
requires org.apache.commons.lang3;
|
||||
requires org.eclipse.jetty.server;
|
||||
requires org.eclipse.jetty.webapp;
|
||||
|
104
src/main/java/org/cryptomator/common/settings/DeviceKey.java
Normal file
104
src/main/java/org/cryptomator/common/settings/DeviceKey.java
Normal file
@ -0,0 +1,104 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.cryptolib.common.P384KeyPair;
|
||||
import org.cryptomator.cryptolib.common.Pkcs12Exception;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Singleton
|
||||
public class DeviceKey {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DeviceKey.class);
|
||||
private static final String KEYCHAIN_KEY = "cryptomator-device-p12";
|
||||
|
||||
private final KeychainManager keychainManager;
|
||||
private final Environment env;
|
||||
private final SecureRandom csprng;
|
||||
private final Supplier<P384KeyPair> keyPairSupplier;
|
||||
|
||||
@Inject
|
||||
public DeviceKey(KeychainManager keychainManager, Environment env, SecureRandom csprng) {
|
||||
this.keychainManager = keychainManager;
|
||||
this.env = env;
|
||||
this.csprng = csprng;
|
||||
this.keyPairSupplier = Suppliers.memoize(this::loadOrCreate);
|
||||
}
|
||||
|
||||
public P384KeyPair get() throws DeviceKeyRetrievalException {
|
||||
Preconditions.checkState(keychainManager.isSupported());
|
||||
return keyPairSupplier.get();
|
||||
}
|
||||
|
||||
private P384KeyPair loadOrCreate() throws DeviceKeyRetrievalException {
|
||||
char[] passphrase = null;
|
||||
try {
|
||||
passphrase = keychainManager.loadPassphrase(KEYCHAIN_KEY);
|
||||
if (passphrase != null) {
|
||||
return loadExistingKeyPair(passphrase);
|
||||
} else {
|
||||
passphrase = randomPassword();
|
||||
keychainManager.storePassphrase(KEYCHAIN_KEY, CharBuffer.wrap(passphrase));
|
||||
return createAndStoreNewKeyPair(passphrase);
|
||||
}
|
||||
} catch (KeychainAccessException e) {
|
||||
throw new DeviceKeyRetrievalException("Failed to access system keychain", e);
|
||||
} catch (Pkcs12Exception | IOException e) {
|
||||
throw new DeviceKeyRetrievalException("Failed to access .p12 file", e);
|
||||
} finally {
|
||||
if (passphrase != null) {
|
||||
Arrays.fill(passphrase, '\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private P384KeyPair loadExistingKeyPair(char[] passphrase) throws IOException {
|
||||
var p12File = env.getP12Path() //
|
||||
.filter(Files::isRegularFile) //
|
||||
.findFirst() //
|
||||
.orElseThrow(() -> new DeviceKeyRetrievalException("Missing .p12 file"));
|
||||
LOG.debug("Loading existing device key from {}", p12File);
|
||||
return P384KeyPair.load(p12File, passphrase);
|
||||
}
|
||||
|
||||
private P384KeyPair createAndStoreNewKeyPair(char[] passphrase) throws IOException {
|
||||
var p12File = env.getP12Path() //
|
||||
.findFirst() //
|
||||
.orElseThrow(() -> new DeviceKeyRetrievalException("No path for .p12 file configured"));
|
||||
var keyPair = P384KeyPair.generate();
|
||||
LOG.debug("Store new device key to {}", p12File);
|
||||
keyPair.store(p12File, passphrase);
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
private char[] randomPassword() {
|
||||
// this is a fast & easy attempt to create a random string:
|
||||
var uuid = new UUID(csprng.nextLong(), csprng.nextLong());
|
||||
return uuid.toString().toCharArray();
|
||||
}
|
||||
|
||||
public static class DeviceKeyRetrievalException extends RuntimeException {
|
||||
private DeviceKeyRetrievalException(String message) {
|
||||
super(message);
|
||||
}
|
||||
private DeviceKeyRetrievalException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -182,7 +182,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
|
||||
// 2. initialize vault:
|
||||
try {
|
||||
MasterkeyLoader loader = ignored -> masterkey.clone();
|
||||
MasterkeyLoader loader = ignored -> masterkey.copy();
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).withKeyLoader(loader).build();
|
||||
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
|
||||
|
||||
|
@ -15,7 +15,6 @@ public enum FxmlFile {
|
||||
HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), //
|
||||
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
|
||||
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
|
||||
HUB_P12("/fxml/hub_p12.fxml"), //
|
||||
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
|
||||
HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
|
||||
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||
|
@ -67,7 +67,7 @@ public class CheckExecutor {
|
||||
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
try (var masterkeyClone = masterkey.clone(); //
|
||||
try (var masterkeyClone = masterkey.copy(); //
|
||||
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
|
||||
c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
|
||||
Platform.runLater(() -> c.getResults().add(Result.create(diagnosis)));
|
||||
|
@ -50,7 +50,7 @@ class ResultFixApplier {
|
||||
|
||||
public void fix(DiagnosticResult diagnosis) {
|
||||
Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
|
||||
try (var masterkeyClone = masterkey.clone(); //
|
||||
try (var masterkeyClone = masterkey.copy(); //
|
||||
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
|
||||
diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
|
||||
} catch (Exception e) {
|
||||
|
@ -84,7 +84,7 @@ public class StartController implements FxController {
|
||||
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) {
|
||||
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
|
||||
vaultConfigRef.set(verifiedCfg);
|
||||
var old = masterkeyRef.getAndSet(masterkey.clone());
|
||||
var old = masterkeyRef.getAndSet(masterkey.copy());
|
||||
if (old != null) {
|
||||
old.destroy();
|
||||
}
|
||||
|
@ -1,135 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
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.nio.ByteOrder;
|
||||
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;
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.util.Arrays;
|
||||
|
||||
class EciesHelper {
|
||||
|
||||
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
|
||||
|
||||
private EciesHelper() {}
|
||||
|
||||
public static Masterkey decryptMasterkey(KeyPair deviceKey, EciesParams eciesParams) throws MasterkeyLoadingFailedException {
|
||||
var sharedSecret = ecdhAndKdf(deviceKey.getPrivate(), eciesParams.getEphemeralPublicKey(), GCM_KEY_SIZE + GCM_NONCE_SIZE);
|
||||
var cleartext = new byte[0];
|
||||
try (var kek = new DestroyableSecretKey(sharedSecret, 0, GCM_KEY_SIZE, "AES")) {
|
||||
var nonce = Arrays.copyOfRange(sharedSecret, GCM_KEY_SIZE, GCM_KEY_SIZE + 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[] sharedSecret = new byte[0];
|
||||
try {
|
||||
var keyAgreement = createKeyAgreement();
|
||||
keyAgreement.init(privateKey);
|
||||
keyAgreement.doPhase(publicKey, true);
|
||||
sharedSecret = keyAgreement.generateSecret();
|
||||
return kdf(sharedSecret, new byte[0], numBytes);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("Invalid keys", e);
|
||||
} finally {
|
||||
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 sharedInfo Additional authenticated data
|
||||
* @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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyAgreement createKeyAgreement() {
|
||||
try {
|
||||
return KeyAgreement.getInstance("ECDH");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("ECDH not supported");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,43 +1,14 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
/**
|
||||
* ECIES parameters required to decrypt the masterkey:
|
||||
* <ul>
|
||||
* <li><code>m</code> Encrypted Masterkey (base64url-encoded ciphertext)</li>
|
||||
* <li><code>epk</code> Ephemeral Public Key (base64url-encoded SPKI format)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* No separate tag required, since we use GCM for encryption.
|
||||
*/
|
||||
record EciesParams(String m, String epk) {
|
||||
|
||||
public byte[] getCiphertext() {
|
||||
return BaseEncoding.base64Url().decode(m());
|
||||
}
|
||||
|
||||
public ECPublicKey getEphemeralPublicKey() {
|
||||
try {
|
||||
byte[] keyBytes = BaseEncoding.base64Url().decode(epk());
|
||||
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
|
||||
if (key instanceof ECPublicKey k) {
|
||||
return k;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -46,12 +46,6 @@ public abstract class HubKeyLoadingModule {
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static AtomicReference<KeyPair> provideKeyPair() {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("bearerToken")
|
||||
@KeyLoadingScoped
|
||||
@ -83,13 +77,6 @@ public abstract class HubKeyLoadingModule {
|
||||
@StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS)
|
||||
abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttps(HubKeyLoadingStrategy strategy);
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_P12)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubP12LoadingScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_P12);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_AUTH_FLOW)
|
||||
@KeyLoadingScoped
|
||||
@ -97,7 +84,6 @@ public abstract class HubKeyLoadingModule {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_AUTH_FLOW);
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_RECEIVE_KEY)
|
||||
@KeyLoadingScoped
|
||||
@ -112,22 +98,6 @@ public abstract class HubKeyLoadingModule {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE);
|
||||
}
|
||||
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(P12Controller.class)
|
||||
abstract FxController bindP12Controller(P12Controller controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(P12LoadController.class)
|
||||
abstract FxController bindP12LoadController(P12LoadController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(P12CreateController.class)
|
||||
abstract FxController bindP12CreateController(P12CreateController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(AuthFlowController.class)
|
||||
|
@ -2,10 +2,12 @@ package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.settings.DeviceKey;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.cryptolib.common.Destroyables;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyHubAccess;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
@ -30,17 +32,17 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> p12LoadingScene;
|
||||
private final Lazy<Scene> authFlowScene;
|
||||
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction;
|
||||
private final AtomicReference<KeyPair> keyPairRef;
|
||||
private final DeviceKey deviceKey;
|
||||
private final AtomicReference<EciesParams> eciesParams;
|
||||
|
||||
@Inject
|
||||
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> eciesParams) {
|
||||
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction, DeviceKey deviceKey, AtomicReference<EciesParams> eciesParams) {
|
||||
this.window = window;
|
||||
this.p12LoadingScene = p12LoadingScene;
|
||||
this.authFlowScene = authFlowScene;
|
||||
this.userInteraction = userInteraction;
|
||||
this.keyPairRef = keyPairRef;
|
||||
this.deviceKey = deviceKey;
|
||||
this.eciesParams = eciesParams;
|
||||
}
|
||||
|
||||
@ -48,28 +50,23 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
|
||||
Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
|
||||
try {
|
||||
var keyPair = deviceKey.get();
|
||||
return switch (auth()) {
|
||||
case SUCCESS -> EciesHelper.decryptMasterkey(keyPairRef.get(), eciesParams.get());
|
||||
case SUCCESS -> MasterkeyHubAccess.decryptMasterkey(keyPair.getPrivate(), eciesParams.get().m(), eciesParams.get().epk());
|
||||
case FAILED -> throw new MasterkeyLoadingFailedException("failed to load keypair");
|
||||
case CANCELLED -> throw new UnlockCancelledException("User cancelled auth workflow");
|
||||
};
|
||||
} catch (DeviceKey.DeviceKeyRetrievalException e) {
|
||||
throw new MasterkeyLoadingFailedException("Failed to create or load device key pair", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new UnlockCancelledException("Loading interrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(boolean unlockedSuccessfully) {
|
||||
var keyPair = keyPairRef.getAndSet(null);
|
||||
if (keyPair != null) {
|
||||
Destroyables.destroySilently(keyPair.getPrivate());
|
||||
}
|
||||
}
|
||||
|
||||
private HubKeyLoadingModule.HubLoadingResult auth() throws InterruptedException {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(p12LoadingScene.get());
|
||||
window.setScene(authFlowScene.get());
|
||||
window.show();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
|
@ -1,108 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
|
||||
class P12AccessHelper {
|
||||
|
||||
private static final String EC_ALG = "EC";
|
||||
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";
|
||||
|
||||
private P12AccessHelper() {}
|
||||
|
||||
/**
|
||||
* Creates a new key pair and stores it in PKCS#12 format at the given path.
|
||||
*
|
||||
* @param p12File The path of the .p12 file
|
||||
* @param pw The password to protect the key material
|
||||
* @throws IOException In case of I/O errors
|
||||
* @throws MasterkeyLoadingFailedException If any cryptographic operation fails
|
||||
*/
|
||||
public static KeyPair createNew(Path p12File, char[] pw) throws IOException, MasterkeyLoadingFailedException {
|
||||
try {
|
||||
var keyPair = getKeyPairGenerator().generateKeyPair();
|
||||
var keyStore = getKeyStore();
|
||||
keyStore.load(null, pw);
|
||||
var cert = X509Helper.createSelfSignedCert(keyPair, SIGNATURE_ALG);
|
||||
var chain = new X509Certificate[]{cert};
|
||||
keyStore.setKeyEntry(KEYSTORE_ALIAS_KEY, keyPair.getPrivate(), pw, chain);
|
||||
keyStore.setCertificateEntry(KEYSTORE_ALIAS_CERT, cert);
|
||||
var tmpFile = p12File.resolveSibling(p12File.getFileName().toString() + ".tmp");
|
||||
try (var out = Files.newOutputStream(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
keyStore.store(out, pw);
|
||||
}
|
||||
Files.move(tmpFile, p12File, StandardCopyOption.REPLACE_EXISTING);
|
||||
return keyPair;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new MasterkeyLoadingFailedException("Failed to store PKCS12 file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a key pair from a PKCS#12 file located at the given path.
|
||||
*
|
||||
* @param p12File The path of the .p12 file
|
||||
* @param pw The password to protect the key material
|
||||
* @throws IOException In case of I/O errors
|
||||
* @throws InvalidPassphraseException If the supplied password is incorrect
|
||||
* @throws MasterkeyLoadingFailedException If any cryptographic operation fails
|
||||
*/
|
||||
public static KeyPair loadExisting(Path p12File, char[] pw) throws IOException, InvalidPassphraseException, MasterkeyLoadingFailedException {
|
||||
try (var in = Files.newInputStream(p12File, StandardOpenOption.READ)) {
|
||||
var keyStore = getKeyStore();
|
||||
keyStore.load(in, pw);
|
||||
var sk = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS_KEY, pw);
|
||||
var pk = keyStore.getCertificate(KEYSTORE_ALIAS_CERT).getPublicKey();
|
||||
return new KeyPair(pk, sk);
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
throw new InvalidPassphraseException();
|
||||
} catch (IOException e) {
|
||||
if (e.getCause() instanceof UnrecoverableKeyException) {
|
||||
throw new InvalidPassphraseException();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new MasterkeyLoadingFailedException("Failed to load PKCS12 file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyPairGenerator getKeyPairGenerator() {
|
||||
try {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(EC_ALG);
|
||||
keyGen.initialize(new ECGenParameterSpec(EC_CURVE_NAME));
|
||||
return keyGen;
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalStateException(EC_CURVE_NAME + " curve not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyStore getKeyStore() {
|
||||
try {
|
||||
return KeyStore.getInstance("PKCS12");
|
||||
} catch (KeyStoreException e) {
|
||||
throw new IllegalStateException("Every implementation of the Java platform is required to support PKCS12.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.nio.file.Files;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class P12Controller implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(P12Controller.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Environment env;
|
||||
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction;
|
||||
|
||||
@Inject
|
||||
public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction) {
|
||||
this.window = window;
|
||||
this.env = env;
|
||||
this.userInteraction = userInteraction;
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
// if not already interacted, mark this workflow as cancelled:
|
||||
if (userInteraction.awaitingInteraction().get()) {
|
||||
LOG.debug("P12 loading cancelled by user.");
|
||||
userInteraction.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED);
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public boolean isP12Present() {
|
||||
return env.getP12Path().anyMatch(Files::isRegularFile);
|
||||
}
|
||||
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.cryptolib.common.Destroyables;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class P12CreateController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(P12LoadController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Environment env;
|
||||
private final AtomicReference<KeyPair> keyPairRef;
|
||||
private final Lazy<Scene> authFlowScene;
|
||||
|
||||
private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
|
||||
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled);
|
||||
private final BooleanProperty readyToCreate = new SimpleBooleanProperty();
|
||||
|
||||
public NewPasswordController newPasswordController;
|
||||
|
||||
@Inject
|
||||
public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene) {
|
||||
this.window = window;
|
||||
this.env = env;
|
||||
this.keyPairRef = keyPairRef;
|
||||
this.authFlowScene = authFlowScene;
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
readyToCreate.bind(newPasswordController.goodPasswordProperty());
|
||||
newPasswordController.passwordField.requestFocus();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void cancel() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
newPasswordController.passwordField.wipe();
|
||||
newPasswordController.reenterField.wipe();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void create() {
|
||||
Preconditions.checkState(newPasswordController.goodPasswordProperty().get());
|
||||
char[] pw = newPasswordController.passwordField.copyChars();
|
||||
try {
|
||||
Path p12File = env.getP12Path().findFirst().orElseThrow(IllegalStateException::new);
|
||||
var keyPair = P12AccessHelper.createNew(p12File, pw);
|
||||
setKeyPair(keyPair);
|
||||
LOG.debug("Created .p12 file {}", p12File);
|
||||
window.setScene(authFlowScene.get());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load .p12 file.", e);
|
||||
// TODO
|
||||
} finally {
|
||||
Arrays.fill(pw, '\0');
|
||||
}
|
||||
}
|
||||
|
||||
private void setKeyPair(KeyPair keyPair) {
|
||||
var oldKeyPair = keyPairRef.getAndSet(keyPair);
|
||||
if (oldKeyPair != null) {
|
||||
Destroyables.destroySilently(oldKeyPair.getPrivate());
|
||||
}
|
||||
}
|
||||
/* Getter/Setter */
|
||||
|
||||
|
||||
public BooleanExpression userInteractionDisabledProperty() {
|
||||
return userInteractionDisabled;
|
||||
}
|
||||
|
||||
public boolean isUserInteractionDisabled() {
|
||||
return userInteractionDisabled.get();
|
||||
}
|
||||
|
||||
public ObjectExpression<ContentDisplay> unlockButtonContentDisplayProperty() {
|
||||
return unlockButtonContentDisplay;
|
||||
}
|
||||
|
||||
public ContentDisplay getUnlockButtonContentDisplay() {
|
||||
return userInteractionDisabled.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
|
||||
}
|
||||
|
||||
public BooleanProperty readyToCreateProperty() {
|
||||
return readyToCreate;
|
||||
}
|
||||
|
||||
public boolean isReadyToCreate() {
|
||||
return readyToCreate.get();
|
||||
}
|
||||
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.common.Destroyables;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class P12LoadController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(P12LoadController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Environment env;
|
||||
private final AtomicReference<KeyPair> keyPairRef;
|
||||
private final Lazy<Scene> authFlowScene;
|
||||
private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
|
||||
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled);
|
||||
|
||||
public NiceSecurePasswordField passwordField;
|
||||
|
||||
@Inject
|
||||
public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene) {
|
||||
this.window = window;
|
||||
this.env = env;
|
||||
this.keyPairRef = keyPairRef;
|
||||
this.authFlowScene = authFlowScene;
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
passwordField.requestFocus();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void cancel() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
passwordField.wipe();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void load() {
|
||||
char[] pw = passwordField.copyChars();
|
||||
try {
|
||||
Path p12File = env.getP12Path().filter(Files::isRegularFile).findFirst().orElseThrow(IllegalStateException::new);
|
||||
var keyPair = P12AccessHelper.loadExisting(p12File, pw);
|
||||
setKeyPair(keyPair);
|
||||
LOG.debug("Loaded .p12 file {}", p12File);
|
||||
window.setScene(authFlowScene.get());
|
||||
} catch (InvalidPassphraseException e) {
|
||||
LOG.warn("Invalid passphrase entered for .p12 file");
|
||||
Animations.createShakeWindowAnimation(window).playFromStart();
|
||||
// TODO
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to load .p12 file.", e);
|
||||
// TODO
|
||||
} finally {
|
||||
Arrays.fill(pw, '\0');
|
||||
}
|
||||
}
|
||||
|
||||
private void setKeyPair(KeyPair keyPair) {
|
||||
var oldKeyPair = keyPairRef.getAndSet(keyPair);
|
||||
if (oldKeyPair != null) {
|
||||
Destroyables.destroySilently(oldKeyPair.getPrivate());
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public BooleanExpression userInteractionDisabledProperty() {
|
||||
return userInteractionDisabled;
|
||||
}
|
||||
|
||||
public boolean isUserInteractionDisabled() {
|
||||
return userInteractionDisabled.get();
|
||||
}
|
||||
|
||||
public ObjectExpression<ContentDisplay> unlockButtonContentDisplayProperty() {
|
||||
return unlockButtonContentDisplay;
|
||||
}
|
||||
|
||||
public ContentDisplay getUnlockButtonContentDisplay() {
|
||||
return userInteractionDisabled.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
|
||||
}
|
||||
}
|
@ -59,7 +59,7 @@ public class ReceiveKeyController implements FxController {
|
||||
private final HttpClient httpClient;
|
||||
|
||||
@Inject
|
||||
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, AtomicReference<KeyPair> keyPairRef, @Named("bearerToken") AtomicReference<String> tokenRef, AtomicReference<EciesParams> eciesParamsRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, ErrorComponent.Builder errorComponent) {
|
||||
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("bearerToken") AtomicReference<String> tokenRef, AtomicReference<EciesParams> eciesParamsRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, ErrorComponent.Builder errorComponent) {
|
||||
this.window = window;
|
||||
this.bearerToken = Objects.requireNonNull(tokenRef.get());
|
||||
this.eciesParamsRef = eciesParamsRef;
|
||||
|
@ -1,6 +1,8 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import org.cryptomator.common.settings.DeviceKey;
|
||||
import org.cryptomator.cryptolib.common.P384KeyPair;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
@ -25,16 +27,16 @@ public class RegisterDeviceController implements FxController {
|
||||
private final Application application;
|
||||
private final Stage window;
|
||||
private final HubConfig hubConfig;
|
||||
private final KeyPair keyPair;
|
||||
private final P384KeyPair keyPair;
|
||||
private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
|
||||
private final String verificationCode;
|
||||
|
||||
@Inject
|
||||
public RegisterDeviceController(Application application, SecureRandom csprng, @KeyLoading Stage window, HubConfig hubConfig, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result) {
|
||||
public RegisterDeviceController(Application application, SecureRandom csprng, @KeyLoading Stage window, HubConfig hubConfig, DeviceKey deviceKey, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result) {
|
||||
this.application = application;
|
||||
this.window = window;
|
||||
this.hubConfig = hubConfig;
|
||||
this.keyPair = Objects.requireNonNull(keyPairRef.get());
|
||||
this.keyPair = Objects.requireNonNull(deviceKey.get());
|
||||
this.result = result;
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
this.verificationCode = String.format("%06d", csprng.nextInt(1_000_000));
|
||||
|
@ -1,81 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.sql.Date;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
class X509Helper {
|
||||
|
||||
private static final X500Name ISSUER = new X500Name("CN=Cryptomator");
|
||||
private static final X500Name SUBJECT = new X500Name("CN=Self Signed Cert");
|
||||
private static final ASN1ObjectIdentifier ASN1_SUBJECT_KEY_ID = new ASN1ObjectIdentifier("2.5.29.14");
|
||||
|
||||
private X509Helper() {}
|
||||
|
||||
/**
|
||||
* Creates a self-signed X509Certificate containing the public key and signed with the private key of a given key pair.
|
||||
*
|
||||
* @param keyPair A key pair
|
||||
* @param signatureAlg A signature algorithm suited for the given key pair (see <a href="https://docs.oracle.com/en/java/javase/16/docs/specs/security/standard-names.html#signature-algorithms">available algorithms</a>)
|
||||
* @return A self-signed X509Certificate
|
||||
* @throws CertificateException If certificate generation failed, e.g. because of unsupported algorithms
|
||||
*/
|
||||
public static X509Certificate createSelfSignedCert(KeyPair keyPair, String signatureAlg) throws CertificateException {
|
||||
try {
|
||||
X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder( //
|
||||
ISSUER, //
|
||||
randomSerialNo(), //
|
||||
Date.from(Instant.now()), //
|
||||
Date.from(Instant.now().plus(3650, ChronoUnit.DAYS)), //
|
||||
SUBJECT, //
|
||||
keyPair.getPublic());
|
||||
certificateBuilder.addExtension(ASN1_SUBJECT_KEY_ID, false, getX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
|
||||
var signer = new JcaContentSignerBuilder(signatureAlg).build(keyPair.getPrivate());
|
||||
var cert = certificateBuilder.build(signer);
|
||||
try (InputStream in = new ByteArrayInputStream(cert.getEncoded())) {
|
||||
return (X509Certificate) getCertFactory().generateCertificate(in);
|
||||
}
|
||||
} catch (IOException | OperatorCreationException e) {
|
||||
throw new CertificateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static BigInteger randomSerialNo() {
|
||||
return BigInteger.valueOf(UUID.randomUUID().getMostSignificantBits());
|
||||
}
|
||||
|
||||
private static JcaX509ExtensionUtils getX509ExtensionUtils() {
|
||||
try {
|
||||
return new JcaX509ExtensionUtils();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Every implementation of the Java platform is required to support SHA-1.");
|
||||
}
|
||||
}
|
||||
|
||||
private static CertificateFactory getCertFactory() {
|
||||
try {
|
||||
return CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Every implementation of the Java platform is required to support X.509.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.P12Controller"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
spacing="12">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<fx:include source="hub_p12_load.fxml" visible="${controller.p12Present}" managed="${controller.p12Present}"/>
|
||||
<fx:include source="hub_p12_create.fxml" visible="${!controller.p12Present}" managed="${!controller.p12Present}"/>
|
||||
</children>
|
||||
</VBox>
|
@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.P12CreateController"
|
||||
spacing="12">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<HBox spacing="12" VBox.vgrow="ALWAYS">
|
||||
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" cache="true">
|
||||
<Image url="@../img/bot/bot.png"/>
|
||||
</ImageView>
|
||||
<fx:include fx:id="newPassword" source="new_password.fxml" disable="${controller.userInteractionDisabled}"/>
|
||||
</HBox>
|
||||
|
||||
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CO">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
|
||||
<Button text="%generic.button.next" ButtonBar.buttonData="OK_DONE" defaultButton="true" onAction="#create" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${!controller.readyToCreate || controller.userInteractionDisabled}">
|
||||
<graphic>
|
||||
<FontAwesome5Spinner glyphSize="12"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
</children>
|
||||
</VBox>
|
@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||
<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.Hyperlink?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.keyloading.hub.P12LoadController"
|
||||
spacing="12">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<HBox spacing="12" VBox.vgrow="ALWAYS">
|
||||
<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" cache="true">
|
||||
<Image url="@../img/bot/bot.png"/>
|
||||
</ImageView>
|
||||
<VBox spacing="6" HBox.hgrow="ALWAYS">
|
||||
<Label text="TODO: Please enter your device secret to start communicating with Cryptomator Hub"/>
|
||||
<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
|
||||
<CheckBox fx:id="savePasswordCheckbox" text="TODO save password" disable="${controller.userInteractionDisabled}"/>
|
||||
<Hyperlink text="TODO: Click to reset your device secret (you need to re-apply for vault access)"/>
|
||||
</VBox>
|
||||
</HBox>
|
||||
|
||||
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CO">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
|
||||
<Button text="%generic.button.next" ButtonBar.buttonData="OK_DONE" defaultButton="true" onAction="#load" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${controller.userInteractionDisabled}">
|
||||
<graphic>
|
||||
<FontAwesome5Spinner glyphSize="12"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
</children>
|
||||
</VBox>
|
@ -11,7 +11,7 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see http://www.gnu.org/licenses/.
|
||||
|
||||
Cryptomator uses 46 third-party dependencies under the following licenses:
|
||||
Cryptomator uses 43 third-party dependencies under the following licenses:
|
||||
Apache License v2.0:
|
||||
- jffi (com.github.jnr:jffi:1.2.23 - http://github.com/jnr/jffi)
|
||||
- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
|
||||
@ -44,10 +44,6 @@ Cryptomator uses 46 third-party dependencies under the following licenses:
|
||||
- asm-commons (org.ow2.asm:asm-commons:7.1 - http://asm.ow2.org/)
|
||||
- asm-tree (org.ow2.asm:asm-tree:7.1 - http://asm.ow2.org/)
|
||||
- asm-util (org.ow2.asm:asm-util:7.1 - http://asm.ow2.org/)
|
||||
Bouncy Castle Licence:
|
||||
- Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk15on:1.69 - https://www.bouncycastle.org/java.html)
|
||||
- Bouncy Castle Provider (org.bouncycastle:bcprov-jdk15on:1.69 - https://www.bouncycastle.org/java.html)
|
||||
- Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk15on:1.69 - https://www.bouncycastle.org/java.html)
|
||||
Eclipse Public License - Version 1.0:
|
||||
- Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - https://eclipse.org/jetty/jetty-servlet-api)
|
||||
Eclipse Public License - Version 2.0:
|
||||
|
@ -1,60 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class P12AccessHelperTest {
|
||||
|
||||
@Test
|
||||
public void testCreate(@TempDir Path tmpDir) throws IOException {
|
||||
var p12File = tmpDir.resolve("test.p12");
|
||||
|
||||
var keyPair = P12AccessHelper.createNew(p12File, "asd".toCharArray());
|
||||
|
||||
Assertions.assertNotNull(keyPair);
|
||||
Assertions.assertTrue(Files.exists(p12File));
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class ExistingFile {
|
||||
|
||||
private Path p12File;
|
||||
|
||||
@BeforeEach
|
||||
public void setup(@TempDir Path tmpDir) throws IOException {
|
||||
p12File = tmpDir.resolve("test.p12");
|
||||
P12AccessHelper.createNew(p12File, "foo".toCharArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadWithWrongPassword() {
|
||||
Assertions.assertThrows(InvalidPassphraseException.class, () -> {
|
||||
P12AccessHelper.loadExisting(p12File, "bar".toCharArray());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoad() throws IOException {
|
||||
var keyPair = P12AccessHelper.loadExisting(p12File, "foo".toCharArray());
|
||||
|
||||
Assertions.assertNotNull(keyPair);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
|
||||
public class X509HelperTest {
|
||||
|
||||
@Test
|
||||
public void testCreateCert() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, InvalidAlgorithmParameterException {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
|
||||
keyGen.initialize(new ECGenParameterSpec("secp256r1"));
|
||||
var keyPair = keyGen.generateKeyPair();
|
||||
var cert = X509Helper.createSelfSignedCert(keyPair, "SHA256withECDSA");
|
||||
Assertions.assertNotNull(cert);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user