Add vault format 8 support
This commit is contained in:
parent
6df05fd95b
commit
3fef796546
@ -6,7 +6,7 @@ allprojects {
|
|||||||
|
|
||||||
ext {
|
ext {
|
||||||
androidBuildToolsVersion = "29.0.2"
|
androidBuildToolsVersion = "29.0.2"
|
||||||
androidMinSdkVersion = 23
|
androidMinSdkVersion = 24
|
||||||
androidTargetSdkVersion = 29
|
androidTargetSdkVersion = 29
|
||||||
androidCompileSdkVersion = 29
|
androidCompileSdkVersion = 29
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ ext {
|
|||||||
// cloud provider libs
|
// cloud provider libs
|
||||||
|
|
||||||
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
|
// do not update to 1.4.0 until minsdk is 7.x (or desugaring works better) otherwise it will crash on 6.x
|
||||||
cryptolibVersion = '1.3.0'
|
cryptolibVersion = '2.0.0-beta6'
|
||||||
|
|
||||||
dropboxVersion = '3.2.0'
|
dropboxVersion = '3.2.0'
|
||||||
|
|
||||||
|
@ -30,10 +30,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public DirType root(CloudType cloud) throws BackendException {
|
public DirType root(CloudType cloud) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.root(cloud);
|
return delegate.root(cloud);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -43,10 +40,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public DirType resolve(CloudType cloud, String path) throws BackendException {
|
public DirType resolve(CloudType cloud, String path) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.resolve(cloud, path);
|
return delegate.resolve(cloud, path);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -56,10 +50,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public FileType file(DirType parent, String name) throws BackendException {
|
public FileType file(DirType parent, String name) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.file(parent, name);
|
return delegate.file(parent, name);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -69,10 +60,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public FileType file(DirType parent, String name, Optional<Long> size) throws BackendException {
|
public FileType file(DirType parent, String name, Optional<Long> size) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.file(parent, name, size);
|
return delegate.file(parent, name, size);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -82,10 +70,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public DirType folder(DirType parent, String name) throws BackendException {
|
public DirType folder(DirType parent, String name) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.folder(parent, name);
|
return delegate.folder(parent, name);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -95,10 +80,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public boolean exists(NodeType node) throws BackendException {
|
public boolean exists(NodeType node) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.exists(node);
|
return delegate.exists(node);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -108,10 +90,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public List<? extends CloudNode> list(DirType folder) throws BackendException {
|
public List<? extends CloudNode> list(DirType folder) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.list(folder);
|
return delegate.list(folder);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -121,10 +100,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public DirType create(DirType folder) throws BackendException {
|
public DirType create(DirType folder) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.create(folder);
|
return delegate.create(folder);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -134,10 +110,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public DirType move(DirType source, DirType target) throws BackendException {
|
public DirType move(DirType source, DirType target) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.move(source, target);
|
return delegate.move(source, target);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -147,10 +120,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public FileType move(FileType source, FileType target) throws BackendException {
|
public FileType move(FileType source, FileType target) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.move(source, target);
|
return delegate.move(source, target);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -160,10 +130,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public FileType write(FileType file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
public FileType write(FileType file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.write(file, data, progressAware, replace, size);
|
return delegate.write(file, data, progressAware, replace, size);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -173,10 +140,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public void read(FileType file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
public void read(FileType file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
|
||||||
try {
|
try {
|
||||||
delegate.read(file, encryptedTmpFile, data, progressAware);
|
delegate.read(file, encryptedTmpFile, data, progressAware);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -186,10 +150,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public void delete(NodeType node) throws BackendException {
|
public void delete(NodeType node) throws BackendException {
|
||||||
try {
|
try {
|
||||||
delegate.delete(node);
|
delegate.delete(node);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -199,10 +160,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException {
|
public String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException {
|
||||||
try {
|
try {
|
||||||
return delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud);
|
return delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -212,10 +170,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
|||||||
public void logout(CloudType cloud) throws BackendException {
|
public void logout(CloudType cloud) throws BackendException {
|
||||||
try {
|
try {
|
||||||
delegate.logout(cloud);
|
delegate.logout(cloud);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException | RuntimeException e) {
|
||||||
throwWrappedIfRequired(e);
|
|
||||||
throw e;
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
throwWrappedIfRequired(e);
|
throwWrappedIfRequired(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -33,16 +33,19 @@ class CryptoCloudContentRepository implements CloudContentRepository<CryptoCloud
|
|||||||
throw new FatalBackendException(e);
|
throw new FatalBackendException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (cloud.getVault().getVersion()) {
|
switch (cloud.getVault().getFormat()) {
|
||||||
case 7:
|
case 7:
|
||||||
this.cryptoImpl = new CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7());
|
this.cryptoImpl = new CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7());
|
||||||
break;
|
break;
|
||||||
|
case 8:
|
||||||
|
this.cryptoImpl = new CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7(), cloud.getVault().getMaxFileNameLength());
|
||||||
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
case 5:
|
case 5:
|
||||||
this.cryptoImpl = new CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormatPre7());
|
this.cryptoImpl = new CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormatPre7());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException(format("No CryptoImpl for vault version %d.", cloud.getVault().getVersion()));
|
throw new IllegalStateException(format("No CryptoImpl for vault format %d.", cloud.getVault().getFormat()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,226 +1,95 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.Cryptors;
|
|
||||||
import org.cryptomator.cryptolib.api.Cryptor;
|
|
||||||
import org.cryptomator.cryptolib.api.CryptorProvider;
|
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
|
||||||
import org.cryptomator.cryptolib.api.KeyFile;
|
|
||||||
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
|
|
||||||
import org.cryptomator.domain.Cloud;
|
import org.cryptomator.domain.Cloud;
|
||||||
import org.cryptomator.domain.CloudFile;
|
import org.cryptomator.domain.CloudFile;
|
||||||
import org.cryptomator.domain.CloudFolder;
|
import org.cryptomator.domain.CloudFolder;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.exception.CancellationException;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
|
|
||||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
import org.cryptomator.util.Optional;
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.text.Normalizer;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import static android.R.attr.version;
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME;
|
||||||
import static java.text.Normalizer.normalize;
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_BACKUP_FILE_EXT;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MIN_VAULT_VERSION;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID;
|
|
||||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS;
|
|
||||||
import static org.cryptomator.domain.Vault.aCopyOf;
|
import static org.cryptomator.domain.Vault.aCopyOf;
|
||||||
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
||||||
|
import static org.cryptomator.util.Encodings.UTF_8;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class CryptoCloudFactory {
|
public class CryptoCloudFactory {
|
||||||
|
|
||||||
private final CryptorProvider cryptorProvider;
|
|
||||||
private final CloudContentRepository cloudContentRepository;
|
private final CloudContentRepository cloudContentRepository;
|
||||||
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CryptoCloudFactory( //
|
public CryptoCloudFactory(CloudContentRepository cloudContentRepository, //
|
||||||
CloudContentRepository cloudContentRepository, //
|
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) {
|
||||||
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, //
|
|
||||||
CryptorProvider cryptorProvider) {
|
|
||||||
this.cryptorProvider = cryptorProvider;
|
|
||||||
this.cloudContentRepository = cloudContentRepository;
|
this.cloudContentRepository = cloudContentRepository;
|
||||||
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
||||||
Cryptor cryptor = cryptorProvider.createNew();
|
cryptoCloudProvider(Optional.empty()).create(location, password);
|
||||||
try {
|
|
||||||
KeyFile keyFile = cryptor.writeKeysToMasterkeyFile(normalizePassword(password, version), MAX_VAULT_VERSION);
|
|
||||||
writeKeyFile(location, keyFile);
|
|
||||||
createRootFolder(location, cryptor);
|
|
||||||
} finally {
|
|
||||||
cryptor.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createRootFolder(CloudFolder location, Cryptor cryptor) throws BackendException {
|
|
||||||
CloudFolder dFolder = cloudContentRepository.folder(location, DATA_DIR_NAME);
|
|
||||||
dFolder = cloudContentRepository.create(dFolder);
|
|
||||||
String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID);
|
|
||||||
CloudFolder lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2));
|
|
||||||
lvl1Folder = cloudContentRepository.create(lvl1Folder);
|
|
||||||
CloudFolder lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2));
|
|
||||||
cloudContentRepository.create(lvl2Folder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
||||||
return new CryptoCloud(aCopyOf(vault).build());
|
return new CryptoCloud(aCopyOf(vault).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vault unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException {
|
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
||||||
return unlock(createUnlockToken(vault), password, cancelledFlag);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vault unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException {
|
|
||||||
UnlockTokenImpl impl = (UnlockTokenImpl) token;
|
|
||||||
Cryptor cryptor = cryptorFor(impl.getKeyFile(), password);
|
|
||||||
|
|
||||||
if (cancelledFlag.get()) {
|
|
||||||
throw new CancellationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
Vault vault = aCopyOf(token.getVault()) //
|
|
||||||
.withVersion(impl.getKeyFile().getVersion()) //
|
|
||||||
.withUnlocked(true) //
|
|
||||||
.build();
|
|
||||||
|
|
||||||
cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor);
|
|
||||||
|
|
||||||
return vault;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UnlockTokenImpl createUnlockToken(Vault vault) throws BackendException {
|
|
||||||
CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
CloudFolder vaultLocation = cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
||||||
return createUnlockToken(vault, vaultLocation);
|
String jwt = new String(readConfigFileData(vaultLocation), UTF_8);
|
||||||
|
return Optional.of(VaultConfig.decode(jwt));
|
||||||
}
|
}
|
||||||
|
|
||||||
private UnlockTokenImpl createUnlockToken(Vault vault, CloudFolder location) throws BackendException {
|
private byte[] readConfigFileData(CloudFolder location) throws BackendException {
|
||||||
byte[] keyFileData = readKeyFileData(location);
|
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||||
UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData);
|
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
|
||||||
assertVaultVersionIsSupported(unlockToken.getKeyFile().getVersion());
|
cloudContentRepository.read(vaultFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE);
|
||||||
return unlockToken;
|
return data.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cryptor cryptorFor(KeyFile keyFile, CharSequence password) {
|
public Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||||
return cryptorProvider.createFromKeyFile(keyFile, normalizePassword(password, keyFile.getVersion()), keyFile.getVersion());
|
return cryptoCloudProvider(unverifiedVaultConfig).unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CloudFolder vaultLocation(Vault vault) throws BackendException {
|
public Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||||
return cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
return cryptoCloudProvider(unverifiedVaultConfig).unlock(token, unverifiedVaultConfig, password, cancelledFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException {
|
public UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||||
try {
|
return cryptoCloudProvider(unverifiedVaultConfig).createUnlockToken(vault, unverifiedVaultConfig);
|
||||||
// create a cryptor, which checks the password, then destroy it immediately
|
|
||||||
cryptorFor(createUnlockToken(vault).getKeyFile(), password).destroy();
|
|
||||||
return true;
|
|
||||||
} catch (InvalidPassphraseException e) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||||
|
return cryptoCloudProvider(unverifiedVaultConfig).isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void lock(Vault vault) {
|
public void lock(Vault vault) {
|
||||||
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault);
|
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertVaultVersionIsSupported(int version) {
|
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
|
||||||
if (version < MIN_VAULT_VERSION) {
|
cryptoCloudProvider(unverifiedVaultConfig).changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
|
||||||
throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION);
|
|
||||||
} else if (version > MAX_VAULT_VERSION) {
|
|
||||||
throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeKeyFile(CloudFolder location, KeyFile keyFile) throws BackendException {
|
private CryptoCloudProvider cryptoCloudProvider(Optional<UnverifiedVaultConfig> unverifiedVaultConfigOptional) {
|
||||||
byte[] data = keyFile.serialize();
|
if (unverifiedVaultConfigOptional.isPresent()) {
|
||||||
cloudContentRepository.write(masterkeyFile(location), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, false, data.length);
|
switch (unverifiedVaultConfigOptional.get().getKeyId().getScheme()) {
|
||||||
|
case MASTERKEY_SCHEME: {
|
||||||
|
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory);
|
||||||
}
|
}
|
||||||
|
default: throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
|
||||||
private byte[] readKeyFileData(CloudFolder location) throws BackendException {
|
|
||||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
|
||||||
cloudContentRepository.read(masterkeyFile(location), Optional.empty(), data, NO_OP_PROGRESS_AWARE);
|
|
||||||
return data.toByteArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CloudFile masterkeyFile(CloudFolder location) throws BackendException {
|
|
||||||
return cloudContentRepository.file(location, MASTERKEY_FILE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CloudFile masterkeyBackupFile(CloudFolder location, byte[] data) throws BackendException {
|
|
||||||
String fileName = MASTERKEY_FILE_NAME + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT;
|
|
||||||
return cloudContentRepository.file(location, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException {
|
|
||||||
CloudFolder vaultLocation = vaultLocation(vault);
|
|
||||||
ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream();
|
|
||||||
cloudContentRepository.read(masterkeyFile(vaultLocation), Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE);
|
|
||||||
|
|
||||||
byte[] data = dataOutputStream.toByteArray();
|
|
||||||
int vaultVersion = KeyFile.parse(data).getVersion();
|
|
||||||
|
|
||||||
createBackupMasterKeyFile(data, vaultLocation);
|
|
||||||
createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, vaultLocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createBackupMasterKeyFile(byte[] data, CloudFolder vaultLocation) throws BackendException {
|
|
||||||
cloudContentRepository.write( //
|
|
||||||
masterkeyBackupFile(vaultLocation, data), //
|
|
||||||
ByteArrayDataSource.from(data), //
|
|
||||||
NO_OP_PROGRESS_AWARE, //
|
|
||||||
true, //
|
|
||||||
data.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFolder vaultLocation) throws BackendException {
|
|
||||||
byte[] newMasterKeyFile = Cryptors.changePassphrase(cryptorProvider, //
|
|
||||||
data, //
|
|
||||||
normalizePassword(oldPassword, vaultVersion), //
|
|
||||||
normalizePassword(newPassword, vaultVersion));
|
|
||||||
cloudContentRepository.write(masterkeyFile(vaultLocation), //
|
|
||||||
ByteArrayDataSource.from(newMasterKeyFile), //
|
|
||||||
NO_OP_PROGRESS_AWARE, //
|
|
||||||
true, //
|
|
||||||
newMasterKeyFile.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CharSequence normalizePassword(CharSequence password, int vaultVersion) {
|
|
||||||
if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) {
|
|
||||||
return normalize(password, Normalizer.Form.NFC);
|
|
||||||
} else {
|
} else {
|
||||||
return password;
|
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class UnlockTokenImpl implements UnlockToken {
|
|
||||||
|
|
||||||
private final Vault vault;
|
|
||||||
private final byte[] keyFileData;
|
|
||||||
|
|
||||||
private UnlockTokenImpl(Vault vault, byte[] keyFileData) {
|
|
||||||
this.vault = vault;
|
|
||||||
this.keyFileData = keyFileData;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Vault getVault() {
|
|
||||||
return vault;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyFile getKeyFile() {
|
|
||||||
return KeyFile.parse(keyFileData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.CloudFolder;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
|
import org.cryptomator.domain.Vault;
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
|
public interface CryptoCloudProvider {
|
||||||
|
|
||||||
|
void create(CloudFolder location, CharSequence password) throws BackendException;
|
||||||
|
|
||||||
|
Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException;
|
||||||
|
|
||||||
|
UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException;
|
||||||
|
|
||||||
|
Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException;
|
||||||
|
|
||||||
|
boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException;
|
||||||
|
|
||||||
|
void lock(Vault vault);
|
||||||
|
|
||||||
|
void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException;
|
||||||
|
|
||||||
|
}
|
@ -1,14 +1,26 @@
|
|||||||
package org.cryptomator.data.cloud.crypto;
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
class CryptoConstants {
|
public class CryptoConstants {
|
||||||
|
|
||||||
|
public static final String MASTERKEY_SCHEME = "masterkeyfile";
|
||||||
|
|
||||||
|
static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator";
|
||||||
|
|
||||||
static final String ROOT_DIR_ID = "";
|
static final String ROOT_DIR_ID = "";
|
||||||
static final String DATA_DIR_NAME = "d";
|
static final String DATA_DIR_NAME = "d";
|
||||||
static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator";
|
static final String VAULT_FILE_NAME = "vault.cryptomator";
|
||||||
static final String MASTERKEY_BACKUP_FILE_EXT = ".bkup";
|
static final String MASTERKEY_BACKUP_FILE_EXT = ".bkup";
|
||||||
|
|
||||||
static final int MAX_VAULT_VERSION = 7;
|
static final int DEFAULT_MASTERKEY_FILE_VERSION = 999;
|
||||||
|
static final int MAX_VAULT_VERSION = 8;
|
||||||
|
static final int MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG = 7;
|
||||||
static final int VERSION_WITH_NORMALIZED_PASSWORDS = 6;
|
static final int VERSION_WITH_NORMALIZED_PASSWORDS = 6;
|
||||||
static final int MIN_VAULT_VERSION = 5;
|
static final int MIN_VAULT_VERSION = 5;
|
||||||
|
|
||||||
|
static final int DEFAULT_MAX_FILE_NAME = 220;
|
||||||
|
|
||||||
|
static final byte[] PEPPER = new byte[0];
|
||||||
|
|
||||||
|
static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_CTRMAC;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -54,18 +54,20 @@ abstract class CryptoImplDecorator {
|
|||||||
final CloudContentRepository cloudContentRepository;
|
final CloudContentRepository cloudContentRepository;
|
||||||
final Context context;
|
final Context context;
|
||||||
final DirIdCache dirIdCache;
|
final DirIdCache dirIdCache;
|
||||||
|
final int maxFileNameLength;
|
||||||
|
|
||||||
private final Supplier<Cryptor> cryptor;
|
private final Supplier<Cryptor> cryptor;
|
||||||
private final CloudFolder storageLocation;
|
private final CloudFolder storageLocation;
|
||||||
|
|
||||||
private RootCryptoFolder root;
|
private RootCryptoFolder root;
|
||||||
|
|
||||||
CryptoImplDecorator(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
|
CryptoImplDecorator(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.cryptor = cryptor;
|
this.cryptor = cryptor;
|
||||||
this.cloudContentRepository = cloudContentRepository;
|
this.cloudContentRepository = cloudContentRepository;
|
||||||
this.storageLocation = storageLocation;
|
this.storageLocation = storageLocation;
|
||||||
this.dirIdCache = dirIdCache;
|
this.dirIdCache = dirIdCache;
|
||||||
|
this.maxFileNameLength = maxFileNameLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException;
|
abstract CryptoFolder folder(CryptoFolder cryptoParent, String cleartextName) throws BackendException;
|
||||||
|
@ -51,9 +51,8 @@ import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE
|
|||||||
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
|
||||||
import static org.cryptomator.util.Encodings.UTF_8;
|
import static org.cryptomator.util.Encodings.UTF_8;
|
||||||
|
|
||||||
final class CryptoImplVaultFormat7 extends CryptoImplDecorator {
|
class CryptoImplVaultFormat7 extends CryptoImplDecorator {
|
||||||
|
|
||||||
private static final int SHORT_NAMES_MAX_LENGTH = 220;
|
|
||||||
private static final String CLOUD_NODE_EXT = ".c9r";
|
private static final String CLOUD_NODE_EXT = ".c9r";
|
||||||
private static final String LONG_NODE_FILE_EXT = ".c9s";
|
private static final String LONG_NODE_FILE_EXT = ".c9s";
|
||||||
private static final String CLOUD_FOLDER_DIR_FILE_PRE = "dir";
|
private static final String CLOUD_FOLDER_DIR_FILE_PRE = "dir";
|
||||||
@ -65,7 +64,11 @@ final class CryptoImplVaultFormat7 extends CryptoImplDecorator {
|
|||||||
private static final BaseEncoding BASE64 = BaseEncoding.base64Url();
|
private static final BaseEncoding BASE64 = BaseEncoding.base64Url();
|
||||||
|
|
||||||
CryptoImplVaultFormat7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
|
CryptoImplVaultFormat7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
|
||||||
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache);
|
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, CryptoConstants.DEFAULT_MAX_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
CryptoImplVaultFormat7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) {
|
||||||
|
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, maxFileNameLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -82,7 +85,7 @@ final class CryptoImplVaultFormat7 extends CryptoImplDecorator {
|
|||||||
.fileNameCryptor() //
|
.fileNameCryptor() //
|
||||||
.encryptFilename(BASE64, name, dirIdInfo(cryptoFolder).getId().getBytes(UTF_8)) + CLOUD_NODE_EXT;
|
.encryptFilename(BASE64, name, dirIdInfo(cryptoFolder).getId().getBytes(UTF_8)) + CLOUD_NODE_EXT;
|
||||||
|
|
||||||
if (ciphertextName.length() > SHORT_NAMES_MAX_LENGTH) {
|
if (ciphertextName.length() > maxFileNameLength) {
|
||||||
ciphertextName = deflate(cryptoFolder, ciphertextName);
|
ciphertextName = deflate(cryptoFolder, ciphertextName);
|
||||||
}
|
}
|
||||||
return ciphertextName;
|
return ciphertextName;
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor;
|
||||||
|
import org.cryptomator.domain.CloudFolder;
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
|
import org.cryptomator.util.Supplier;
|
||||||
|
|
||||||
|
public class CryptoImplVaultFormat8 extends CryptoImplVaultFormat7 {
|
||||||
|
|
||||||
|
CryptoImplVaultFormat8(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) {
|
||||||
|
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, maxFileNameLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -36,17 +36,16 @@ import static org.cryptomator.util.Encodings.UTF_8;
|
|||||||
|
|
||||||
final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
|
final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
|
||||||
|
|
||||||
private static final int SHORT_NAMES_MAX_LENGTH = 129;
|
static final int MAX_FILE_NAME_LENGTH = 129;
|
||||||
private static final String DIR_PREFIX = "0";
|
private static final String DIR_PREFIX = "0";
|
||||||
private static final String SYMLINK_PREFIX = "1S";
|
private static final String SYMLINK_PREFIX = "1S";
|
||||||
private static final String LONG_NAME_FILE_EXT = ".lng";
|
private static final String LONG_NAME_FILE_EXT = ".lng";
|
||||||
private static final String METADATA_DIR_NAME = "m";
|
private static final String METADATA_DIR_NAME = "m";
|
||||||
|
|
||||||
private static final BaseNCodec BASE32 = new Base32();
|
private static final BaseNCodec BASE32 = new Base32();
|
||||||
private static final Pattern BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$");
|
private static final Pattern BASE32_ENCRYPTED_NAME_PATTERN = Pattern.compile("^(0|1S)?(([A-Z2-7]{8})*[A-Z2-7=]{8})$");
|
||||||
|
|
||||||
CryptoImplVaultFormatPre7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
|
CryptoImplVaultFormatPre7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
|
||||||
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache);
|
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, MAX_FILE_NAME_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -75,7 +74,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
|
|||||||
|
|
||||||
private String encryptName(CryptoFolder cryptoParent, String name, String prefix) throws BackendException {
|
private String encryptName(CryptoFolder cryptoParent, String name, String prefix) throws BackendException {
|
||||||
String ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(name, dirIdInfo(cryptoParent).getId().getBytes(UTF_8));
|
String ciphertextName = prefix + cryptor().fileNameCryptor().encryptFilename(name, dirIdInfo(cryptoParent).getId().getBytes(UTF_8));
|
||||||
if (ciphertextName.length() > SHORT_NAMES_MAX_LENGTH) {
|
if (ciphertextName.length() > maxFileNameLength) {
|
||||||
ciphertextName = deflate(ciphertextName);
|
ciphertextName = deflate(ciphertextName);
|
||||||
}
|
}
|
||||||
return ciphertextName;
|
return ciphertextName;
|
||||||
@ -140,7 +139,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
|
|||||||
if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
|
if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
|
||||||
try {
|
try {
|
||||||
ciphertextName = inflate(ciphertextName);
|
ciphertextName = inflate(ciphertextName);
|
||||||
if (ciphertextName.length() <= SHORT_NAMES_MAX_LENGTH) {
|
if (ciphertextName.length() <= maxFileNameLength) {
|
||||||
cloudFile = inflatePermanently(cloudFile, ciphertextName);
|
cloudFile = inflatePermanently(cloudFile, ciphertextName);
|
||||||
}
|
}
|
||||||
} catch (NoSuchCloudFileException e) {
|
} catch (NoSuchCloudFileException e) {
|
||||||
|
@ -0,0 +1,312 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor;
|
||||||
|
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
|
||||||
|
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||||
|
import org.cryptomator.domain.CloudFile;
|
||||||
|
import org.cryptomator.domain.CloudFolder;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
|
import org.cryptomator.domain.Vault;
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
import org.cryptomator.domain.exception.CancellationException;
|
||||||
|
import org.cryptomator.domain.exception.FatalBackendException;
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
|
import org.cryptomator.domain.usecases.cloud.ByteArrayDataSource;
|
||||||
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.text.Normalizer;
|
||||||
|
|
||||||
|
import static java.text.Normalizer.normalize;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_CIPHER_COMBO;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MASTERKEY_FILE_VERSION;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DEFAULT_MAX_FILE_NAME;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_BACKUP_FILE_EXT;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_FILE_NAME;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.MIN_VAULT_VERSION;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.PEPPER;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.ROOT_DIR_ID;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VERSION_WITH_NORMALIZED_PASSWORDS;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.VaultCipherCombo.SIV_CTRMAC;
|
||||||
|
import static org.cryptomator.domain.Vault.aCopyOf;
|
||||||
|
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
||||||
|
import static org.cryptomator.util.Encodings.UTF_8;
|
||||||
|
|
||||||
|
public class MasterkeyCryptoCloudProvider implements CryptoCloudProvider {
|
||||||
|
|
||||||
|
private final CloudContentRepository cloudContentRepository;
|
||||||
|
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
||||||
|
|
||||||
|
public MasterkeyCryptoCloudProvider(CloudContentRepository cloudContentRepository, //
|
||||||
|
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) {
|
||||||
|
this.cloudContentRepository = cloudContentRepository;
|
||||||
|
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
||||||
|
// 1. write masterkey:
|
||||||
|
Masterkey masterkey = Masterkey.generate(new SecureRandom());
|
||||||
|
try (ByteArrayOutputStream data = new ByteArrayOutputStream()) {
|
||||||
|
new MasterkeyFileAccess(PEPPER, new SecureRandom()).persist(masterkey, data, password, DEFAULT_MASTERKEY_FILE_VERSION);
|
||||||
|
cloudContentRepository.write(legacyMasterkeyFile(location), ByteArrayDataSource.from(data.toByteArray()), NO_OP_PROGRESS_AWARE, false, data.size());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FatalBackendException("Failed to write masterkey", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. initialize vault:
|
||||||
|
VaultConfig vaultConfig = new VaultConfig.VaultConfigBuilder() //
|
||||||
|
.vaultFormat(MAX_VAULT_VERSION) //
|
||||||
|
.cipherCombo(DEFAULT_CIPHER_COMBO) //
|
||||||
|
.keyId(URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME))) //
|
||||||
|
.maxFilenameLength(DEFAULT_MAX_FILE_NAME) //
|
||||||
|
.build();
|
||||||
|
|
||||||
|
byte[] encodedVaultConfig = vaultConfig.toToken(masterkey.getEncoded()).getBytes(UTF_8);
|
||||||
|
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
|
||||||
|
cloudContentRepository.write(vaultFile, ByteArrayDataSource.from(encodedVaultConfig), NO_OP_PROGRESS_AWARE, false, encodedVaultConfig.length);
|
||||||
|
|
||||||
|
// 3. create root folder:
|
||||||
|
createRootFolder(location, cryptorFor(masterkey, vaultConfig.getCipherCombo()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createRootFolder(CloudFolder location, Cryptor cryptor) throws BackendException {
|
||||||
|
CloudFolder dFolder = cloudContentRepository.folder(location, DATA_DIR_NAME);
|
||||||
|
dFolder = cloudContentRepository.create(dFolder);
|
||||||
|
String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID);
|
||||||
|
CloudFolder lvl1Folder = cloudContentRepository.folder(dFolder, rootDirHash.substring(0, 2));
|
||||||
|
lvl1Folder = cloudContentRepository.create(lvl1Folder);
|
||||||
|
CloudFolder lvl2Folder = cloudContentRepository.folder(lvl1Folder, rootDirHash.substring(2));
|
||||||
|
cloudContentRepository.create(lvl2Folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||||
|
return unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||||
|
UnlockTokenImpl impl = (UnlockTokenImpl) token;
|
||||||
|
try {
|
||||||
|
Masterkey masterkey = impl.getKeyFile(password);
|
||||||
|
|
||||||
|
int vaultFormat;
|
||||||
|
int maxFileNameLength;
|
||||||
|
Cryptor cryptor;
|
||||||
|
|
||||||
|
if (unverifiedVaultConfig.isPresent()) {
|
||||||
|
VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get());
|
||||||
|
vaultFormat = vaultConfig.getVaultFormat();
|
||||||
|
assertVaultVersionIsSupported(vaultConfig.getVaultFormat());
|
||||||
|
maxFileNameLength = vaultConfig.getMaxFilenameLength();
|
||||||
|
cryptor = cryptorFor(masterkey, vaultConfig.getCipherCombo());
|
||||||
|
} else {
|
||||||
|
vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData);
|
||||||
|
assertLegacyVaultVersionIsSupported(vaultFormat);
|
||||||
|
maxFileNameLength = vaultFormat > 6 ? CryptoConstants.DEFAULT_MAX_FILE_NAME : CryptoImplVaultFormatPre7.MAX_FILE_NAME_LENGTH;
|
||||||
|
cryptor = cryptorFor(masterkey, SIV_CTRMAC);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (cancelledFlag.get()) {
|
||||||
|
throw new CancellationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vault vault = aCopyOf(token.getVault()) //
|
||||||
|
.withUnlocked(true) //
|
||||||
|
.withFormat(vaultFormat) //
|
||||||
|
.withMaxFileNameLength(maxFileNameLength)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
cryptoCloudContentRepositoryFactory.registerCryptor(vault, cryptor);
|
||||||
|
|
||||||
|
return vault;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FatalBackendException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UnlockTokenImpl createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||||
|
CloudFolder vaultLocation = vaultLocation(vault);
|
||||||
|
if (unverifiedVaultConfig.isPresent()) {
|
||||||
|
return createUnlockToken(vault, masterkeyFile(vaultLocation, unverifiedVaultConfig.get()));
|
||||||
|
} else {
|
||||||
|
return createUnlockToken(vault, legacyMasterkeyFile(vaultLocation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CloudFile masterkeyFile(CloudFolder vaultLocation, UnverifiedVaultConfig unverifiedVaultConfig) throws BackendException {
|
||||||
|
String path = unverifiedVaultConfig.getKeyId().getSchemeSpecificPart();
|
||||||
|
// TODO / FIXME sanitize path and throw specific exception
|
||||||
|
//throw new UnsupportedMasterkeyLocationException(unverifiedVaultConfig);
|
||||||
|
return cloudContentRepository.file(vaultLocation, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CloudFile legacyMasterkeyFile(CloudFolder location) throws BackendException {
|
||||||
|
return cloudContentRepository.file(location, MASTERKEY_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UnlockTokenImpl createUnlockToken(Vault vault, CloudFile location) throws BackendException {
|
||||||
|
byte[] keyFileData = readKeyFileData(location);
|
||||||
|
UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData);
|
||||||
|
return unlockToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] readKeyFileData(CloudFile masterkeyFile) throws BackendException {
|
||||||
|
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||||
|
cloudContentRepository.read(masterkeyFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE);
|
||||||
|
return data.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cryptor cryptorFor(Masterkey keyFile, VaultCipherCombo vaultCipherCombo) {
|
||||||
|
return vaultCipherCombo.getCryptorProvider(new SecureRandom()).withKey(keyFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||||
|
try {
|
||||||
|
// create a cryptor, which checks the password, then destroy it immediately
|
||||||
|
Masterkey masterkey = createUnlockToken(vault, unverifiedVaultConfig).getKeyFile(password);
|
||||||
|
VaultCipherCombo vaultCipherCombo;
|
||||||
|
if(unverifiedVaultConfig.isPresent()) {
|
||||||
|
VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get());
|
||||||
|
assertVaultVersionIsSupported(vaultConfig.getVaultFormat());
|
||||||
|
vaultCipherCombo = vaultConfig.getCipherCombo();
|
||||||
|
} else {
|
||||||
|
int vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(masterkey.getEncoded());
|
||||||
|
assertLegacyVaultVersionIsSupported(vaultVersion);
|
||||||
|
vaultCipherCombo = SIV_CTRMAC;
|
||||||
|
}
|
||||||
|
cryptorFor(masterkey, vaultCipherCombo).destroy();
|
||||||
|
return true;
|
||||||
|
} catch (InvalidPassphraseException e) {
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FatalBackendException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void lock(Vault vault) {
|
||||||
|
cryptoCloudContentRepositoryFactory.deregisterCryptor(vault);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertVaultVersionIsSupported(int version) {
|
||||||
|
if (version < MIN_VAULT_VERSION) {
|
||||||
|
throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION);
|
||||||
|
} else if (version > MAX_VAULT_VERSION) {
|
||||||
|
throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertLegacyVaultVersionIsSupported(int version) {
|
||||||
|
if (version < MIN_VAULT_VERSION) {
|
||||||
|
throw new UnsupportedVaultFormatException(version, MIN_VAULT_VERSION);
|
||||||
|
} else if (version > MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG) {
|
||||||
|
throw new UnsupportedVaultFormatException(version, MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
|
||||||
|
CloudFolder vaultLocation = vaultLocation(vault);
|
||||||
|
ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
CloudFile masterkeyFile;
|
||||||
|
if (unverifiedVaultConfig.isPresent()) {
|
||||||
|
masterkeyFile = masterkeyFile(vaultLocation, unverifiedVaultConfig.get());
|
||||||
|
} else {
|
||||||
|
masterkeyFile = legacyMasterkeyFile(vaultLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudContentRepository.read(masterkeyFile, Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE);
|
||||||
|
byte[] data = dataOutputStream.toByteArray();
|
||||||
|
|
||||||
|
int vaultVersion;
|
||||||
|
if (unverifiedVaultConfig.isPresent()) {
|
||||||
|
vaultVersion = unverifiedVaultConfig.get().getVaultFormat();
|
||||||
|
assertVaultVersionIsSupported(vaultVersion);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
vaultVersion = MasterkeyFileAccess.readAllegedVaultVersion(data);
|
||||||
|
assertLegacyVaultVersionIsSupported(vaultVersion);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FatalBackendException("Failed to read legacy vault version", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createBackupMasterKeyFile(data, masterkeyFile);
|
||||||
|
createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, masterkeyFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CloudFolder vaultLocation(Vault vault) throws BackendException {
|
||||||
|
return cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createBackupMasterKeyFile(byte[] data, CloudFile masterkeyFile) throws BackendException {
|
||||||
|
cloudContentRepository.write(masterkeyBackupFile(masterkeyFile, data), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, true, data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CloudFile masterkeyBackupFile(CloudFile masterkeyFile, byte[] data) throws BackendException {
|
||||||
|
String fileName = masterkeyFile.getName() + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT;
|
||||||
|
return cloudContentRepository.file(masterkeyFile.getParent(), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFile masterkeyFile) throws BackendException {
|
||||||
|
try {
|
||||||
|
byte[] newMasterKeyFile = new MasterkeyFileAccess(PEPPER, new SecureRandom()) //
|
||||||
|
.changePassphrase(data, normalizePassword(oldPassword, vaultVersion), normalizePassword(newPassword, vaultVersion));
|
||||||
|
cloudContentRepository.write(masterkeyFile, //
|
||||||
|
ByteArrayDataSource.from(newMasterKeyFile), //
|
||||||
|
NO_OP_PROGRESS_AWARE, //
|
||||||
|
true, //
|
||||||
|
newMasterKeyFile.length);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new FatalBackendException("Failed to read legacy vault version", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharSequence normalizePassword(CharSequence password, int vaultVersion) {
|
||||||
|
if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) {
|
||||||
|
return normalize(password, Normalizer.Form.NFC);
|
||||||
|
} else {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UnlockTokenImpl implements UnlockToken {
|
||||||
|
|
||||||
|
private final Vault vault;
|
||||||
|
private final byte[] keyFileData;
|
||||||
|
|
||||||
|
private UnlockTokenImpl(Vault vault, byte[] keyFileData) {
|
||||||
|
this.vault = vault;
|
||||||
|
this.keyFileData = keyFileData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Vault getVault() {
|
||||||
|
return vault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Masterkey getKeyFile(CharSequence password) throws IOException {
|
||||||
|
return new MasterkeyFileAccess(PEPPER, new SecureRandom()).load(new ByteArrayInputStream(keyFileData), password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.Cryptors;
|
||||||
|
import org.cryptomator.cryptolib.api.CryptorProvider;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A combination of different ciphers and/or cipher modes in a Cryptomator vault.
|
||||||
|
*/
|
||||||
|
public enum VaultCipherCombo {
|
||||||
|
/**
|
||||||
|
* AES-SIV for file name encryption
|
||||||
|
* AES-CTR + HMAC for content encryption
|
||||||
|
*/
|
||||||
|
SIV_CTRMAC(Cryptors::version1),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES-SIV for file name encryption
|
||||||
|
* AES-GCM for content encryption
|
||||||
|
*/
|
||||||
|
SIV_GCM(Cryptors::version2);
|
||||||
|
|
||||||
|
private final Function<SecureRandom, CryptorProvider> cryptorProvider;
|
||||||
|
|
||||||
|
VaultCipherCombo(Function<SecureRandom, CryptorProvider> cryptorProvider) {
|
||||||
|
this.cryptorProvider = cryptorProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CryptorProvider getCryptorProvider(SecureRandom csprng) {
|
||||||
|
return cryptorProvider.apply(csprng);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,148 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto
|
||||||
|
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultVersionMismatchException
|
||||||
|
import java.net.URI
|
||||||
|
import java.security.Key
|
||||||
|
import java.util.UUID
|
||||||
|
import io.jsonwebtoken.Claims
|
||||||
|
import io.jsonwebtoken.JwsHeader
|
||||||
|
import io.jsonwebtoken.JwtException
|
||||||
|
import io.jsonwebtoken.Jwts
|
||||||
|
import io.jsonwebtoken.SigningKeyResolverAdapter
|
||||||
|
import io.jsonwebtoken.security.Keys
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
||||||
|
|
||||||
|
val keyId: URI
|
||||||
|
val id: String
|
||||||
|
val vaultFormat: Int
|
||||||
|
val cipherCombo: VaultCipherCombo
|
||||||
|
val maxFilenameLength: Int
|
||||||
|
|
||||||
|
fun toToken(rawKey: ByteArray): String {
|
||||||
|
return Jwts.builder()
|
||||||
|
.setHeaderParam(JSON_KEY_ID, keyId.toASCIIString()) //
|
||||||
|
.setId(id) //
|
||||||
|
.claim(JSON_KEY_VAULTFORMAT, vaultFormat) //
|
||||||
|
.claim(JSON_KEY_CIPHERCONFIG, cipherCombo.name) //
|
||||||
|
.claim(JSON_KEY_MAXFILENAMELEN, maxFilenameLength) //
|
||||||
|
.signWith(Keys.hmacShaKeyFor(rawKey)) //
|
||||||
|
.compact()
|
||||||
|
}
|
||||||
|
|
||||||
|
class VaultConfigBuilder {
|
||||||
|
internal var id: String = UUID.randomUUID().toString()
|
||||||
|
internal var vaultFormat = CryptoConstants.MAX_VAULT_VERSION;
|
||||||
|
internal var cipherCombo = VaultCipherCombo.SIV_CTRMAC
|
||||||
|
internal var maxFilenameLength = CryptoConstants.DEFAULT_MAX_FILE_NAME;
|
||||||
|
lateinit var keyId: URI
|
||||||
|
|
||||||
|
fun keyId(keyId: URI): VaultConfigBuilder {
|
||||||
|
this.keyId = keyId
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cipherCombo(cipherCombo: VaultCipherCombo): VaultConfigBuilder {
|
||||||
|
this.cipherCombo = cipherCombo
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun maxFilenameLength(maxFilenameLength: Int): VaultConfigBuilder {
|
||||||
|
this.maxFilenameLength = maxFilenameLength
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun id(id: String): VaultConfigBuilder {
|
||||||
|
this.id = id
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun vaultFormat(vaultFormat: Int): VaultConfigBuilder {
|
||||||
|
this.vaultFormat = vaultFormat
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): VaultConfig {
|
||||||
|
return VaultConfig(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val JSON_KEY_VAULTFORMAT = "format"
|
||||||
|
private const val JSON_KEY_CIPHERCONFIG = "cipherCombo"
|
||||||
|
private const val JSON_KEY_MAXFILENAMELEN = "maxFilenameLen"
|
||||||
|
private const val JSON_KEY_ID = "kid"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(VaultConfigLoadException::class)
|
||||||
|
fun decode(token: String): UnverifiedVaultConfig {
|
||||||
|
val unverifiedSigningKeyResolver = UnverifiedSigningKeyResolver()
|
||||||
|
|
||||||
|
// At this point we can't verify the signature because we don't have the masterkey yet.
|
||||||
|
try {
|
||||||
|
Jwts.parserBuilder().setSigningKeyResolver(unverifiedSigningKeyResolver).build().parse(token)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
return UnverifiedVaultConfig(token, unverifiedSigningKeyResolver.keyId, unverifiedSigningKeyResolver.vaultFormat)
|
||||||
|
}
|
||||||
|
throw VaultConfigLoadException("Failed to load vaultconfig")
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Throws(VaultKeyInvalidException::class, VaultVersionMismatchException::class, VaultConfigLoadException::class)
|
||||||
|
fun verify(rawKey: ByteArray, unverifiedVaultConfig: UnverifiedVaultConfig): VaultConfig {
|
||||||
|
return try {
|
||||||
|
val parser = Jwts //
|
||||||
|
.parserBuilder() //
|
||||||
|
.setSigningKey(rawKey) //
|
||||||
|
.require(JSON_KEY_VAULTFORMAT, unverifiedVaultConfig.vaultFormat) //
|
||||||
|
.build() //
|
||||||
|
.parseClaimsJws(unverifiedVaultConfig.jwt)
|
||||||
|
|
||||||
|
val vaultConfigBuilder = createVaultConfig() //
|
||||||
|
.keyId(unverifiedVaultConfig.keyId)
|
||||||
|
.id(parser.header[JSON_KEY_ID] as String) //
|
||||||
|
.cipherCombo(VaultCipherCombo.valueOf(parser.body.get(JSON_KEY_CIPHERCONFIG, String::class.java))) //
|
||||||
|
.vaultFormat(unverifiedVaultConfig.vaultFormat) //
|
||||||
|
.maxFilenameLength(parser.body[JSON_KEY_MAXFILENAMELEN] as Int)
|
||||||
|
|
||||||
|
VaultConfig(vaultConfigBuilder)
|
||||||
|
/*} catch (SignatureVerificationException e) {
|
||||||
|
throw new VaultKeyInvalidException();
|
||||||
|
} catch (InvalidClaimException e) {
|
||||||
|
throw new VaultVersionMismatchException("Vault config not for version " + expectedVaultFormat);
|
||||||
|
} catch (JWTVerificationException e) {
|
||||||
|
throw new VaultConfigLoadException("Failed to verify vault config: " + unverifiedConfig.getToken());
|
||||||
|
*/
|
||||||
|
} catch (e: JwtException) {
|
||||||
|
throw VaultConfigLoadException("Failed to verify vault config", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createVaultConfig(): VaultConfigBuilder {
|
||||||
|
return VaultConfigBuilder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UnverifiedSigningKeyResolver : SigningKeyResolverAdapter() {
|
||||||
|
lateinit var keyId: URI
|
||||||
|
var vaultFormat: Int by Delegates.notNull()
|
||||||
|
|
||||||
|
override fun resolveSigningKey(jwsHeader: JwsHeader<*>, claims: Claims): Key? {
|
||||||
|
keyId = URI.create(jwsHeader.keyId)
|
||||||
|
vaultFormat = claims[JSON_KEY_VAULTFORMAT] as Int
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
id = builder.id
|
||||||
|
keyId = builder.keyId
|
||||||
|
vaultFormat = builder.vaultFormat
|
||||||
|
cipherCombo = builder.cipherCombo
|
||||||
|
maxFilenameLength = builder.maxFilenameLength
|
||||||
|
}
|
||||||
|
}
|
@ -182,9 +182,7 @@ public class VaultEntity extends DatabaseEntity {
|
|||||||
this.position = position;
|
this.position = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** called by internal mechanisms, do not call yourself. */
|
||||||
* called by internal mechanisms, do not call yourself.
|
|
||||||
*/
|
|
||||||
@Generated(hash = 674742652)
|
@Generated(hash = 674742652)
|
||||||
public void __setDaoSession(DaoSession daoSession) {
|
public void __setDaoSession(DaoSession daoSession) {
|
||||||
this.daoSession = daoSession;
|
this.daoSession = daoSession;
|
||||||
|
@ -7,11 +7,13 @@ import org.cryptomator.data.db.mappers.CloudEntityMapper;
|
|||||||
import org.cryptomator.domain.Cloud;
|
import org.cryptomator.domain.Cloud;
|
||||||
import org.cryptomator.domain.CloudFolder;
|
import org.cryptomator.domain.CloudFolder;
|
||||||
import org.cryptomator.domain.CloudType;
|
import org.cryptomator.domain.CloudType;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.repository.CloudRepository;
|
import org.cryptomator.domain.repository.CloudRepository;
|
||||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -92,26 +94,30 @@ class CloudRepositoryImpl implements CloudRepository {
|
|||||||
return cryptoCloudFactory.decryptedViewOf(vault);
|
return cryptoCloudFactory.decryptedViewOf(vault);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
||||||
|
return cryptoCloudFactory.unverifiedVaultConfig(vault);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cloud unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException {
|
public Cloud unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||||
Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, password, cancelledFlag);
|
Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, unverifiedVaultConfig, password, cancelledFlag);
|
||||||
return decryptedViewOf(vaultWithVersion);
|
return decryptedViewOf(vaultWithVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cloud unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException {
|
public Cloud unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||||
Vault vaultWithVersion = cryptoCloudFactory.unlock(token, password, cancelledFlag);
|
Vault vaultWithVersion = cryptoCloudFactory.unlock(token, unverifiedVaultConfig, password, cancelledFlag);
|
||||||
return decryptedViewOf(vaultWithVersion);
|
return decryptedViewOf(vaultWithVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UnlockToken prepareUnlock(Vault vault) throws BackendException {
|
public UnlockToken prepareUnlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||||
return cryptoCloudFactory.createUnlockToken(vault);
|
return cryptoCloudFactory.createUnlockToken(vault, unverifiedVaultConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException {
|
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||||
return cryptoCloudFactory.isVaultPasswordValid(vault, password);
|
return cryptoCloudFactory.isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -121,8 +127,8 @@ class CloudRepositoryImpl implements CloudRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException {
|
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
|
||||||
cryptoCloudFactory.changePassword(vault, oldPassword, newPassword);
|
cryptoCloudFactory.changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
package org.cryptomator.data.repository;
|
package org.cryptomator.data.repository;
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.Cryptors;
|
|
||||||
import org.cryptomator.cryptolib.api.CryptorProvider;
|
|
||||||
import org.cryptomator.domain.repository.CloudContentRepository;
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
import org.cryptomator.domain.repository.CloudRepository;
|
import org.cryptomator.domain.repository.CloudRepository;
|
||||||
import org.cryptomator.domain.repository.UpdateCheckRepository;
|
import org.cryptomator.domain.repository.UpdateCheckRepository;
|
||||||
import org.cryptomator.domain.repository.VaultRepository;
|
import org.cryptomator.domain.repository.VaultRepository;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
@ -17,12 +13,6 @@ import dagger.Provides;
|
|||||||
@Module
|
@Module
|
||||||
public class RepositoryModule {
|
public class RepositoryModule {
|
||||||
|
|
||||||
@Singleton
|
|
||||||
@Provides
|
|
||||||
public CryptorProvider provideCryptorProvider() {
|
|
||||||
return Cryptors.version1(new SecureRandom());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Provides
|
@Provides
|
||||||
public CloudRepository provideCloudRepository(CloudRepositoryImpl cloudRepository) {
|
public CloudRepository provideCloudRepository(CloudRepositoryImpl cloudRepository) {
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package org.cryptomator.domain
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
class UnverifiedVaultConfig(val jwt: String, val keyId: URI, val vaultFormat: Int) : Serializable
|
@ -12,7 +12,8 @@ public class Vault implements Serializable {
|
|||||||
private final CloudType cloudType;
|
private final CloudType cloudType;
|
||||||
private final boolean unlocked;
|
private final boolean unlocked;
|
||||||
private final String password;
|
private final String password;
|
||||||
private final int version;
|
private final int format;
|
||||||
|
private final int maxFileNameLength;
|
||||||
private final int position;
|
private final int position;
|
||||||
|
|
||||||
private Vault(Builder builder) {
|
private Vault(Builder builder) {
|
||||||
@ -23,7 +24,8 @@ public class Vault implements Serializable {
|
|||||||
this.unlocked = builder.unlocked;
|
this.unlocked = builder.unlocked;
|
||||||
this.cloudType = builder.cloudType;
|
this.cloudType = builder.cloudType;
|
||||||
this.password = builder.password;
|
this.password = builder.password;
|
||||||
this.version = builder.version;
|
this.format = builder.format;
|
||||||
|
this.maxFileNameLength = builder.maxFileNameLength;
|
||||||
this.position = builder.position;
|
this.position = builder.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +42,8 @@ public class Vault implements Serializable {
|
|||||||
.withPath(vault.getPath()) //
|
.withPath(vault.getPath()) //
|
||||||
.withUnlocked(vault.isUnlocked()) //
|
.withUnlocked(vault.isUnlocked()) //
|
||||||
.withSavedPassword(vault.getPassword()) //
|
.withSavedPassword(vault.getPassword()) //
|
||||||
.withVersion(vault.getVersion()) //
|
.withFormat(vault.getFormat()) //
|
||||||
|
.withMaxFileNameLength(vault.getMaxFileNameLength()) //
|
||||||
.withPosition(vault.getPosition());
|
.withPosition(vault.getPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +75,12 @@ public class Vault implements Serializable {
|
|||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getVersion() {
|
public int getFormat() {
|
||||||
return version;
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxFileNameLength() {
|
||||||
|
return maxFileNameLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPosition() {
|
public int getPosition() {
|
||||||
@ -109,7 +116,8 @@ public class Vault implements Serializable {
|
|||||||
private CloudType cloudType;
|
private CloudType cloudType;
|
||||||
private boolean unlocked;
|
private boolean unlocked;
|
||||||
private String password;
|
private String password;
|
||||||
private int version = -1;
|
private int format = -1;
|
||||||
|
private int maxFileNameLength = -1;
|
||||||
private int position = -1;
|
private int position = -1;
|
||||||
|
|
||||||
private Builder() {
|
private Builder() {
|
||||||
@ -176,8 +184,13 @@ public class Vault implements Serializable {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder withVersion(int version) {
|
public Builder withFormat(int version) {
|
||||||
this.version = version;
|
this.format = version;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withMaxFileNameLength(int maxFileNameLength) {
|
||||||
|
this.maxFileNameLength = maxFileNameLength;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
package org.cryptomator.domain.exception.vaultconfig;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
|
||||||
|
public class UnsupportedMasterkeyLocationException extends BackendException {
|
||||||
|
|
||||||
|
UnverifiedVaultConfig unverifiedVaultConfig;
|
||||||
|
|
||||||
|
public UnsupportedMasterkeyLocationException(UnverifiedVaultConfig unverifiedVaultConfig) {
|
||||||
|
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsupportedMasterkeyLocationException(String message, JwtException e) {
|
||||||
|
super(message, e);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package org.cryptomator.domain.exception.vaultconfig;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
|
||||||
|
public class VaultConfigLoadException extends BackendException {
|
||||||
|
|
||||||
|
public VaultConfigLoadException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VaultConfigLoadException(String message, JwtException e) {
|
||||||
|
super(message, e);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package org.cryptomator.domain.exception.vaultconfig;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
|
||||||
|
public class VaultKeyInvalidException extends BackendException {
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.cryptomator.domain.exception.vaultconfig;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
|
||||||
|
public class VaultVersionMismatchException extends BackendException {
|
||||||
|
|
||||||
|
public VaultVersionMismatchException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,10 +3,12 @@ package org.cryptomator.domain.repository;
|
|||||||
import org.cryptomator.domain.Cloud;
|
import org.cryptomator.domain.Cloud;
|
||||||
import org.cryptomator.domain.CloudFolder;
|
import org.cryptomator.domain.CloudFolder;
|
||||||
import org.cryptomator.domain.CloudType;
|
import org.cryptomator.domain.CloudType;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -24,16 +26,18 @@ public interface CloudRepository {
|
|||||||
|
|
||||||
Cloud decryptedViewOf(Vault vault) throws BackendException;
|
Cloud decryptedViewOf(Vault vault) throws BackendException;
|
||||||
|
|
||||||
boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException;
|
boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException;
|
||||||
|
|
||||||
void lock(Vault vault) throws BackendException;
|
void lock(Vault vault) throws BackendException;
|
||||||
|
|
||||||
void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException;
|
void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException;
|
||||||
|
|
||||||
UnlockToken prepareUnlock(Vault vault) throws BackendException;
|
Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException;
|
||||||
|
|
||||||
Cloud unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException;
|
UnlockToken prepareUnlock(Vault vault, Optional<UnverifiedVaultConfig> vaultFile) throws BackendException;
|
||||||
|
|
||||||
Cloud unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException;
|
Cloud unlock(UnlockToken token, Optional<UnverifiedVaultConfig> vaultFile, CharSequence password, Flag cancelledFlag) throws BackendException;
|
||||||
|
|
||||||
|
Cloud unlock(Vault vault, Optional<UnverifiedVaultConfig> vaultFile, CharSequence password, Flag cancelledFlag) throws BackendException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -37,8 +37,10 @@ public class DoLicenseCheck {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final Claims claims = Jwts //
|
final Claims claims = Jwts //
|
||||||
.parserBuilder().setSigningKey(getPublicKey()) //
|
.parserBuilder() //
|
||||||
.build().parseClaimsJws(license) //
|
.setSigningKey(getPublicKey()) //
|
||||||
|
.build() //
|
||||||
|
.parseClaimsJws(license) //
|
||||||
.getBody();
|
.getBody();
|
||||||
|
|
||||||
return claims::getSubject;
|
return claims::getSubject;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.cryptomator.domain.usecases.vault;
|
package org.cryptomator.domain.usecases.vault;
|
||||||
|
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
||||||
@ -8,6 +9,7 @@ import org.cryptomator.domain.exception.NoSuchVaultException;
|
|||||||
import org.cryptomator.domain.repository.CloudRepository;
|
import org.cryptomator.domain.repository.CloudRepository;
|
||||||
import org.cryptomator.generator.Parameter;
|
import org.cryptomator.generator.Parameter;
|
||||||
import org.cryptomator.generator.UseCase;
|
import org.cryptomator.generator.UseCase;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
import static org.cryptomator.util.ExceptionUtil.contains;
|
||||||
|
|
||||||
@ -16,23 +18,26 @@ class ChangePassword {
|
|||||||
|
|
||||||
private final CloudRepository cloudRepository;
|
private final CloudRepository cloudRepository;
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
|
private final Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||||
private final String oldPassword;
|
private final String oldPassword;
|
||||||
private final String newPassword;
|
private final String newPassword;;
|
||||||
|
|
||||||
public ChangePassword(CloudRepository cloudRepository, //
|
public ChangePassword(CloudRepository cloudRepository, //
|
||||||
@Parameter Vault vault, //
|
@Parameter Vault vault, //
|
||||||
|
@Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig, //
|
||||||
@Parameter String oldPassword, //
|
@Parameter String oldPassword, //
|
||||||
@Parameter String newPassword) {
|
@Parameter String newPassword) {
|
||||||
this.cloudRepository = cloudRepository;
|
this.cloudRepository = cloudRepository;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
|
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||||
this.oldPassword = oldPassword;
|
this.oldPassword = oldPassword;
|
||||||
this.newPassword = newPassword;
|
this.newPassword = newPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void execute() throws BackendException {
|
public void execute() throws BackendException {
|
||||||
try {
|
try {
|
||||||
if (cloudRepository.isVaultPasswordValid(vault, oldPassword)) {
|
if (cloudRepository.isVaultPasswordValid(vault, unverifiedVaultConfig, oldPassword)) {
|
||||||
cloudRepository.changePassword(vault, oldPassword, newPassword);
|
cloudRepository.changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidPassphraseException();
|
throw new InvalidPassphraseException();
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package org.cryptomator.domain.usecases.vault;
|
package org.cryptomator.domain.usecases.vault;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.repository.CloudRepository;
|
import org.cryptomator.domain.repository.CloudRepository;
|
||||||
import org.cryptomator.generator.Parameter;
|
import org.cryptomator.generator.Parameter;
|
||||||
import org.cryptomator.generator.UseCase;
|
import org.cryptomator.generator.UseCase;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
@UseCase
|
@UseCase
|
||||||
class CheckVaultPassword {
|
class CheckVaultPassword {
|
||||||
@ -12,15 +14,17 @@ class CheckVaultPassword {
|
|||||||
private final CloudRepository cloudRepository;
|
private final CloudRepository cloudRepository;
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
private final String password;
|
private final String password;
|
||||||
|
private final Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||||
|
|
||||||
public CheckVaultPassword(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter String password) {
|
public CheckVaultPassword(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter String password, @Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig) {
|
||||||
this.cloudRepository = cloudRepository;
|
this.cloudRepository = cloudRepository;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
|
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean execute() throws BackendException {
|
public Boolean execute() throws BackendException {
|
||||||
return cloudRepository.isVaultPasswordValid(vault, password);
|
return cloudRepository.isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
package org.cryptomator.domain.usecases.vault;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
|
import org.cryptomator.domain.Vault;
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
||||||
|
import org.cryptomator.domain.repository.CloudRepository;
|
||||||
|
import org.cryptomator.generator.Parameter;
|
||||||
|
import org.cryptomator.generator.UseCase;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
|
import static org.cryptomator.util.ExceptionUtil.contains;
|
||||||
|
|
||||||
|
@UseCase
|
||||||
|
public class GetUnverifiedVaultConfig {
|
||||||
|
|
||||||
|
private final CloudRepository cloudRepository;
|
||||||
|
private final Vault vault;
|
||||||
|
|
||||||
|
public GetUnverifiedVaultConfig(CloudRepository cloudRepository, @Parameter Vault vault) {
|
||||||
|
this.cloudRepository = cloudRepository;
|
||||||
|
this.vault = vault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<UnverifiedVaultConfig> execute() throws BackendException {
|
||||||
|
try {
|
||||||
|
return cloudRepository.unverifiedVaultConfig(vault);
|
||||||
|
} catch (BackendException e) {
|
||||||
|
if (contains(e, NoSuchCloudFileException.class)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package org.cryptomator.domain.usecases.vault;
|
package org.cryptomator.domain.usecases.vault;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
import org.cryptomator.domain.exception.NoSuchCloudFileException;
|
||||||
@ -7,6 +8,7 @@ import org.cryptomator.domain.exception.NoSuchVaultException;
|
|||||||
import org.cryptomator.domain.repository.CloudRepository;
|
import org.cryptomator.domain.repository.CloudRepository;
|
||||||
import org.cryptomator.generator.Parameter;
|
import org.cryptomator.generator.Parameter;
|
||||||
import org.cryptomator.generator.UseCase;
|
import org.cryptomator.generator.UseCase;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
import static org.cryptomator.util.ExceptionUtil.contains;
|
||||||
|
|
||||||
@ -15,15 +17,17 @@ class PrepareUnlock {
|
|||||||
|
|
||||||
private final CloudRepository cloudRepository;
|
private final CloudRepository cloudRepository;
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
|
private final Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||||
|
|
||||||
public PrepareUnlock(CloudRepository cloudRepository, @Parameter Vault vault) {
|
public PrepareUnlock(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig) {
|
||||||
this.cloudRepository = cloudRepository;
|
this.cloudRepository = cloudRepository;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
|
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UnlockToken execute() throws BackendException {
|
public UnlockToken execute() throws BackendException {
|
||||||
try {
|
try {
|
||||||
return cloudRepository.prepareUnlock(vault);
|
return cloudRepository.prepareUnlock(vault, unverifiedVaultConfig);
|
||||||
} catch (BackendException e) {
|
} catch (BackendException e) {
|
||||||
if (contains(e, NoSuchCloudFileException.class)) {
|
if (contains(e, NoSuchCloudFileException.class)) {
|
||||||
throw new NoSuchVaultException(vault, e);
|
throw new NoSuchVaultException(vault, e);
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
package org.cryptomator.domain.usecases.vault;
|
package org.cryptomator.domain.usecases.vault;
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
import org.cryptomator.domain.Cloud;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.repository.CloudRepository;
|
import org.cryptomator.domain.repository.CloudRepository;
|
||||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||||
import org.cryptomator.generator.Parameter;
|
import org.cryptomator.generator.Parameter;
|
||||||
import org.cryptomator.generator.UseCase;
|
import org.cryptomator.generator.UseCase;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
|
||||||
@UseCase
|
@UseCase
|
||||||
class UnlockVault {
|
class UnlockVaultUsingMasterkey {
|
||||||
|
|
||||||
private final CloudRepository cloudRepository;
|
private final CloudRepository cloudRepository;
|
||||||
private final VaultOrUnlockToken vaultOrUnlockToken;
|
private final VaultOrUnlockToken vaultOrUnlockToken;
|
||||||
|
private Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||||
private final String password;
|
private final String password;
|
||||||
|
|
||||||
private volatile boolean cancelled;
|
private volatile boolean cancelled;
|
||||||
@ -22,9 +25,10 @@ class UnlockVault {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public UnlockVault(CloudRepository cloudRepository, @Parameter VaultOrUnlockToken vaultOrUnlockToken, @Parameter String password) {
|
public UnlockVaultUsingMasterkey(CloudRepository cloudRepository, @Parameter VaultOrUnlockToken vaultOrUnlockToken, @Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig, @Parameter String password) {
|
||||||
this.cloudRepository = cloudRepository;
|
this.cloudRepository = cloudRepository;
|
||||||
this.vaultOrUnlockToken = vaultOrUnlockToken;
|
this.vaultOrUnlockToken = vaultOrUnlockToken;
|
||||||
|
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,9 +38,9 @@ class UnlockVault {
|
|||||||
|
|
||||||
public Cloud execute() throws BackendException {
|
public Cloud execute() throws BackendException {
|
||||||
if (vaultOrUnlockToken.getVault().isPresent()) {
|
if (vaultOrUnlockToken.getVault().isPresent()) {
|
||||||
return cloudRepository.unlock(vaultOrUnlockToken.getVault().get(), password, cancelledFlag);
|
return cloudRepository.unlock(vaultOrUnlockToken.getVault().get(), unverifiedVaultConfig, password, cancelledFlag);
|
||||||
} else {
|
} else {
|
||||||
return cloudRepository.unlock(vaultOrUnlockToken.getUnlockToken().get(), password, cancelledFlag);
|
return cloudRepository.unlock(vaultOrUnlockToken.getUnlockToken().get(), unverifiedVaultConfig, password, cancelledFlag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,17 @@
|
|||||||
package org.cryptomator.domain.usecases.vault;
|
package org.cryptomator.domain.usecases.vault;
|
||||||
|
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
import org.cryptomator.domain.Vault;
|
import org.cryptomator.domain.Vault;
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
import org.cryptomator.domain.repository.CloudRepository;
|
import org.cryptomator.domain.repository.CloudRepository;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
public class UnlockVaultTest {
|
public class UnlockVaultUsingMasterkeyTest {
|
||||||
|
|
||||||
private static final String A_STRING = "89dfhsjdhfjsd";
|
private static final String A_STRING = "89dfhsjdhfjsd";
|
||||||
|
|
||||||
@ -19,30 +21,33 @@ public class UnlockVaultTest {
|
|||||||
|
|
||||||
private CloudRepository cloudRepository;
|
private CloudRepository cloudRepository;
|
||||||
|
|
||||||
private UnlockVault inTest;
|
private UnlockVaultUsingMasterkey inTest;
|
||||||
|
|
||||||
|
private Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setup() {
|
public void setup() {
|
||||||
unlockToken = Mockito.mock(UnlockToken.class);
|
unlockToken = Mockito.mock(UnlockToken.class);
|
||||||
vault = Mockito.mock(Vault.class);
|
vault = Mockito.mock(Vault.class);
|
||||||
cloudRepository = Mockito.mock(CloudRepository.class);
|
cloudRepository = Mockito.mock(CloudRepository.class);
|
||||||
inTest = Mockito.mock(UnlockVault.class);
|
unverifiedVaultConfig = Mockito.mock(Optional.class);
|
||||||
|
inTest = Mockito.mock(UnlockVaultUsingMasterkey.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExecuteDelegatesToUnlockWhenInvokedWithVault() throws BackendException {
|
public void testExecuteDelegatesToUnlockWhenInvokedWithVault() throws BackendException {
|
||||||
inTest = new UnlockVault(cloudRepository, VaultOrUnlockToken.from(vault), A_STRING);
|
inTest = new UnlockVaultUsingMasterkey(cloudRepository, VaultOrUnlockToken.from(vault), unverifiedVaultConfig, A_STRING);
|
||||||
inTest.execute();
|
inTest.execute();
|
||||||
|
|
||||||
verify(cloudRepository).unlock(Mockito.eq(vault), Mockito.eq(A_STRING), Mockito.any());
|
verify(cloudRepository).unlock(Mockito.eq(vault), Mockito.any(), Mockito.eq(A_STRING), Mockito.any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testExecuteDelegatesToUnlockWhenInvokedWithUnlockToken() throws BackendException {
|
public void testExecuteDelegatesToUnlockWhenInvokedWithUnlockToken() throws BackendException {
|
||||||
inTest = new UnlockVault(cloudRepository, VaultOrUnlockToken.from(unlockToken), A_STRING);
|
inTest = new UnlockVaultUsingMasterkey(cloudRepository, VaultOrUnlockToken.from(unlockToken), unverifiedVaultConfig, A_STRING);
|
||||||
inTest.execute();
|
inTest.execute();
|
||||||
|
|
||||||
verify(cloudRepository).unlock(Mockito.eq(unlockToken), Mockito.eq(A_STRING), Mockito.any());
|
verify(cloudRepository).unlock(Mockito.eq(unlockToken), Mockito.any(), Mockito.eq(A_STRING), Mockito.any());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -15,8 +15,10 @@ class VaultModel(private val vault: Vault) : Serializable {
|
|||||||
get() = !vault.isUnlocked
|
get() = !vault.isUnlocked
|
||||||
val position: Int
|
val position: Int
|
||||||
get() = vault.position
|
get() = vault.position
|
||||||
val version: Int
|
val format: Int
|
||||||
get() = vault.version
|
get() = vault.format
|
||||||
|
val maxFileNameLength: Int
|
||||||
|
get() = vault.maxFileNameLength
|
||||||
|
|
||||||
fun toVault(): Vault {
|
fun toVault(): Vault {
|
||||||
return vault
|
return vault
|
||||||
|
@ -2,18 +2,21 @@ package org.cryptomator.presentation.presenter
|
|||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
|
import org.cryptomator.data.cloud.crypto.CryptoConstants
|
||||||
import org.cryptomator.domain.Cloud
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
import org.cryptomator.domain.Vault
|
import org.cryptomator.domain.Vault
|
||||||
import org.cryptomator.domain.di.PerView
|
import org.cryptomator.domain.di.PerView
|
||||||
import org.cryptomator.domain.exception.NetworkConnectionException
|
import org.cryptomator.domain.exception.NetworkConnectionException
|
||||||
import org.cryptomator.domain.exception.authentication.AuthenticationException
|
import org.cryptomator.domain.exception.authentication.AuthenticationException
|
||||||
import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase
|
import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.GetUnverifiedVaultConfigUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.LockVaultUseCase
|
import org.cryptomator.domain.usecases.vault.LockVaultUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase
|
import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase
|
import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.SaveVaultUseCase
|
import org.cryptomator.domain.usecases.vault.SaveVaultUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockToken
|
import org.cryptomator.domain.usecases.vault.UnlockToken
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase
|
import org.cryptomator.domain.usecases.vault.UnlockVaultUsingMasterkeyUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken
|
import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken
|
||||||
import org.cryptomator.generator.Callback
|
import org.cryptomator.generator.Callback
|
||||||
import org.cryptomator.generator.InjectIntent
|
import org.cryptomator.generator.InjectIntent
|
||||||
@ -27,6 +30,7 @@ import org.cryptomator.presentation.model.VaultModel
|
|||||||
import org.cryptomator.presentation.ui.activity.view.UnlockVaultView
|
import org.cryptomator.presentation.ui.activity.view.UnlockVaultView
|
||||||
import org.cryptomator.presentation.workflow.ActivityResult
|
import org.cryptomator.presentation.workflow.ActivityResult
|
||||||
import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler
|
import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler
|
||||||
|
import org.cryptomator.util.Optional
|
||||||
import org.cryptomator.util.SharedPreferencesHandler
|
import org.cryptomator.util.SharedPreferencesHandler
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -35,8 +39,9 @@ import timber.log.Timber
|
|||||||
@PerView
|
@PerView
|
||||||
class UnlockVaultPresenter @Inject constructor(
|
class UnlockVaultPresenter @Inject constructor(
|
||||||
private val changePasswordUseCase: ChangePasswordUseCase,
|
private val changePasswordUseCase: ChangePasswordUseCase,
|
||||||
|
private val getUnverifiedVaultConfigUseCase: GetUnverifiedVaultConfigUseCase,
|
||||||
private val lockVaultUseCase: LockVaultUseCase,
|
private val lockVaultUseCase: LockVaultUseCase,
|
||||||
private val unlockVaultUseCase: UnlockVaultUseCase,
|
private val unlockVaultUsingMasterkeyUseCase: UnlockVaultUsingMasterkeyUseCase,
|
||||||
private val prepareUnlockUseCase: PrepareUnlockUseCase,
|
private val prepareUnlockUseCase: PrepareUnlockUseCase,
|
||||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase,
|
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase,
|
||||||
private val saveVaultUseCase: SaveVaultUseCase,
|
private val saveVaultUseCase: SaveVaultUseCase,
|
||||||
@ -63,13 +68,34 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
if(intent.vaultAction() == UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD) {
|
||||||
|
view?.getEncryptedPasswordWithBiometricAuthentication(intent.vaultModel())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnverifiedVaultConfigUseCase
|
||||||
|
.withVault(intent.vaultModel().toVault())
|
||||||
|
.run(object : DefaultResultHandler<Optional<UnverifiedVaultConfig>>() {
|
||||||
|
override fun onSuccess(unverifiedVaultConfig: Optional<UnverifiedVaultConfig>) {
|
||||||
|
if (unverifiedVaultConfig.isAbsent || unverifiedVaultConfig.get().keyId.scheme == CryptoConstants.MASTERKEY_SCHEME) {
|
||||||
when (intent.vaultAction()) {
|
when (intent.vaultAction()) {
|
||||||
UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> view?.getEncryptedPasswordWithBiometricAuthentication(intent.vaultModel())
|
UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> {
|
||||||
UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> unlockVault(intent.vaultModel())
|
startedUsingPrepareUnlock = sharedPreferencesHandler.backgroundUnlockPreparation()
|
||||||
UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(intent.vaultModel())
|
pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig = unverifiedVaultConfig.orElse(null)
|
||||||
|
unlockVault(intent.vaultModel())
|
||||||
|
}
|
||||||
|
UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(intent.vaultModel(), unverifiedVaultConfig.orElse(null))
|
||||||
else -> TODO("Not yet implemented")
|
else -> TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
super.onError(e)
|
||||||
|
finishWithResult(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private fun unlockVault(vaultModel: VaultModel) {
|
private fun unlockVault(vaultModel: VaultModel) {
|
||||||
if (canUseBiometricOn(vaultModel)) {
|
if (canUseBiometricOn(vaultModel)) {
|
||||||
@ -109,18 +135,18 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
|
|
||||||
fun onUnlockCanceled() {
|
fun onUnlockCanceled() {
|
||||||
prepareUnlockUseCase.unsubscribe()
|
prepareUnlockUseCase.unsubscribe()
|
||||||
unlockVaultUseCase.cancel()
|
unlockVaultUsingMasterkeyUseCase.cancel()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startPrepareUnlockUseCase(vault: Vault) {
|
fun startPrepareUnlockUseCase(vault: Vault) {
|
||||||
pendingUnlock = null
|
|
||||||
prepareUnlockUseCase //
|
prepareUnlockUseCase //
|
||||||
.withVault(vault) //
|
.withVault(vault) //
|
||||||
|
.andUnverifiedVaultConfig(Optional.ofNullable(pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig))
|
||||||
.run(object : DefaultResultHandler<UnlockToken>() {
|
.run(object : DefaultResultHandler<UnlockToken>() {
|
||||||
override fun onSuccess(unlockToken: UnlockToken) {
|
override fun onSuccess(unlockToken: UnlockToken) {
|
||||||
if (!startedUsingPrepareUnlock && vault.password != null) {
|
if (!startedUsingPrepareUnlock && vault.password != null) {
|
||||||
doUnlock(unlockToken, vault.password)
|
doUnlock(unlockToken, vault.password, pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig)
|
||||||
} else {
|
} else {
|
||||||
unlockTokenObtained(unlockToken)
|
unlockTokenObtained(unlockToken)
|
||||||
}
|
}
|
||||||
@ -170,7 +196,7 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
.run(object : DefaultResultHandler<UnlockToken>() {
|
.run(object : DefaultResultHandler<UnlockToken>() {
|
||||||
override fun onSuccess(unlockToken: UnlockToken) {
|
override fun onSuccess(unlockToken: UnlockToken) {
|
||||||
if (!startedUsingPrepareUnlock && vault.password != null) {
|
if (!startedUsingPrepareUnlock && vault.password != null) {
|
||||||
doUnlock(unlockToken, vault.password)
|
doUnlock(unlockToken, vault.password, pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig)
|
||||||
} else {
|
} else {
|
||||||
unlockTokenObtained(unlockToken)
|
unlockTokenObtained(unlockToken)
|
||||||
}
|
}
|
||||||
@ -195,9 +221,10 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
pendingUnlockFor(vault.toVault())?.setPassword(password, this)
|
pendingUnlockFor(vault.toVault())?.setPassword(password, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doUnlock(token: UnlockToken, password: String) {
|
private fun doUnlock(token: UnlockToken, password: String, unverifiedVaultConfig: UnverifiedVaultConfig?) {
|
||||||
unlockVaultUseCase //
|
unlockVaultUsingMasterkeyUseCase //
|
||||||
.withVaultOrUnlockToken(VaultOrUnlockToken.from(token)) //
|
.withVaultOrUnlockToken(VaultOrUnlockToken.from(token)) //
|
||||||
|
.andUnverifiedVaultConfig(Optional.ofNullable(unverifiedVaultConfig)) //
|
||||||
.andPassword(password) //
|
.andPassword(password) //
|
||||||
.run(object : DefaultResultHandler<Cloud>() {
|
.run(object : DefaultResultHandler<Cloud>() {
|
||||||
override fun onSuccess(cloud: Cloud) {
|
override fun onSuccess(cloud: Cloud) {
|
||||||
@ -214,7 +241,7 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
|
|
||||||
private fun handleUnlockVaultSuccess(vault: Vault, cloud: Cloud, password: String) {
|
private fun handleUnlockVaultSuccess(vault: Vault, cloud: Cloud, password: String) {
|
||||||
lockVaultUseCase.withVault(vault).run(object : DefaultResultHandler<Vault>() {
|
lockVaultUseCase.withVault(vault).run(object : DefaultResultHandler<Vault>() {
|
||||||
override fun onFinished() {
|
override fun onSuccess(vault: Vault) {
|
||||||
finishWithResultAndExtra(cloud, PASSWORD, password)
|
finishWithResultAndExtra(cloud, PASSWORD, password)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -265,9 +292,10 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String) {
|
fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String) {
|
||||||
view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD))
|
view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD))
|
||||||
changePasswordUseCase.withVault(vaultModel.toVault()) //
|
changePasswordUseCase.withVault(vaultModel.toVault()) //
|
||||||
|
.andUnverifiedVaultConfig(Optional.ofNullable(unverifiedVaultConfig)) //
|
||||||
.andOldPassword(oldPassword) //
|
.andOldPassword(oldPassword) //
|
||||||
.andNewPassword(newPassword) //
|
.andNewPassword(newPassword) //
|
||||||
.run(object : DefaultResultHandler<Void?>() {
|
.run(object : DefaultResultHandler<Void?>() {
|
||||||
@ -287,7 +315,7 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
override fun onError(e: Throwable) {
|
override fun onError(e: Throwable) {
|
||||||
if (!authenticationExceptionHandler.handleAuthenticationException( //
|
if (!authenticationExceptionHandler.handleAuthenticationException( //
|
||||||
this@UnlockVaultPresenter, e, //
|
this@UnlockVaultPresenter, e, //
|
||||||
ActivityResultCallbacks.changePasswordAfterAuthentication(vaultModel.toVault(), oldPassword, newPassword))) {
|
ActivityResultCallbacks.changePasswordAfterAuthentication(vaultModel.toVault(), unverifiedVaultConfig, oldPassword, newPassword))) {
|
||||||
showError(e)
|
showError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -295,10 +323,10 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Callback
|
@Callback
|
||||||
fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault, oldPassword: String, newPassword: String) {
|
fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault, unverifiedVaultConfig: UnverifiedVaultConfig, oldPassword: String, newPassword: String) {
|
||||||
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
||||||
val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build()
|
val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build()
|
||||||
onChangePasswordClick(VaultModel(vaultWithUpdatedCloud), oldPassword, newPassword)
|
onChangePasswordClick(VaultModel(vaultWithUpdatedCloud), unverifiedVaultConfig, oldPassword, newPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveVaultAfterChangePasswordButFailedBiometricAuth(vault: Vault) {
|
fun saveVaultAfterChangePasswordButFailedBiometricAuth(vault: Vault) {
|
||||||
@ -317,6 +345,8 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
private var unlockToken: UnlockToken? = null
|
private var unlockToken: UnlockToken? = null
|
||||||
private var password: String? = null
|
private var password: String? = null
|
||||||
|
|
||||||
|
var unverifiedVaultConfig: UnverifiedVaultConfig? = null
|
||||||
|
|
||||||
fun setUnlockToken(unlockToken: UnlockToken?, presenter: UnlockVaultPresenter) {
|
fun setUnlockToken(unlockToken: UnlockToken?, presenter: UnlockVaultPresenter) {
|
||||||
this.unlockToken = unlockToken
|
this.unlockToken = unlockToken
|
||||||
continueIfComplete(presenter)
|
continueIfComplete(presenter)
|
||||||
@ -328,7 +358,7 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun continueIfComplete(presenter: UnlockVaultPresenter) {
|
open fun continueIfComplete(presenter: UnlockVaultPresenter) {
|
||||||
unlockToken?.let { token -> password?.let { password -> presenter.doUnlock(token, password) } }
|
unlockToken?.let { token -> password?.let { password -> presenter.doUnlock(token, password, unverifiedVaultConfig) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun belongsTo(vault: Vault): Boolean {
|
fun belongsTo(vault: Vault): Boolean {
|
||||||
@ -351,7 +381,7 @@ class UnlockVaultPresenter @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
unsubscribeOnDestroy(changePasswordUseCase, lockVaultUseCase, unlockVaultUseCase, prepareUnlockUseCase, removeStoredVaultPasswordsUseCase, saveVaultUseCase)
|
unsubscribeOnDestroy(changePasswordUseCase, getUnverifiedVaultConfigUseCase, lockVaultUseCase, unlockVaultUsingMasterkeyUseCase, prepareUnlockUseCase, removeStoredVaultPasswordsUseCase, saveVaultUseCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package org.cryptomator.presentation.ui.activity
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
import org.cryptomator.domain.Vault
|
import org.cryptomator.domain.Vault
|
||||||
import org.cryptomator.generator.Activity
|
import org.cryptomator.generator.Activity
|
||||||
import org.cryptomator.generator.InjectIntent
|
import org.cryptomator.generator.InjectIntent
|
||||||
@ -13,6 +14,7 @@ import org.cryptomator.presentation.ui.activity.view.UnlockVaultView
|
|||||||
import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog
|
import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog
|
import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
||||||
|
import org.cryptomator.presentation.ui.dialog.VaultNotFoundDialog
|
||||||
import org.cryptomator.presentation.ui.fragment.UnlockVaultFragment
|
import org.cryptomator.presentation.ui.fragment.UnlockVaultFragment
|
||||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
import org.cryptomator.presentation.util.BiometricAuthentication
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -21,7 +23,8 @@ import javax.inject.Inject
|
|||||||
class UnlockVaultActivity : BaseActivity(), //
|
class UnlockVaultActivity : BaseActivity(), //
|
||||||
UnlockVaultView, //
|
UnlockVaultView, //
|
||||||
BiometricAuthentication.Callback,
|
BiometricAuthentication.Callback,
|
||||||
ChangePasswordDialog.Callback {
|
ChangePasswordDialog.Callback,
|
||||||
|
VaultNotFoundDialog.Callback {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var presenter: UnlockVaultPresenter
|
lateinit var presenter: UnlockVaultPresenter
|
||||||
@ -99,11 +102,16 @@ class UnlockVaultActivity : BaseActivity(), //
|
|||||||
private fun unlockVaultFragment(): UnlockVaultFragment = //
|
private fun unlockVaultFragment(): UnlockVaultFragment = //
|
||||||
getCurrentFragment(R.id.fragmentContainer) as UnlockVaultFragment
|
getCurrentFragment(R.id.fragmentContainer) as UnlockVaultFragment
|
||||||
|
|
||||||
override fun showChangePasswordDialog(vaultModel: VaultModel) {
|
override fun showChangePasswordDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?) {
|
||||||
showDialog(ChangePasswordDialog.newInstance(vaultModel))
|
showDialog(ChangePasswordDialog.newInstance(vaultModel, unverifiedVaultConfig))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String) {
|
override fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String) {
|
||||||
presenter.onChangePasswordClick(vaultModel, oldPassword, newPassword)
|
presenter.onChangePasswordClick(vaultModel, unverifiedVaultConfig, oldPassword, newPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDeleteMissingVaultClicked(vault: Vault) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.cryptomator.presentation.ui.activity.view
|
package org.cryptomator.presentation.ui.activity.view
|
||||||
|
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
import org.cryptomator.presentation.model.VaultModel
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
||||||
|
|
||||||
@ -11,6 +12,6 @@ interface UnlockVaultView : View, EnterPasswordDialog.Callback {
|
|||||||
fun showBiometricAuthKeyInvalidatedDialog()
|
fun showBiometricAuthKeyInvalidatedDialog()
|
||||||
fun cancelBasicAuthIfRunning()
|
fun cancelBasicAuthIfRunning()
|
||||||
fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean
|
fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean
|
||||||
fun showChangePasswordDialog(vaultModel: VaultModel)
|
fun showChangePasswordDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
import org.cryptomator.generator.Dialog
|
import org.cryptomator.generator.Dialog
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
import org.cryptomator.presentation.model.VaultModel
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
@ -23,7 +24,7 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
|||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
|
|
||||||
fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String)
|
fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
@ -33,10 +34,12 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
|||||||
changePasswordButton = dialog.getButton(android.app.Dialog.BUTTON_POSITIVE)
|
changePasswordButton = dialog.getButton(android.app.Dialog.BUTTON_POSITIVE)
|
||||||
changePasswordButton?.setOnClickListener {
|
changePasswordButton?.setOnClickListener {
|
||||||
val vaultModel = requireArguments().getSerializable(VAULT_ARG) as VaultModel
|
val vaultModel = requireArguments().getSerializable(VAULT_ARG) as VaultModel
|
||||||
|
val unverifiedVaultConfig = requireArguments().getSerializable(VAULT_CONFIG_ARG) as UnverifiedVaultConfig?
|
||||||
if (valid(et_old_password.text.toString(), //
|
if (valid(et_old_password.text.toString(), //
|
||||||
et_new_password.text.toString(), //
|
et_new_password.text.toString(), //
|
||||||
et_new_retype_password.text.toString())) {
|
et_new_retype_password.text.toString())) {
|
||||||
callback?.onChangePasswordClick(vaultModel, //
|
callback?.onChangePasswordClick(vaultModel, //
|
||||||
|
unverifiedVaultConfig, //
|
||||||
et_old_password.text.toString(), //
|
et_old_password.text.toString(), //
|
||||||
et_new_password.text.toString())
|
et_new_password.text.toString())
|
||||||
onWaitForResponse(et_old_password)
|
onWaitForResponse(et_old_password)
|
||||||
@ -93,9 +96,11 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val VAULT_ARG = "vault"
|
private const val VAULT_ARG = "vault"
|
||||||
fun newInstance(vaultModel: VaultModel): ChangePasswordDialog {
|
private const val VAULT_CONFIG_ARG = "vaultConfig"
|
||||||
|
fun newInstance(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?): ChangePasswordDialog {
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putSerializable(VAULT_ARG, vaultModel)
|
args.putSerializable(VAULT_ARG, vaultModel)
|
||||||
|
args.putSerializable(VAULT_CONFIG_ARG, unverifiedVaultConfig)
|
||||||
val fragment = ChangePasswordDialog()
|
val fragment = ChangePasswordDialog()
|
||||||
fragment.arguments = args
|
fragment.arguments = args
|
||||||
return fragment
|
return fragment
|
||||||
|
@ -91,7 +91,7 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
|
|||||||
.getString(R.string.screen_file_browser_add_existing_vault_extra_text)) //
|
.getString(R.string.screen_file_browser_add_existing_vault_extra_text)) //
|
||||||
.selectingFilesWithNameOnly("masterkey.cryptomator") //
|
.selectingFilesWithNameOnly("masterkey.cryptomator") //
|
||||||
.build()), //
|
.build()), //
|
||||||
SerializableResultCallbacks.masterkeyFileChosen());
|
SerializableResultCallbacks.cryptomatorFileChosen());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -112,7 +112,7 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Callback
|
@Callback
|
||||||
void masterkeyFileChosen(SerializableResult<CloudFileModel> result) {
|
void cryptomatorFileChosen(SerializableResult<CloudFileModel> result) {
|
||||||
CloudFileModel masterkeyFile = result.getResult();
|
CloudFileModel masterkeyFile = result.getResult();
|
||||||
state().masterkeyFile = masterkeyFile.toCloudNode();
|
state().masterkeyFile = masterkeyFile.toCloudNode();
|
||||||
presenter().getView().showProgress(ProgressModel.GENERIC);
|
presenter().getView().showProgress(ProgressModel.GENERIC);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user