Merge branch 'feature/vault-format-8' into develop
This commit is contained in:
commit
cd986981c2
@ -6,7 +6,7 @@ allprojects {
|
|||||||
|
|
||||||
ext {
|
ext {
|
||||||
androidBuildToolsVersion = "29.0.3"
|
androidBuildToolsVersion = "29.0.3"
|
||||||
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-rc1'
|
||||||
|
|
||||||
awsAndroidSdkS3 = '2.23.0'
|
awsAndroidSdkS3 = '2.23.0'
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@ android {
|
|||||||
|
|
||||||
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
|
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
|
||||||
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
||||||
|
|
||||||
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -98,6 +100,11 @@ dependencies {
|
|||||||
// dagger
|
// dagger
|
||||||
annotationProcessor dependencies.daggerCompiler
|
annotationProcessor dependencies.daggerCompiler
|
||||||
implementation dependencies.dagger
|
implementation dependencies.dagger
|
||||||
|
|
||||||
|
api dependencies.jsonWebTokenApi
|
||||||
|
implementation dependencies.jsonWebTokenImpl
|
||||||
|
implementation dependencies.jsonWebTokenJson
|
||||||
|
|
||||||
// cloud
|
// cloud
|
||||||
implementation dependencies.awsAndroidS3
|
implementation dependencies.awsAndroidS3
|
||||||
implementation dependencies.dropbox
|
implementation dependencies.dropbox
|
||||||
@ -151,6 +158,7 @@ dependencies {
|
|||||||
testRuntimeOnly dependencies.junit4Engine
|
testRuntimeOnly dependencies.junit4Engine
|
||||||
|
|
||||||
testImplementation dependencies.mockito
|
testImplementation dependencies.mockito
|
||||||
|
testImplementation dependencies.mockitoInline
|
||||||
testImplementation dependencies.hamcrest
|
testImplementation dependencies.hamcrest
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,3 +169,16 @@ configurations {
|
|||||||
static def getApiKey(key) {
|
static def getApiKey(key) {
|
||||||
return System.getenv().getOrDefault(key, "")
|
return System.getenv().getOrDefault(key, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(Test) {
|
||||||
|
testLogging {
|
||||||
|
events "failed"
|
||||||
|
|
||||||
|
showExceptions true
|
||||||
|
exceptionFormat "full"
|
||||||
|
showCauses true
|
||||||
|
showStackTraces true
|
||||||
|
|
||||||
|
showStandardStreams = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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().getShorteningThreshold());
|
||||||
|
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,223 +1,97 @@
|
|||||||
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 java.security.SecureRandom;
|
||||||
|
|
||||||
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;
|
||||||
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoCloudContentRepositoryFactory.registerCryptor(impl.getVault(), cryptor);
|
|
||||||
|
|
||||||
return aCopyOf(token.getVault()) //
|
|
||||||
.withVersion(impl.getKeyFile().getVersion()) //
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||||
} catch (InvalidPassphraseException e) {
|
return cryptoCloudProvider(unverifiedVaultConfig).isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, secureRandom);
|
||||||
private byte[] readKeyFileData(CloudFolder location) throws BackendException {
|
}
|
||||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
default: throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
|
||||||
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, secureRandom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 shorteningThreshold;
|
||||||
|
|
||||||
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 shorteningThreshold) {
|
||||||
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.shorteningThreshold = shorteningThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 shorteningThreshold) {
|
||||||
|
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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() > shorteningThreshold) {
|
||||||
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 shorteningThreshold) {
|
||||||
|
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, shorteningThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 SHORTENING_THRESHOLD = 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, SHORTENING_THRESHOLD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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() > shorteningThreshold) {
|
||||||
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() <= shorteningThreshold) {
|
||||||
cloudFile = inflatePermanently(cloudFile, ciphertextName);
|
cloudFile = inflatePermanently(cloudFile, ciphertextName);
|
||||||
}
|
}
|
||||||
} catch (NoSuchCloudFileException e) {
|
} catch (NoSuchCloudFileException e) {
|
||||||
|
@ -0,0 +1,324 @@
|
|||||||
|
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.exception.vaultconfig.UnsupportedMasterkeyLocationException;
|
||||||
|
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;
|
||||||
|
private final SecureRandom secureRandom;
|
||||||
|
|
||||||
|
public MasterkeyCryptoCloudProvider(CloudContentRepository cloudContentRepository, //
|
||||||
|
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, //
|
||||||
|
SecureRandom secureRandom) {
|
||||||
|
this.cloudContentRepository = cloudContentRepository;
|
||||||
|
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
||||||
|
this.secureRandom = secureRandom;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
||||||
|
// Just for testing (id in VaultConfig is auto generated which makes sense while creating a vault but not for testing)
|
||||||
|
create(location, password, VaultConfig.createVaultConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible for testing
|
||||||
|
void create(CloudFolder location, CharSequence password, VaultConfig.VaultConfigBuilder vaultConfigBuilder) throws BackendException {
|
||||||
|
// 1. write masterkey:
|
||||||
|
Masterkey masterkey = Masterkey.generate(secureRandom);
|
||||||
|
try (ByteArrayOutputStream data = new ByteArrayOutputStream()) {
|
||||||
|
new MasterkeyFileAccess(PEPPER, 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 = vaultConfigBuilder //
|
||||||
|
.vaultFormat(MAX_VAULT_VERSION) //
|
||||||
|
.cipherCombo(DEFAULT_CIPHER_COMBO) //
|
||||||
|
.keyId(URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME))) //
|
||||||
|
.shorteningThreshold(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 shorteningThreshold;
|
||||||
|
Cryptor cryptor;
|
||||||
|
|
||||||
|
if (unverifiedVaultConfig.isPresent()) {
|
||||||
|
VaultConfig vaultConfig = VaultConfig.verify(masterkey.getEncoded(), unverifiedVaultConfig.get());
|
||||||
|
vaultFormat = vaultConfig.getVaultFormat();
|
||||||
|
assertVaultVersionIsSupported(vaultConfig.getVaultFormat());
|
||||||
|
shorteningThreshold = vaultConfig.getShorteningThreshold();
|
||||||
|
cryptor = cryptorFor(masterkey, vaultConfig.getCipherCombo());
|
||||||
|
} else {
|
||||||
|
vaultFormat = MasterkeyFileAccess.readAllegedVaultVersion(impl.keyFileData);
|
||||||
|
assertLegacyVaultVersionIsSupported(vaultFormat);
|
||||||
|
shorteningThreshold = vaultFormat > 6 ? CryptoConstants.DEFAULT_MAX_FILE_NAME : CryptoImplVaultFormatPre7.SHORTENING_THRESHOLD;
|
||||||
|
cryptor = cryptorFor(masterkey, SIV_CTRMAC);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (cancelledFlag.get()) {
|
||||||
|
throw new CancellationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vault vault = aCopyOf(token.getVault()) //
|
||||||
|
.withUnlocked(true) //
|
||||||
|
.withFormat(vaultFormat) //
|
||||||
|
.withShorteningThreshold(shorteningThreshold) //
|
||||||
|
.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();
|
||||||
|
if(!path.equals(MASTERKEY_FILE_NAME)) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visible for testing
|
||||||
|
Cryptor cryptorFor(Masterkey keyFile, VaultCipherCombo vaultCipherCombo) {
|
||||||
|
return vaultCipherCombo.getCryptorProvider(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
|
||||||
|
UnlockTokenImpl unlockToken = createUnlockToken(vault, unverifiedVaultConfig);
|
||||||
|
Masterkey masterkey = unlockToken.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(unlockToken.keyFileData);
|
||||||
|
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);
|
||||||
|
CloudFile masterkeyFile;
|
||||||
|
if (unverifiedVaultConfig.isPresent()) {
|
||||||
|
masterkeyFile = masterkeyFile(vaultLocation, unverifiedVaultConfig.get());
|
||||||
|
} else {
|
||||||
|
masterkeyFile = legacyMasterkeyFile(vaultLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream();
|
||||||
|
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, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class UnlockTokenImpl implements UnlockToken {
|
||||||
|
|
||||||
|
private final Vault vault;
|
||||||
|
private final byte[] keyFileData;
|
||||||
|
|
||||||
|
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,153 @@
|
|||||||
|
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.IncorrectClaimException
|
||||||
|
import io.jsonwebtoken.JwsHeader
|
||||||
|
import io.jsonwebtoken.JwtException
|
||||||
|
import io.jsonwebtoken.Jwts
|
||||||
|
import io.jsonwebtoken.MissingClaimException
|
||||||
|
import io.jsonwebtoken.SigningKeyResolverAdapter
|
||||||
|
import io.jsonwebtoken.security.Keys
|
||||||
|
import io.jsonwebtoken.security.SignatureException
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class VaultConfig private constructor(builder: VaultConfigBuilder) {
|
||||||
|
|
||||||
|
val keyId: URI
|
||||||
|
val id: String
|
||||||
|
val vaultFormat: Int
|
||||||
|
val cipherCombo: VaultCipherCombo
|
||||||
|
val shorteningThreshold: 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_SHORTENING_THRESHOLD, shorteningThreshold) //
|
||||||
|
.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 shorteningThreshold = 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 shorteningThreshold(shorteningThreshold: Int): VaultConfigBuilder {
|
||||||
|
this.shorteningThreshold = shorteningThreshold
|
||||||
|
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_SHORTENING_THRESHOLD = "shorteningThreshold"
|
||||||
|
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) //
|
||||||
|
.shorteningThreshold(parser.body[JSON_KEY_SHORTENING_THRESHOLD] as Int)
|
||||||
|
|
||||||
|
VaultConfig(vaultConfigBuilder)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
when (e) {
|
||||||
|
is MissingClaimException, is IncorrectClaimException -> throw VaultVersionMismatchException("Vault config not for version " + unverifiedVaultConfig.vaultFormat)
|
||||||
|
is SignatureException -> throw VaultKeyInvalidException()
|
||||||
|
is JwtException -> throw VaultConfigLoadException("Failed to verify vault config", e)
|
||||||
|
else -> throw VaultConfigLoadException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
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
|
||||||
|
shorteningThreshold = builder.shorteningThreshold
|
||||||
|
}
|
||||||
|
}
|
@ -195,7 +195,7 @@ class DropboxImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DropboxFile write(DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws AuthenticationException, DbxException, IOException, CloudNodeAlreadyExistsException {
|
public DropboxFile write(DropboxFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, long size) throws AuthenticationException, DbxException, IOException, CloudNodeAlreadyExistsException {
|
||||||
if (exists(file) && !replace) {
|
if (!replace && exists(file)) {
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ class LocalStorageImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LocalFile write(final CloudFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) throws IOException, BackendException {
|
public LocalFile write(final CloudFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) throws IOException, BackendException {
|
||||||
if (exists(file) && !replace) {
|
if (!replace && exists(file)) {
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,7 +427,7 @@ class LocalStorageAccessFrameworkImpl {
|
|||||||
|
|
||||||
progressAware.onProgress(Progress.started(UploadState.upload(file)));
|
progressAware.onProgress(Progress.started(UploadState.upload(file)));
|
||||||
Optional<Uri> fileUri = existingFileUri(file);
|
Optional<Uri> fileUri = existingFileUri(file);
|
||||||
if (fileUri.isPresent() && !replace) {
|
if (!replace && fileUri.isPresent()) {
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +242,7 @@ class OnedriveImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public OnedriveFile write(final OnedriveFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) throws BackendException {
|
public OnedriveFile write(final OnedriveFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) throws BackendException {
|
||||||
if (exists(file) && !replace) {
|
if (!replace && exists(file)) {
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +138,7 @@ class WebDavImpl {
|
|||||||
|
|
||||||
public WebDavFile write(final WebDavFile uploadFile, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) //
|
public WebDavFile write(final WebDavFile uploadFile, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) //
|
||||||
throws BackendException, IOException {
|
throws BackendException, IOException {
|
||||||
if (exists(uploadFile) && !replace) {
|
if (!replace && exists(uploadFile)) {
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -255,7 +255,7 @@ class GoogleDriveImpl {
|
|||||||
|
|
||||||
public GoogleDriveFile write(final GoogleDriveFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) //
|
public GoogleDriveFile write(final GoogleDriveFile file, DataSource data, final ProgressAware<UploadState> progressAware, boolean replace, final long size) //
|
||||||
throws IOException, BackendException {
|
throws IOException, BackendException {
|
||||||
if (exists(file) && !replace) {
|
if (!replace && exists(file)) {
|
||||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,336 @@
|
|||||||
|
package org.cryptomator.data.cloud.crypto;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.Cryptor;
|
||||||
|
import org.cryptomator.cryptolib.api.FileNameCryptor;
|
||||||
|
import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
|
||||||
|
import org.cryptomator.data.util.CopyStream;
|
||||||
|
import org.cryptomator.domain.Cloud;
|
||||||
|
import org.cryptomator.domain.CloudType;
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||||
|
import org.cryptomator.domain.Vault;
|
||||||
|
import org.cryptomator.domain.exception.BackendException;
|
||||||
|
import org.cryptomator.domain.repository.CloudContentRepository;
|
||||||
|
import org.cryptomator.domain.usecases.cloud.DataSource;
|
||||||
|
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||||
|
import org.cryptomator.util.Optional;
|
||||||
|
import org.hamcrest.MatcherAssert;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.cryptomator.cryptolib.api.Masterkey.SUBKEY_LEN_BYTES;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.DATA_DIR_NAME;
|
||||||
|
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.ROOT_DIR_ID;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
||||||
|
import static org.cryptomator.data.cloud.crypto.VaultCipherCombo.SIV_CTRMAC;
|
||||||
|
import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE;
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
class MasterkeyCryptoCloudProviderTest {
|
||||||
|
|
||||||
|
private final String masterkeyV8 = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}";
|
||||||
|
private final String masterkeyV7 = "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"hmacMasterKey\": \"D2kc+xBoAcVY+M7s74YBEy6l7ga2+Nz+HS5o0TQY3JMW1uQ5jTlLIQ==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}";
|
||||||
|
private final String vaultConfig = "eyJraWQiOiJtYXN0ZXJrZXlmaWxlOm1hc3RlcmtleS5jcnlwdG9tYXRvciIsImFsZyI6IkhTNTEyIn0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.Evt5KXS_35pm53DynIwL3qvXWF56UkfqDZKv12n7SD288jzcdvvmtvu5sQhhqvxU6CPL4Q9v3yFQ_lvBynyrYA";
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private Cloud cloud;
|
||||||
|
private CloudContentRepository cloudContentRepository;
|
||||||
|
private CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
||||||
|
private Vault vault;
|
||||||
|
private VaultConfig.VaultConfigBuilder vaultConfigBuilder;
|
||||||
|
private Cryptor cryptor;
|
||||||
|
private FileNameCryptor fileNameCryptor;
|
||||||
|
private SecureRandom secureRandom;
|
||||||
|
private MasterkeyCryptoCloudProvider inTest;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
context = Mockito.mock(Context.class);
|
||||||
|
cloud = Mockito.mock(Cloud.class);
|
||||||
|
cloudContentRepository = Mockito.mock(CloudContentRepository.class);
|
||||||
|
|
||||||
|
cryptoCloudContentRepositoryFactory = Mockito.mock(CryptoCloudContentRepositoryFactory.class);
|
||||||
|
|
||||||
|
vault = Mockito.mock(Vault.class);
|
||||||
|
vaultConfigBuilder = VaultConfig.createVaultConfig().id("");
|
||||||
|
|
||||||
|
cryptor = Mockito.mock(Cryptor.class);
|
||||||
|
fileNameCryptor = Mockito.mock(FileNameCryptor.class);
|
||||||
|
secureRandom = Mockito.mock(SecureRandom.class);
|
||||||
|
|
||||||
|
Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
|
||||||
|
|
||||||
|
byte[] key = new byte[SUBKEY_LEN_BYTES + SUBKEY_LEN_BYTES];
|
||||||
|
Mockito.doNothing().when(secureRandom).nextBytes(key);
|
||||||
|
|
||||||
|
inTest = Mockito.spy(new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("create(\"/foo\", \"foo\")")
|
||||||
|
public void testCreateVault() throws BackendException {
|
||||||
|
TestFolder rootFolder = new RootTestFolder(cloud);
|
||||||
|
TestFolder foo = new TestFolder(rootFolder, "foo", "/foo");
|
||||||
|
TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty());
|
||||||
|
TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty());
|
||||||
|
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile);
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile);
|
||||||
|
|
||||||
|
// 1. write masterkey
|
||||||
|
Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> {
|
||||||
|
DataSource in = invocationOnMock.getArgument(1);
|
||||||
|
String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining());
|
||||||
|
assertThat(masterKeyFileContent, is(masterkeyV8));
|
||||||
|
return invocationOnMock.getArgument(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. initialize vault
|
||||||
|
Mockito.when(cloudContentRepository.write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong())).thenAnswer(invocationOnMock -> {
|
||||||
|
DataSource in = invocationOnMock.getArgument(1);
|
||||||
|
String vaultConfigFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining());
|
||||||
|
assertThat(vaultConfigFileContent, is(vaultConfig));
|
||||||
|
return invocationOnMock.getArgument(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. create root folder
|
||||||
|
String rootDirHash = "KG6TFDGKXGZEGWRZOGTDFDF4YEGAZO6Q";
|
||||||
|
|
||||||
|
TestFolder dFolder = new TestFolder(foo, "d", "/foo/" + DATA_DIR_NAME);
|
||||||
|
TestFolder lvl1Dir = new TestFolder(dFolder, rootDirHash.substring(0, 2), "/foo/" + DATA_DIR_NAME + "/" + rootDirHash.substring(0, 2));
|
||||||
|
TestFolder lvl2Dir = new TestFolder(lvl1Dir, rootDirHash.substring(2), "/foo/" + DATA_DIR_NAME + "/" + rootDirHash.substring(0, 2) + "/" + rootDirHash.substring(2));
|
||||||
|
|
||||||
|
|
||||||
|
Mockito.when(cloudContentRepository.folder(foo, DATA_DIR_NAME)).thenReturn(dFolder);
|
||||||
|
Mockito.when(cloudContentRepository.create(dFolder)).thenReturn(dFolder);
|
||||||
|
|
||||||
|
Mockito.when(cryptor.fileNameCryptor().hashDirectoryId(ROOT_DIR_ID)).thenReturn(ROOT_DIR_ID);
|
||||||
|
|
||||||
|
Mockito.when(cloudContentRepository.folder(dFolder, lvl1Dir.getName())).thenReturn(lvl1Dir);
|
||||||
|
Mockito.when(cloudContentRepository.create(lvl1Dir)).thenReturn(lvl1Dir);
|
||||||
|
|
||||||
|
Mockito.when(cloudContentRepository.folder(lvl1Dir, lvl2Dir.getName())).thenReturn(lvl2Dir);
|
||||||
|
Mockito.when(cloudContentRepository.create(lvl2Dir)).thenReturn(lvl2Dir);
|
||||||
|
|
||||||
|
inTest.create(foo, "foo", vaultConfigBuilder);
|
||||||
|
|
||||||
|
Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong());
|
||||||
|
Mockito.verify(cloudContentRepository).write(Mockito.eq(vaultFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(false), Mockito.anyLong());
|
||||||
|
Mockito.verify(cloudContentRepository).create(dFolder);
|
||||||
|
Mockito.verify(cloudContentRepository).create(lvl1Dir);
|
||||||
|
Mockito.verify(cloudContentRepository).create(lvl2Dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("lock(\"foo\")")
|
||||||
|
public void testLockVault() {
|
||||||
|
inTest.lock(vault);
|
||||||
|
Mockito.verify(cryptoCloudContentRepositoryFactory).deregisterCryptor(vault);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("unlock(\"foo\")")
|
||||||
|
public void testUnlockVault() throws BackendException, IOException {
|
||||||
|
CloudType cloudType = Mockito.mock(CloudType.class);
|
||||||
|
|
||||||
|
Mockito.when(cloud.type()).thenReturn(cloudType);
|
||||||
|
|
||||||
|
Mockito.when(vault.getCloud()).thenReturn(cloud);
|
||||||
|
Mockito.when(vault.getCloudType()).thenReturn(cloudType);
|
||||||
|
Mockito.when(vault.getFormat()).thenReturn(8);
|
||||||
|
Mockito.when(vault.getId()).thenReturn(25L);
|
||||||
|
Mockito.when(vault.getName()).thenReturn("foo");
|
||||||
|
Mockito.when(vault.getPath()).thenReturn("/foo");
|
||||||
|
Mockito.when(vault.isUnlocked()).thenReturn(true);
|
||||||
|
|
||||||
|
MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8));
|
||||||
|
UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION);
|
||||||
|
|
||||||
|
Vault result = inTest.unlock(unlockToken, Optional.of(unverifiedVaultConfig), "foo", () -> false);
|
||||||
|
|
||||||
|
MatcherAssert.assertThat(result.isUnlocked(), is(true));
|
||||||
|
MatcherAssert.assertThat(result.getFormat(), is(8));
|
||||||
|
MatcherAssert.assertThat(result.getShorteningThreshold(), is(DEFAULT_MAX_FILE_NAME));
|
||||||
|
|
||||||
|
Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), SIV_CTRMAC);
|
||||||
|
Mockito.verify(cryptoCloudContentRepositoryFactory).registerCryptor(Mockito.any(Vault.class), Mockito.any(Cryptor.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("unlockLegacy(\"foo\")")
|
||||||
|
public void testUnlockLegacyVault() throws BackendException, IOException {
|
||||||
|
CloudType cloudType = Mockito.mock(CloudType.class);
|
||||||
|
|
||||||
|
Mockito.when(cloud.type()).thenReturn(cloudType);
|
||||||
|
|
||||||
|
Mockito.when(vault.getCloud()).thenReturn(cloud);
|
||||||
|
Mockito.when(vault.getCloudType()).thenReturn(cloudType);
|
||||||
|
Mockito.when(vault.getFormat()).thenReturn(7);
|
||||||
|
Mockito.when(vault.getId()).thenReturn(25L);
|
||||||
|
Mockito.when(vault.getName()).thenReturn("foo");
|
||||||
|
Mockito.when(vault.getPath()).thenReturn("/foo");
|
||||||
|
Mockito.when(vault.isUnlocked()).thenReturn(true);
|
||||||
|
|
||||||
|
MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
Vault result = inTest.unlock(unlockToken, Optional.empty(), "foo", () -> false);
|
||||||
|
|
||||||
|
MatcherAssert.assertThat(result.isUnlocked(), is(true));
|
||||||
|
MatcherAssert.assertThat(result.getFormat(), is(MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG));
|
||||||
|
MatcherAssert.assertThat(result.getShorteningThreshold(), is(DEFAULT_MAX_FILE_NAME));
|
||||||
|
|
||||||
|
Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile("foo"), SIV_CTRMAC);
|
||||||
|
Mockito.verify(cryptoCloudContentRepositoryFactory).registerCryptor(Mockito.any(Vault.class), Mockito.any(Cryptor.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("unlockLegacyUsingNewVault(\"foo\")")
|
||||||
|
public void testUnlockLegacyVaultUsingVaultFormat8() {
|
||||||
|
UnlockToken unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV8.getBytes(StandardCharsets.UTF_8));
|
||||||
|
Assertions.assertThrows(UnsupportedVaultFormatException.class, () -> inTest.unlock(unlockToken, Optional.empty(), "foo", () -> false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("changePassword(\"foo\")")
|
||||||
|
@ParameterizedTest(name = "Legacy vault format {0}")
|
||||||
|
@ValueSource(booleans = {true, false})
|
||||||
|
public void tesChangePassword(boolean legacy) throws BackendException {
|
||||||
|
if (legacy) {
|
||||||
|
testChangePassword(masterkeyV7, Optional.empty());
|
||||||
|
} else {
|
||||||
|
UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION);
|
||||||
|
testChangePassword(masterkeyV8, Optional.of(unverifiedVaultConfig));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testChangePassword(String masterkeyContent, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||||
|
TestFolder rootFolder = new RootTestFolder(cloud);
|
||||||
|
TestFolder foo = new TestFolder(rootFolder, "foo", "/foo");
|
||||||
|
TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty());
|
||||||
|
TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty());
|
||||||
|
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile);
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile);
|
||||||
|
Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo);
|
||||||
|
|
||||||
|
// 1. Read masterkey
|
||||||
|
Mockito.doAnswer(invocation -> {
|
||||||
|
OutputStream out = invocation.getArgument(2);
|
||||||
|
CopyStream.copyStreamToStream(new ByteArrayInputStream(masterkeyContent.getBytes()), out);
|
||||||
|
return null;
|
||||||
|
}).when(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE));
|
||||||
|
|
||||||
|
// 2. Create backup
|
||||||
|
String fileName = masterKeyFile.getName() + BackupFileIdSuffixGenerator.generate(masterkeyContent.getBytes()) + MASTERKEY_BACKUP_FILE_EXT;
|
||||||
|
TestFile masterKeyBackupFile = new TestFile(foo, fileName, "/foo/" + fileName, Optional.empty(), Optional.empty());
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, fileName)).thenReturn(masterKeyBackupFile);
|
||||||
|
|
||||||
|
Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyBackupFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> {
|
||||||
|
DataSource in = invocationOnMock.getArgument(1);
|
||||||
|
String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining());
|
||||||
|
assertThat(masterKeyFileContent, is(masterkeyContent));
|
||||||
|
return invocationOnMock.getArgument(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Create new Masterkey file
|
||||||
|
String changedMasterkey;
|
||||||
|
if (unverifiedVaultConfig.isPresent()) {
|
||||||
|
changedMasterkey = "{ \"version\": 999, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"hmacMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"versionMac\": \"trDKXqDhu94/VPuoWaQGBm8hwSPYc0D9t6DRRxKZ65k=\"}";
|
||||||
|
} else {
|
||||||
|
changedMasterkey = "{ \"version\": 7, \"scryptSalt\": \"AAAAAAAAAAA=\", \"scryptCostParam\": 32768, \"scryptBlockSize\": 8, \"primaryMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"hmacMasterKey\": \"O8Z6ZP+aScORaOrMtWYrXjA5EptZk+IAYjEDEUJ7yIvGOWsR+CiwkA==\", \"versionMac\": \"cn2sAK6l9p1/w9deJVUuW3h7br056mpv5srvALiYw+g=\"}";
|
||||||
|
}
|
||||||
|
Mockito.when(cloudContentRepository.write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong())).thenAnswer(invocationOnMock -> {
|
||||||
|
DataSource in = invocationOnMock.getArgument(1);
|
||||||
|
String masterKeyFileContent = new BufferedReader(new InputStreamReader(in.open(context), StandardCharsets.UTF_8)).lines().collect(Collectors.joining());
|
||||||
|
assertThat(masterKeyFileContent, is(changedMasterkey));
|
||||||
|
return invocationOnMock.getArgument(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
inTest.changePassword(vault, unverifiedVaultConfig, "foo", "bar");
|
||||||
|
|
||||||
|
Mockito.verify(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE));
|
||||||
|
Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyBackupFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong());
|
||||||
|
Mockito.verify(cloudContentRepository).write(Mockito.eq(masterKeyFile), Mockito.any(DataSource.class), Mockito.eq(NO_OP_PROGRESS_AWARE), Mockito.eq(true), Mockito.anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("isVaultPasswordValid(\"foo\", \"foo\")")
|
||||||
|
@ParameterizedTest(name = "Legacy vault format {0}")
|
||||||
|
@ValueSource(booleans = {true, false})
|
||||||
|
public void testVaultPasswordVault(boolean legacy) throws BackendException, IOException {
|
||||||
|
String password = "foo";
|
||||||
|
if (legacy) {
|
||||||
|
assertThat(testVaultPasswordVault(masterkeyV7, Optional.empty(), password), is(true));
|
||||||
|
|
||||||
|
MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV7.getBytes(StandardCharsets.UTF_8));
|
||||||
|
Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile(password), SIV_CTRMAC);
|
||||||
|
} else {
|
||||||
|
UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION);
|
||||||
|
assertThat(testVaultPasswordVault(masterkeyV8, Optional.of(unverifiedVaultConfig), password), is(true));
|
||||||
|
|
||||||
|
MasterkeyCryptoCloudProvider.UnlockTokenImpl unlockToken = new MasterkeyCryptoCloudProvider.UnlockTokenImpl(vault, masterkeyV8.getBytes(StandardCharsets.UTF_8));
|
||||||
|
Mockito.verify(inTest).cryptorFor(unlockToken.getKeyFile(password), SIV_CTRMAC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("isVaultPasswordValid(\"foo\", \"bar\")")
|
||||||
|
@ParameterizedTest(name = "Legacy vault format {0}")
|
||||||
|
@ValueSource(booleans = {true, false})
|
||||||
|
public void testVaultPasswordVaultInvalidPassword(boolean legacy) throws BackendException, IOException {
|
||||||
|
String password = "bar";
|
||||||
|
if (legacy) {
|
||||||
|
assertThat(testVaultPasswordVault(masterkeyV7, Optional.empty(), password), is(false));
|
||||||
|
} else {
|
||||||
|
UnverifiedVaultConfig unverifiedVaultConfig = new UnverifiedVaultConfig(vaultConfig, URI.create(String.format("%s:%s", MASTERKEY_SCHEME, MASTERKEY_FILE_NAME)), MAX_VAULT_VERSION);
|
||||||
|
assertThat(testVaultPasswordVault(masterkeyV8, Optional.of(unverifiedVaultConfig), password), is(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private boolean testVaultPasswordVault(String masterkeyContent, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String password) throws BackendException {
|
||||||
|
TestFolder rootFolder = new RootTestFolder(cloud);
|
||||||
|
TestFolder foo = new TestFolder(rootFolder, "foo", "/foo");
|
||||||
|
TestFile vaultFile = new TestFile(foo, VAULT_FILE_NAME, "/foo/" + VAULT_FILE_NAME, Optional.empty(), Optional.empty());
|
||||||
|
TestFile masterKeyFile = new TestFile(foo, MASTERKEY_FILE_NAME, "/foo/" + MASTERKEY_FILE_NAME, Optional.empty(), Optional.empty());
|
||||||
|
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile);
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile);
|
||||||
|
Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo);
|
||||||
|
|
||||||
|
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, VAULT_FILE_NAME)).thenReturn(vaultFile);
|
||||||
|
Mockito.when(cloudContentRepository.file(foo, MASTERKEY_FILE_NAME)).thenReturn(masterKeyFile);
|
||||||
|
Mockito.when(cloudContentRepository.resolve(vault.getCloud(), vault.getPath())).thenReturn(foo);
|
||||||
|
|
||||||
|
// 1. Read masterkey
|
||||||
|
Mockito.doAnswer(invocation -> {
|
||||||
|
OutputStream out = invocation.getArgument(2);
|
||||||
|
CopyStream.copyStreamToStream(new ByteArrayInputStream(masterkeyContent.getBytes()), out);
|
||||||
|
return null;
|
||||||
|
}).when(cloudContentRepository).read(Mockito.eq(masterKeyFile), Mockito.eq(Optional.empty()), Mockito.any(), Mockito.eq(NO_OP_PROGRESS_AWARE));
|
||||||
|
|
||||||
|
return inTest.isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,6 +16,8 @@ android {
|
|||||||
|
|
||||||
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
|
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
|
||||||
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
||||||
|
|
||||||
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@ -50,10 +52,8 @@ dependencies {
|
|||||||
implementation dependencies.appcompat
|
implementation dependencies.appcompat
|
||||||
|
|
||||||
api dependencies.jsonWebTokenApi
|
api dependencies.jsonWebTokenApi
|
||||||
runtimeOnly dependencies.jsonWebTokenImpl
|
implementation dependencies.jsonWebTokenImpl
|
||||||
runtimeOnly(dependencies.jsonWebTokenJson) {
|
implementation dependencies.jsonWebTokenJson
|
||||||
exclude group: 'org.json', module: 'json' //provided by Android natively
|
|
||||||
}
|
|
||||||
|
|
||||||
// test
|
// test
|
||||||
testImplementation dependencies.junit
|
testImplementation dependencies.junit
|
||||||
@ -65,6 +65,7 @@ dependencies {
|
|||||||
testRuntimeOnly dependencies.junit4Engine
|
testRuntimeOnly dependencies.junit4Engine
|
||||||
|
|
||||||
testImplementation dependencies.mockito
|
testImplementation dependencies.mockito
|
||||||
|
testImplementation dependencies.mockitoInline
|
||||||
testImplementation dependencies.hamcrest
|
testImplementation dependencies.hamcrest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 shorteningThreshold;
|
||||||
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.shorteningThreshold = builder.shorteningThreshold;
|
||||||
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()) //
|
||||||
|
.withShorteningThreshold(vault.getShorteningThreshold()) //
|
||||||
.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 getShorteningThreshold() {
|
||||||
|
return shorteningThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 shorteningThreshold = -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 withShorteningThreshold(int shorteningThreshold) {
|
||||||
|
this.shorteningThreshold = shorteningThreshold;
|
||||||
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,21 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VaultConfigLoadException(Exception e) {
|
||||||
|
super(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,13 +0,0 @@
|
|||||||
package org.cryptomator.domain.repository;
|
|
||||||
|
|
||||||
import org.cryptomator.domain.Cloud;
|
|
||||||
import org.cryptomator.domain.exception.BackendException;
|
|
||||||
|
|
||||||
public interface CloudAuthenticationService {
|
|
||||||
|
|
||||||
boolean isAuthenticated(Cloud cloud) throws BackendException;
|
|
||||||
|
|
||||||
boolean canAuthenticate(Cloud cloud);
|
|
||||||
|
|
||||||
Cloud updateAuthenticatedCloud(Cloud cloud);
|
|
||||||
}
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -169,12 +169,6 @@ dependencies {
|
|||||||
implementation dependencies.zxcvbn
|
implementation dependencies.zxcvbn
|
||||||
implementation dependencies.rxBinding
|
implementation dependencies.rxBinding
|
||||||
|
|
||||||
api dependencies.jsonWebTokenApi
|
|
||||||
runtimeOnly dependencies.jsonWebTokenImpl
|
|
||||||
runtimeOnly(dependencies.jsonWebTokenJson) {
|
|
||||||
exclude group: 'org.json', module: 'json' //provided by Android natively
|
|
||||||
}
|
|
||||||
|
|
||||||
// multidex
|
// multidex
|
||||||
implementation dependencies.multidex
|
implementation dependencies.multidex
|
||||||
|
|
||||||
@ -237,3 +231,16 @@ androidExtensions {
|
|||||||
static def getApiKey(key) {
|
static def getApiKey(key) {
|
||||||
return System.getenv().getOrDefault(key, "")
|
return System.getenv().getOrDefault(key, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(Test) {
|
||||||
|
testLogging {
|
||||||
|
events "failed"
|
||||||
|
|
||||||
|
showExceptions true
|
||||||
|
exceptionFormat "full"
|
||||||
|
showCauses true
|
||||||
|
showStackTraces true
|
||||||
|
|
||||||
|
showStandardStreams = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -97,10 +97,11 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".ui.activity.UnlockVaultActivity"
|
||||||
|
android:theme="@style/TransparentAlertDialogCustom"
|
||||||
|
android:label=""/>
|
||||||
<activity android:name=".ui.activity.EmptyDirIdFileInfoActivity" />
|
<activity android:name=".ui.activity.EmptyDirIdFileInfoActivity" />
|
||||||
|
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<activity android:name=".ui.activity.BiometricAuthSettingsActivity" />
|
<activity android:name=".ui.activity.BiometricAuthSettingsActivity" />
|
||||||
<activity android:name=".ui.activity.CloudConnectionListActivity" />
|
<activity android:name=".ui.activity.CloudConnectionListActivity" />
|
||||||
|
@ -22,6 +22,7 @@ import org.cryptomator.presentation.ui.activity.SettingsActivity;
|
|||||||
import org.cryptomator.presentation.ui.activity.SharedFilesActivity;
|
import org.cryptomator.presentation.ui.activity.SharedFilesActivity;
|
||||||
import org.cryptomator.presentation.ui.activity.SplashActivity;
|
import org.cryptomator.presentation.ui.activity.SplashActivity;
|
||||||
import org.cryptomator.presentation.ui.activity.TextEditorActivity;
|
import org.cryptomator.presentation.ui.activity.TextEditorActivity;
|
||||||
|
import org.cryptomator.presentation.ui.activity.UnlockVaultActivity;
|
||||||
import org.cryptomator.presentation.ui.activity.VaultListActivity;
|
import org.cryptomator.presentation.ui.activity.VaultListActivity;
|
||||||
import org.cryptomator.presentation.ui.activity.WebDavAddOrChangeActivity;
|
import org.cryptomator.presentation.ui.activity.WebDavAddOrChangeActivity;
|
||||||
import org.cryptomator.presentation.ui.fragment.AutoUploadChooseVaultFragment;
|
import org.cryptomator.presentation.ui.fragment.AutoUploadChooseVaultFragment;
|
||||||
@ -36,6 +37,7 @@ import org.cryptomator.presentation.ui.fragment.S3AddOrChangeFragment;
|
|||||||
import org.cryptomator.presentation.ui.fragment.SetPasswordFragment;
|
import org.cryptomator.presentation.ui.fragment.SetPasswordFragment;
|
||||||
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment;
|
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment;
|
||||||
import org.cryptomator.presentation.ui.fragment.TextEditorFragment;
|
import org.cryptomator.presentation.ui.fragment.TextEditorFragment;
|
||||||
|
import org.cryptomator.presentation.ui.fragment.UnlockVaultFragment;
|
||||||
import org.cryptomator.presentation.ui.fragment.VaultListFragment;
|
import org.cryptomator.presentation.ui.fragment.VaultListFragment;
|
||||||
import org.cryptomator.presentation.ui.fragment.WebDavAddOrChangeFragment;
|
import org.cryptomator.presentation.ui.fragment.WebDavAddOrChangeFragment;
|
||||||
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow;
|
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow;
|
||||||
@ -117,6 +119,10 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(LicenseCheckActivity licenseCheckActivity);
|
void inject(LicenseCheckActivity licenseCheckActivity);
|
||||||
|
|
||||||
|
void inject(UnlockVaultActivity unlockVaultActivity);
|
||||||
|
|
||||||
|
void inject(UnlockVaultFragment unlockVaultFragment);
|
||||||
|
|
||||||
void inject(S3AddOrChangeActivity s3AddOrChangeActivity);
|
void inject(S3AddOrChangeActivity s3AddOrChangeActivity);
|
||||||
|
|
||||||
void inject(S3AddOrChangeFragment s3AddOrChangeFragment);
|
void inject(S3AddOrChangeFragment s3AddOrChangeFragment);
|
||||||
|
@ -18,6 +18,10 @@ import org.cryptomator.domain.exception.license.NoLicenseAvailableException
|
|||||||
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException
|
import org.cryptomator.domain.exception.update.GeneralUpdateErrorException
|
||||||
import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException
|
import org.cryptomator.domain.exception.update.HashMismatchUpdateCheckException
|
||||||
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
|
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.UnsupportedMasterkeyLocationException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultConfigLoadException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultKeyInvalidException
|
||||||
|
import org.cryptomator.domain.exception.vaultconfig.VaultVersionMismatchException
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
import org.cryptomator.presentation.ui.activity.view.View
|
import org.cryptomator.presentation.ui.activity.view.View
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
@ -48,6 +52,10 @@ class ExceptionHandlers @Inject constructor(private val context: Context, defaul
|
|||||||
staticHandler(HashMismatchUpdateCheckException::class.java, R.string.error_hash_mismatch_update)
|
staticHandler(HashMismatchUpdateCheckException::class.java, R.string.error_hash_mismatch_update)
|
||||||
staticHandler(GeneralUpdateErrorException::class.java, R.string.error_general_update)
|
staticHandler(GeneralUpdateErrorException::class.java, R.string.error_general_update)
|
||||||
staticHandler(SSLHandshakePreAndroid5UpdateCheckException::class.java, R.string.error_general_update)
|
staticHandler(SSLHandshakePreAndroid5UpdateCheckException::class.java, R.string.error_general_update)
|
||||||
|
staticHandler(VaultVersionMismatchException::class.java, R.string.error_vault_version_mismatch)
|
||||||
|
staticHandler(VaultKeyInvalidException::class.java, R.string.error_vault_key_invalid)
|
||||||
|
staticHandler(VaultConfigLoadException::class.java, R.string.error_vault_config_loading)
|
||||||
|
staticHandler(UnsupportedMasterkeyLocationException::class.java, R.string.error_masterkey_location_not_supported)
|
||||||
staticHandler(NoSuchBucketException::class.java, R.string.error_no_such_bucket)
|
staticHandler(NoSuchBucketException::class.java, R.string.error_no_such_bucket)
|
||||||
exceptionHandlers.add(MissingCryptorExceptionHandler())
|
exceptionHandlers.add(MissingCryptorExceptionHandler())
|
||||||
exceptionHandlers.add(CancellationExceptionHandler())
|
exceptionHandlers.add(CancellationExceptionHandler())
|
||||||
|
@ -160,12 +160,19 @@ public class ChooseCloudNodeSettings implements Serializable {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder selectingFilesWithNameOnly(String name) {
|
public Builder selectingFileWithNameOnly(String name) {
|
||||||
this.selectionMode = FILES_ONLY;
|
this.selectionMode = FILES_ONLY;
|
||||||
this.namePattern = Pattern.compile(Pattern.quote(name));
|
this.namePattern = Pattern.compile(Pattern.quote(name));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder selectingFilesWithNameOnly(List<String> names) {
|
||||||
|
this.selectionMode = FILES_ONLY;
|
||||||
|
String pattern = names.stream().map(Pattern::quote).reduce(Pattern.quote(""), (p1, p2) -> p1 + "|" + p2);
|
||||||
|
this.namePattern = Pattern.compile(pattern);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder selectingFoldersNotContaining(List<String> names) {
|
public Builder selectingFoldersNotContaining(List<String> names) {
|
||||||
this.selectionMode = FOLDERS_ONLY;
|
this.selectionMode = FOLDERS_ONLY;
|
||||||
this.excludeFolderContainingNames = names;
|
this.excludeFolderContainingNames = names;
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.cryptomator.presentation.intent;
|
||||||
|
|
||||||
|
import org.cryptomator.generator.Intent;
|
||||||
|
import org.cryptomator.presentation.model.VaultModel;
|
||||||
|
import org.cryptomator.presentation.ui.activity.UnlockVaultActivity;
|
||||||
|
|
||||||
|
@Intent(UnlockVaultActivity.class)
|
||||||
|
public interface UnlockVaultIntent {
|
||||||
|
|
||||||
|
VaultModel vaultModel();
|
||||||
|
|
||||||
|
VaultAction vaultAction();
|
||||||
|
|
||||||
|
enum VaultAction {
|
||||||
|
UNLOCK,
|
||||||
|
UNLOCK_FOR_BIOMETRIC_AUTH,
|
||||||
|
ENCRYPT_PASSWORD,
|
||||||
|
CHANGE_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +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 format: Int
|
||||||
|
get() = vault.format
|
||||||
|
val shorteningThreshold: Int
|
||||||
|
get() = vault.shorteningThreshold
|
||||||
|
|
||||||
fun toVault(): Vault {
|
fun toVault(): Vault {
|
||||||
return vault
|
return vault
|
||||||
|
@ -7,17 +7,14 @@ import org.cryptomator.domain.di.PerView
|
|||||||
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase
|
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase
|
||||||
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase
|
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
|
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase
|
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase
|
|
||||||
import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken
|
|
||||||
import org.cryptomator.generator.Callback
|
import org.cryptomator.generator.Callback
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||||
import org.cryptomator.presentation.intent.ChooseCloudNodeSettings
|
import org.cryptomator.presentation.intent.ChooseCloudNodeSettings
|
||||||
import org.cryptomator.presentation.intent.Intents
|
import org.cryptomator.presentation.intent.Intents
|
||||||
|
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||||
import org.cryptomator.presentation.model.CloudFolderModel
|
import org.cryptomator.presentation.model.CloudFolderModel
|
||||||
import org.cryptomator.presentation.model.CloudModel
|
import org.cryptomator.presentation.model.CloudModel
|
||||||
import org.cryptomator.presentation.model.ProgressModel
|
|
||||||
import org.cryptomator.presentation.model.VaultModel
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
||||||
import org.cryptomator.presentation.ui.activity.view.AutoUploadChooseVaultView
|
import org.cryptomator.presentation.ui.activity.view.AutoUploadChooseVaultView
|
||||||
@ -32,8 +29,6 @@ class AutoUploadChooseVaultPresenter @Inject constructor( //
|
|||||||
private val getVaultListUseCase: GetVaultListUseCase, //
|
private val getVaultListUseCase: GetVaultListUseCase, //
|
||||||
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
||||||
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
||||||
private val unlockVaultUseCase: UnlockVaultUseCase, //
|
|
||||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
|
|
||||||
private val cloudFolderModelMapper: CloudFolderModelMapper, //
|
private val cloudFolderModelMapper: CloudFolderModelMapper, //
|
||||||
private val sharedPreferencesHandler: SharedPreferencesHandler, //
|
private val sharedPreferencesHandler: SharedPreferencesHandler, //
|
||||||
private val authenticationExceptionHandler: AuthenticationExceptionHandler, //
|
private val authenticationExceptionHandler: AuthenticationExceptionHandler, //
|
||||||
@ -83,11 +78,24 @@ class AutoUploadChooseVaultPresenter @Inject constructor( //
|
|||||||
decryptedCloudFor(authenticatedVault)
|
decryptedCloudFor(authenticatedVault)
|
||||||
} else {
|
} else {
|
||||||
if (!isPaused) {
|
if (!isPaused) {
|
||||||
view?.showEnterPasswordDialog(VaultModel(authenticatedVault))
|
requestActivityResult( //
|
||||||
|
ActivityResultCallbacks.vaultUnlockedAutoUpload(), //
|
||||||
|
Intents.unlockVaultIntent().withVaultModel(VaultModel(authenticatedVault)).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Callback
|
||||||
|
fun vaultUnlockedAutoUpload(result: ActivityResult) {
|
||||||
|
val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud
|
||||||
|
when {
|
||||||
|
result.isResultOk -> rootFolderFor(cloud)
|
||||||
|
else -> TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun decryptedCloudFor(vault: Vault) {
|
private fun decryptedCloudFor(vault: Vault) {
|
||||||
getDecryptedCloudForVaultUseCase //
|
getDecryptedCloudForVaultUseCase //
|
||||||
.withVault(vault) //
|
.withVault(vault) //
|
||||||
@ -151,49 +159,11 @@ class AutoUploadChooseVaultPresenter @Inject constructor( //
|
|||||||
location?.let { view?.showChosenLocation(it) }
|
location?.let { view?.showChosenLocation(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUnlockCanceled() {
|
|
||||||
unlockVaultUseCase.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUnlockPressed(vaultModel: VaultModel, password: String?) {
|
|
||||||
view?.showProgress(ProgressModel.GENERIC)
|
|
||||||
unlockVaultUseCase //
|
|
||||||
.withVaultOrUnlockToken(VaultOrUnlockToken.from(vaultModel.toVault())) //
|
|
||||||
.andPassword(password) //
|
|
||||||
.run(object : DefaultResultHandler<Cloud>() {
|
|
||||||
override fun onSuccess(cloud: Cloud) {
|
|
||||||
view?.showProgress(ProgressModel.COMPLETED)
|
|
||||||
rootFolderFor(cloud)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
if (!authenticationExceptionHandler.handleAuthenticationException( //
|
|
||||||
this@AutoUploadChooseVaultPresenter, //
|
|
||||||
e, //
|
|
||||||
ActivityResultCallbacks.unlockVaultAfterAuth(vaultModel.toVault(), password))) {
|
|
||||||
showError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBiometricKeyInvalidated(vaultModel: VaultModel?) {
|
|
||||||
removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler<Void?>() {
|
|
||||||
override fun onSuccess(void: Void?) {
|
|
||||||
view?.showBiometricAuthKeyInvalidatedDialog()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean {
|
|
||||||
return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication()
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class AuthenticationState {
|
enum class AuthenticationState {
|
||||||
CHOOSE_LOCATION, INIT_ROOT
|
CHOOSE_LOCATION, INIT_ROOT
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
unsubscribeOnDestroy(getVaultListUseCase)
|
unsubscribeOnDestroy(getVaultListUseCase, getRootFolderUseCase, getDecryptedCloudForVaultUseCase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,21 @@ package org.cryptomator.presentation.presenter
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException
|
|
||||||
import org.cryptomator.domain.Cloud
|
import org.cryptomator.domain.Cloud
|
||||||
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.usecases.vault.*
|
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.LockVaultUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.SaveVaultUseCase
|
||||||
import org.cryptomator.generator.Callback
|
import org.cryptomator.generator.Callback
|
||||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||||
import org.cryptomator.presentation.model.CloudModel
|
import org.cryptomator.presentation.intent.Intents
|
||||||
import org.cryptomator.presentation.model.ProgressModel
|
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||||
import org.cryptomator.presentation.model.VaultModel
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
import org.cryptomator.presentation.ui.activity.view.BiometricAuthSettingsView
|
import org.cryptomator.presentation.ui.activity.view.BiometricAuthSettingsView
|
||||||
import org.cryptomator.presentation.workflow.ActivityResult
|
import org.cryptomator.presentation.workflow.ActivityResult
|
||||||
import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler
|
|
||||||
import org.cryptomator.util.SharedPreferencesHandler
|
import org.cryptomator.util.SharedPreferencesHandler
|
||||||
import java.util.*
|
import java.util.ArrayList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -24,13 +24,9 @@ import timber.log.Timber
|
|||||||
class BiometricAuthSettingsPresenter @Inject constructor( //
|
class BiometricAuthSettingsPresenter @Inject constructor( //
|
||||||
private val getVaultListUseCase: GetVaultListUseCase, //
|
private val getVaultListUseCase: GetVaultListUseCase, //
|
||||||
private val saveVaultUseCase: SaveVaultUseCase, //
|
private val saveVaultUseCase: SaveVaultUseCase, //
|
||||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
|
|
||||||
private val checkVaultPasswordUseCase: CheckVaultPasswordUseCase, //
|
|
||||||
private val unlockVaultUseCase: UnlockVaultUseCase, //
|
|
||||||
private val lockVaultUseCase: LockVaultUseCase, //
|
private val lockVaultUseCase: LockVaultUseCase, //
|
||||||
exceptionMappings: ExceptionHandlers, //
|
exceptionMappings: ExceptionHandlers, //
|
||||||
private val sharedPreferencesHandler: SharedPreferencesHandler, //
|
private val sharedPreferencesHandler: SharedPreferencesHandler) : Presenter<BiometricAuthSettingsView>(exceptionMappings) {
|
||||||
private val authenticationExceptionHandler: AuthenticationExceptionHandler) : Presenter<BiometricAuthSettingsView>(exceptionMappings) {
|
|
||||||
|
|
||||||
fun loadVaultList() {
|
fun loadVaultList() {
|
||||||
updateVaultListView()
|
updateVaultListView()
|
||||||
@ -49,92 +45,56 @@ class BiometricAuthSettingsPresenter @Inject constructor( //
|
|||||||
|
|
||||||
fun updateVaultEntityWithChangedBiometricAuthSettings(vaultModel: VaultModel, useBiometricAuth: Boolean) {
|
fun updateVaultEntityWithChangedBiometricAuthSettings(vaultModel: VaultModel, useBiometricAuth: Boolean) {
|
||||||
if (useBiometricAuth) {
|
if (useBiometricAuth) {
|
||||||
view?.showEnterPasswordDialog(VaultModel(vaultModel.toVault()))
|
verifyPassword(vaultModel)
|
||||||
} else {
|
} else {
|
||||||
removePasswordAndSave(vaultModel.toVault())
|
removePasswordAndSave(vaultModel.toVault())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun verifyPassword(vaultModel: VaultModel) {
|
private fun verifyPassword(vaultModel: VaultModel) {
|
||||||
Timber.tag("BiomtricAuthSettngsPres").i("Checking entered vault password")
|
Timber.tag("BiomtricAuthSettngsPres").i("Checking entered vault password")
|
||||||
if (vaultModel.isLocked) {
|
if (vaultModel.isLocked) {
|
||||||
unlockVault(vaultModel)
|
requestActivityResult( //
|
||||||
|
ActivityResultCallbacks.vaultUnlockedBiometricAuthPres(vaultModel), //
|
||||||
|
Intents.unlockVaultIntent().withVaultModel(vaultModel).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH))
|
||||||
} else {
|
} else {
|
||||||
checkPassword(vaultModel)
|
lockVaultUseCase
|
||||||
|
.withVault(vaultModel.toVault())
|
||||||
|
.run(object : DefaultResultHandler<Vault>() {
|
||||||
|
override fun onSuccess(vault: Vault) {
|
||||||
|
super.onSuccess(vault)
|
||||||
|
requestActivityResult( //
|
||||||
|
ActivityResultCallbacks.vaultUnlockedBiometricAuthPres(vaultModel), //
|
||||||
|
Intents.unlockVaultIntent().withVaultModel(vaultModel).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkPassword(vaultModel: VaultModel) {
|
@Callback
|
||||||
view?.showProgress(ProgressModel.GENERIC)
|
fun vaultUnlockedBiometricAuthPres(result: ActivityResult, vaultModel: VaultModel) {
|
||||||
checkVaultPasswordUseCase //
|
val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud
|
||||||
.withVault(vaultModel.toVault()) //
|
val password = result.intent().getStringExtra(UnlockVaultPresenter.PASSWORD)
|
||||||
.andPassword(vaultModel.password) //
|
val vault = Vault.aCopyOf(vaultModel.toVault()).withCloud(cloud).withSavedPassword(password).build()
|
||||||
.run(object : DefaultResultHandler<Boolean>() {
|
when {
|
||||||
override fun onSuccess(passwordCorrect: Boolean) {
|
result.isResultOk -> requestActivityResult( //
|
||||||
if (passwordCorrect) {
|
ActivityResultCallbacks.encryptVaultPassword(vaultModel), //
|
||||||
Timber.tag("BiomtricAuthSettngsPres").i("Password is correct")
|
Intents.unlockVaultIntent().withVaultModel(VaultModel(vault)).withVaultAction(UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD))
|
||||||
onPasswordCheckSucceeded(vaultModel)
|
else -> TODO("Not yet implemented")
|
||||||
} else {
|
}
|
||||||
Timber.tag("BiomtricAuthSettngsPres").i("Password is wrong")
|
|
||||||
showError(InvalidPassphraseException())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
super.onError(e)
|
|
||||||
Timber.tag("BiomtricAuthSettngsPres").e(e, "Password check failed")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun unlockVault(vaultModel: VaultModel) {
|
|
||||||
view?.showProgress(ProgressModel.GENERIC)
|
|
||||||
unlockVaultUseCase //
|
|
||||||
.withVaultOrUnlockToken(VaultOrUnlockToken.from(vaultModel.toVault())) //
|
|
||||||
.andPassword(vaultModel.password) //
|
|
||||||
.run(object : DefaultResultHandler<Cloud>() {
|
|
||||||
override fun onSuccess(cloud: Cloud) {
|
|
||||||
Timber.tag("BiomtricAuthSettngsPres").i("Password is correct")
|
|
||||||
onUnlockSucceeded(vaultModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
if (!authenticationExceptionHandler.handleAuthenticationException(this@BiometricAuthSettingsPresenter, e, ActivityResultCallbacks.unlockVaultAfterAuth(vaultModel.toVault()))) {
|
|
||||||
showError(e)
|
|
||||||
Timber.tag("BiomtricAuthSettngsPres").e(e, "Password check failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onUnlockSucceeded(vaultModel: VaultModel) {
|
|
||||||
lockVaultUseCase
|
|
||||||
.withVault(vaultModel.toVault())
|
|
||||||
.run(object : DefaultResultHandler<Vault>() {
|
|
||||||
override fun onSuccess(vault: Vault) {
|
|
||||||
super.onSuccess(vault)
|
|
||||||
onPasswordCheckSucceeded(vaultModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
Timber.tag("BiomtricAuthSettngsPres").e(e, "Locking vault after unlocking failed but continue to save changes")
|
|
||||||
onPasswordCheckSucceeded(vaultModel)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Callback
|
@Callback
|
||||||
fun unlockVaultAfterAuth(result: ActivityResult, vault: Vault?) {
|
fun encryptVaultPassword(result: ActivityResult, vaultModel: VaultModel) {
|
||||||
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
val tmpVault = result.intent().getSerializableExtra(SINGLE_RESULT) as VaultModel
|
||||||
val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build()
|
val vault = Vault.aCopyOf(vaultModel.toVault()).withSavedPassword(tmpVault.password).build()
|
||||||
unlockVault(VaultModel(vaultWithUpdatedCloud))
|
when {
|
||||||
|
result.isResultOk -> saveVault(vault)
|
||||||
|
else -> TODO("Not yet implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPasswordCheckSucceeded(vaultModel: VaultModel) {
|
private fun saveVault(vault: Vault?) {
|
||||||
view?.showBiometricAuthenticationDialog(vaultModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveVault(vault: Vault?) {
|
|
||||||
saveVaultUseCase //
|
saveVaultUseCase //
|
||||||
.withVault(vault) //
|
.withVault(vault) //
|
||||||
.run(object : ProgressCompletingResultHandler<Vault>() {
|
.run(object : ProgressCompletingResultHandler<Vault>() {
|
||||||
@ -145,8 +105,7 @@ class BiometricAuthSettingsPresenter @Inject constructor( //
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun switchedGeneralBiometricAuthSettings(isChecked: Boolean) {
|
fun switchedGeneralBiometricAuthSettings(isChecked: Boolean) {
|
||||||
sharedPreferencesHandler //
|
sharedPreferencesHandler.changeUseBiometricAuthentication(isChecked)
|
||||||
.changeUseBiometricAuthentication(isChecked)
|
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
loadVaultList()
|
loadVaultList()
|
||||||
} else {
|
} else {
|
||||||
@ -173,32 +132,15 @@ class BiometricAuthSettingsPresenter @Inject constructor( //
|
|||||||
|
|
||||||
fun onSetupBiometricAuthInSystemClicked() {
|
fun onSetupBiometricAuthInSystemClicked() {
|
||||||
val openSecuritySettings = Intent(Settings.ACTION_SECURITY_SETTINGS)
|
val openSecuritySettings = Intent(Settings.ACTION_SECURITY_SETTINGS)
|
||||||
requestActivityResult(ActivityResultCallbacks.onSetupFingerCompleted(), openSecuritySettings)
|
requestActivityResult(ActivityResultCallbacks.onSetupBiometricAuthInSystemCompleted(), openSecuritySettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Callback
|
@Callback
|
||||||
fun onSetupFingerCompleted(result: ActivityResult?) {
|
fun onSetupBiometricAuthInSystemCompleted(result: ActivityResult?) {
|
||||||
view?.showSetupBiometricAuthDialog()
|
view?.showSetupBiometricAuthDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBiometricAuthKeyInvalidated(vaultModel: VaultModel?) {
|
|
||||||
removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler<Void?>() {
|
|
||||||
override fun onSuccess(void: Void?) {
|
|
||||||
view?.showBiometricAuthKeyInvalidatedDialog()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUnlockCanceled() {
|
|
||||||
unlockVaultUseCase.cancel()
|
|
||||||
loadVaultList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean {
|
|
||||||
return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
unsubscribeOnDestroy(getVaultListUseCase, saveVaultUseCase, checkVaultPasswordUseCase, removeStoredVaultPasswordsUseCase, unlockVaultUseCase)
|
unsubscribeOnDestroy(getVaultListUseCase, saveVaultUseCase, lockVaultUseCase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,13 @@ import org.cryptomator.domain.di.PerView
|
|||||||
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase
|
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase
|
||||||
import org.cryptomator.domain.usecases.cloud.*
|
import org.cryptomator.domain.usecases.cloud.*
|
||||||
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
|
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase
|
|
||||||
import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase
|
|
||||||
import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken
|
|
||||||
import org.cryptomator.generator.Callback
|
import org.cryptomator.generator.Callback
|
||||||
import org.cryptomator.generator.InstanceState
|
import org.cryptomator.generator.InstanceState
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||||
import org.cryptomator.presentation.intent.ChooseCloudNodeSettings
|
import org.cryptomator.presentation.intent.ChooseCloudNodeSettings
|
||||||
import org.cryptomator.presentation.intent.Intents
|
import org.cryptomator.presentation.intent.Intents
|
||||||
|
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||||
import org.cryptomator.presentation.model.*
|
import org.cryptomator.presentation.model.*
|
||||||
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
||||||
import org.cryptomator.presentation.model.mappers.ProgressModelMapper
|
import org.cryptomator.presentation.model.mappers.ProgressModelMapper
|
||||||
@ -27,7 +25,6 @@ import org.cryptomator.presentation.workflow.ActivityResult
|
|||||||
import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler
|
import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler
|
||||||
import org.cryptomator.presentation.workflow.PermissionsResult
|
import org.cryptomator.presentation.workflow.PermissionsResult
|
||||||
import org.cryptomator.util.Optional
|
import org.cryptomator.util.Optional
|
||||||
import org.cryptomator.util.SharedPreferencesHandler
|
|
||||||
import org.cryptomator.util.file.FileCacheUtils
|
import org.cryptomator.util.file.FileCacheUtils
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -36,14 +33,11 @@ import timber.log.Timber
|
|||||||
@PerView
|
@PerView
|
||||||
class SharedFilesPresenter @Inject constructor( //
|
class SharedFilesPresenter @Inject constructor( //
|
||||||
private val getVaultListUseCase: GetVaultListUseCase, //
|
private val getVaultListUseCase: GetVaultListUseCase, //
|
||||||
private val unlockVaultUseCase: UnlockVaultUseCase, //
|
|
||||||
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
||||||
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
||||||
private val uploadFilesUseCase: UploadFilesUseCase, //
|
private val uploadFilesUseCase: UploadFilesUseCase, //
|
||||||
private val getCloudListUseCase: GetCloudListUseCase, //
|
private val getCloudListUseCase: GetCloudListUseCase, //
|
||||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
|
|
||||||
private val contentResolverUtil: ContentResolverUtil, //
|
private val contentResolverUtil: ContentResolverUtil, //
|
||||||
private val sharedPreferencesHandler: SharedPreferencesHandler, //
|
|
||||||
private val fileCacheUtils: FileCacheUtils, //
|
private val fileCacheUtils: FileCacheUtils, //
|
||||||
private val authenticationExceptionHandler: AuthenticationExceptionHandler, //
|
private val authenticationExceptionHandler: AuthenticationExceptionHandler, //
|
||||||
private val cloudFolderModelMapper: CloudFolderModelMapper, //
|
private val cloudFolderModelMapper: CloudFolderModelMapper, //
|
||||||
@ -128,6 +122,28 @@ class SharedFilesPresenter @Inject constructor( //
|
|||||||
vaultModel?.let { onCloudOfVaultAuthenticated(it.toVault()) }
|
vaultModel?.let { onCloudOfVaultAuthenticated(it.toVault()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onCloudOfVaultAuthenticated(authenticatedVault: Vault) {
|
||||||
|
if (authenticatedVault.isUnlocked) {
|
||||||
|
decryptedCloudFor(authenticatedVault)
|
||||||
|
} else {
|
||||||
|
if (!isPaused) {
|
||||||
|
requestActivityResult( //
|
||||||
|
ActivityResultCallbacks.vaultUnlockedSharedFiles(), //
|
||||||
|
Intents.unlockVaultIntent().withVaultModel(VaultModel(authenticatedVault)).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Callback
|
||||||
|
fun vaultUnlockedSharedFiles(result: ActivityResult) {
|
||||||
|
val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud
|
||||||
|
when {
|
||||||
|
result.isResultOk -> rootFolderFor(cloud)
|
||||||
|
else -> TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun decryptedCloudFor(vault: Vault) {
|
private fun decryptedCloudFor(vault: Vault) {
|
||||||
getDecryptedCloudForVaultUseCase //
|
getDecryptedCloudForVaultUseCase //
|
||||||
.withVault(vault) //
|
.withVault(vault) //
|
||||||
@ -172,32 +188,6 @@ class SharedFilesPresenter @Inject constructor( //
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUnlockPressed(vaultModel: VaultModel, password: String?) {
|
|
||||||
view?.showProgress(ProgressModel.GENERIC)
|
|
||||||
unlockVaultUseCase //
|
|
||||||
.withVaultOrUnlockToken(VaultOrUnlockToken.from(vaultModel.toVault())) //
|
|
||||||
.andPassword(password) //
|
|
||||||
.run(object : DefaultResultHandler<Cloud>() {
|
|
||||||
override fun onSuccess(cloud: Cloud) {
|
|
||||||
view?.showProgress(ProgressModel.COMPLETED)
|
|
||||||
rootFolderFor(cloud)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
if (!authenticationExceptionHandler.handleAuthenticationException(this@SharedFilesPresenter, e, ActivityResultCallbacks.unlockVaultAfterAuth(vaultModel.toVault(), password))) {
|
|
||||||
showError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Callback
|
|
||||||
fun unlockVaultAfterAuth(result: ActivityResult, vault: Vault?, password: String?) {
|
|
||||||
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
|
||||||
val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build()
|
|
||||||
onUnlockPressed(VaultModel(vaultWithUpdatedCloud), password)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setLocation(location: CloudFolderModel) {
|
private fun setLocation(location: CloudFolderModel) {
|
||||||
this.location = location
|
this.location = location
|
||||||
}
|
}
|
||||||
@ -372,16 +362,6 @@ class SharedFilesPresenter @Inject constructor( //
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCloudOfVaultAuthenticated(authenticatedVault: Vault) {
|
|
||||||
if (authenticatedVault.isUnlocked) {
|
|
||||||
decryptedCloudFor(authenticatedVault)
|
|
||||||
} else {
|
|
||||||
if (!isPaused) {
|
|
||||||
view?.showEnterPasswordDialog(VaultModel(authenticatedVault))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onChooseLocationPressed() {
|
fun onChooseLocationPressed() {
|
||||||
authenticate(selectedVault)
|
authenticate(selectedVault)
|
||||||
}
|
}
|
||||||
@ -410,25 +390,6 @@ class SharedFilesPresenter @Inject constructor( //
|
|||||||
view?.closeDialog()
|
view?.closeDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBiometricAuthKeyInvalidated() {
|
|
||||||
removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler<Void?>() {
|
|
||||||
override fun onFinished() {
|
|
||||||
view?.showBiometricAuthKeyInvalidatedDialog()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
selectedVault?.let {
|
|
||||||
selectedVault = VaultModel(Vault.aCopyOf(it.toVault()).withSavedPassword(null).build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUnlockCanceled() {
|
|
||||||
unlockVaultUseCase.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean {
|
|
||||||
return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication()
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class AuthenticationState {
|
private enum class AuthenticationState {
|
||||||
CHOOSE_LOCATION, INIT_ROOT
|
CHOOSE_LOCATION, INIT_ROOT
|
||||||
}
|
}
|
||||||
@ -444,11 +405,9 @@ class SharedFilesPresenter @Inject constructor( //
|
|||||||
init {
|
init {
|
||||||
unsubscribeOnDestroy( //
|
unsubscribeOnDestroy( //
|
||||||
getRootFolderUseCase, //
|
getRootFolderUseCase, //
|
||||||
unlockVaultUseCase, //
|
|
||||||
getVaultListUseCase, //
|
getVaultListUseCase, //
|
||||||
getDecryptedCloudForVaultUseCase, //
|
getDecryptedCloudForVaultUseCase, //
|
||||||
uploadFilesUseCase, //
|
uploadFilesUseCase, //
|
||||||
getCloudListUseCase, //
|
getCloudListUseCase)
|
||||||
removeStoredVaultPasswordsUseCase)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,423 @@
|
|||||||
|
package org.cryptomator.presentation.presenter
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import org.cryptomator.data.cloud.crypto.CryptoConstants
|
||||||
|
import org.cryptomator.domain.Cloud
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
|
import org.cryptomator.domain.Vault
|
||||||
|
import org.cryptomator.domain.di.PerView
|
||||||
|
import org.cryptomator.domain.exception.NetworkConnectionException
|
||||||
|
import org.cryptomator.domain.exception.authentication.AuthenticationException
|
||||||
|
import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.GetUnverifiedVaultConfigUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.LockVaultUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.SaveVaultUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.UnlockToken
|
||||||
|
import org.cryptomator.domain.usecases.vault.UnlockVaultUsingMasterkeyUseCase
|
||||||
|
import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken
|
||||||
|
import org.cryptomator.generator.Callback
|
||||||
|
import org.cryptomator.generator.InjectIntent
|
||||||
|
import org.cryptomator.presentation.R
|
||||||
|
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||||
|
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||||
|
import org.cryptomator.presentation.model.CloudModel
|
||||||
|
import org.cryptomator.presentation.model.ProgressModel
|
||||||
|
import org.cryptomator.presentation.model.ProgressStateModel
|
||||||
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
|
import org.cryptomator.presentation.ui.activity.view.UnlockVaultView
|
||||||
|
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
||||||
|
import org.cryptomator.presentation.workflow.ActivityResult
|
||||||
|
import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler
|
||||||
|
import org.cryptomator.util.Optional
|
||||||
|
import org.cryptomator.util.SharedPreferencesHandler
|
||||||
|
import java.io.Serializable
|
||||||
|
import javax.inject.Inject
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@PerView
|
||||||
|
class UnlockVaultPresenter @Inject constructor(
|
||||||
|
private val changePasswordUseCase: ChangePasswordUseCase,
|
||||||
|
private val deleteVaultUseCase: DeleteVaultUseCase,
|
||||||
|
private val getUnverifiedVaultConfigUseCase: GetUnverifiedVaultConfigUseCase,
|
||||||
|
private val lockVaultUseCase: LockVaultUseCase,
|
||||||
|
private val unlockVaultUsingMasterkeyUseCase: UnlockVaultUsingMasterkeyUseCase,
|
||||||
|
private val prepareUnlockUseCase: PrepareUnlockUseCase,
|
||||||
|
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase,
|
||||||
|
private val saveVaultUseCase: SaveVaultUseCase,
|
||||||
|
private val authenticationExceptionHandler: AuthenticationExceptionHandler,
|
||||||
|
private val sharedPreferencesHandler: SharedPreferencesHandler,
|
||||||
|
exceptionMappings: ExceptionHandlers) : Presenter<UnlockVaultView>(exceptionMappings) {
|
||||||
|
|
||||||
|
private var startedUsingPrepareUnlock = false
|
||||||
|
private var retryUnlockHandler: Handler? = null
|
||||||
|
private var pendingUnlock: PendingUnlock? = null
|
||||||
|
|
||||||
|
@InjectIntent
|
||||||
|
lateinit var intent: UnlockVaultIntent
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var running: Boolean = false
|
||||||
|
|
||||||
|
override fun destroyed() {
|
||||||
|
super.destroyed()
|
||||||
|
if (retryUnlockHandler != null) {
|
||||||
|
running = false
|
||||||
|
retryUnlockHandler?.removeCallbacks(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> {
|
||||||
|
startedUsingPrepareUnlock = sharedPreferencesHandler.backgroundUnlockPreparation()
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
super.onError(e)
|
||||||
|
finishWithResult(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unlockVault(vaultModel: VaultModel) {
|
||||||
|
if (canUseBiometricOn(vaultModel)) {
|
||||||
|
if (startedUsingPrepareUnlock) {
|
||||||
|
startPrepareUnlockUseCase(vaultModel.toVault())
|
||||||
|
}
|
||||||
|
view?.showBiometricDialog(vaultModel)
|
||||||
|
} else {
|
||||||
|
view?.showEnterPasswordDialog(vaultModel)
|
||||||
|
startPrepareUnlockUseCase(vaultModel.toVault())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
if (hasFocus) {
|
||||||
|
if (retryUnlockHandler != null) {
|
||||||
|
running = false
|
||||||
|
retryUnlockHandler?.removeCallbacks(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pendingUnlockFor(vault: Vault): PendingUnlock? {
|
||||||
|
if (pendingUnlock == null) {
|
||||||
|
pendingUnlock = PendingUnlock(vault)
|
||||||
|
}
|
||||||
|
return if (pendingUnlock?.belongsTo(vault) == true) {
|
||||||
|
pendingUnlock
|
||||||
|
} else {
|
||||||
|
PendingUnlock.NO_OP_PENDING_UNLOCK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canUseBiometricOn(vault: VaultModel): Boolean {
|
||||||
|
return vault.password != null && BiometricManager //
|
||||||
|
.from(context()) //
|
||||||
|
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUnlockCanceled() {
|
||||||
|
prepareUnlockUseCase.unsubscribe()
|
||||||
|
unlockVaultUsingMasterkeyUseCase.cancel()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startPrepareUnlockUseCase(vault: Vault) {
|
||||||
|
prepareUnlockUseCase //
|
||||||
|
.withVault(vault) //
|
||||||
|
.andUnverifiedVaultConfig(Optional.ofNullable(pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig))
|
||||||
|
.run(object : DefaultResultHandler<UnlockToken>() {
|
||||||
|
override fun onSuccess(unlockToken: UnlockToken) {
|
||||||
|
if (!startedUsingPrepareUnlock && vault.password != null) {
|
||||||
|
doUnlock(unlockToken, vault.password, pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig)
|
||||||
|
} else {
|
||||||
|
unlockTokenObtained(unlockToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
if (e is AuthenticationException) {
|
||||||
|
view?.cancelBasicAuthIfRunning()
|
||||||
|
}
|
||||||
|
if (!authenticationExceptionHandler.handleAuthenticationException(this@UnlockVaultPresenter, e, ActivityResultCallbacks.authenticatedAfterUnlock(vault))) {
|
||||||
|
super.onError(e)
|
||||||
|
if (e is NetworkConnectionException) {
|
||||||
|
running = true
|
||||||
|
retryUnlockHandler = Handler()
|
||||||
|
restartUnlockUseCase(vault)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Callback(dispatchResultOkOnly = false)
|
||||||
|
fun authenticatedAfterUnlock(result: ActivityResult, vault: Vault) {
|
||||||
|
if (result.isResultOk) {
|
||||||
|
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
||||||
|
if (startedUsingPrepareUnlock) {
|
||||||
|
startPrepareUnlockUseCase(Vault.aCopyOf(vault).withCloud(cloud).build())
|
||||||
|
if (view?.stoppedBiometricAuthDuringCloudAuthentication() == true) {
|
||||||
|
view?.showBiometricDialog(VaultModel(vault))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
view?.showProgress(ProgressModel.GENERIC)
|
||||||
|
startPrepareUnlockUseCase(vault)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
view?.closeDialog()
|
||||||
|
val error = result.getSingleResult(Throwable::class.java)
|
||||||
|
error?.let { showError(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restartUnlockUseCase(vault: Vault) {
|
||||||
|
retryUnlockHandler?.postDelayed({
|
||||||
|
if (running) {
|
||||||
|
prepareUnlockUseCase //
|
||||||
|
.withVault(vault) //
|
||||||
|
.run(object : DefaultResultHandler<UnlockToken>() {
|
||||||
|
override fun onSuccess(unlockToken: UnlockToken) {
|
||||||
|
if (!startedUsingPrepareUnlock && vault.password != null) {
|
||||||
|
doUnlock(unlockToken, vault.password, pendingUnlockFor(intent.vaultModel().toVault())?.unverifiedVaultConfig)
|
||||||
|
} else {
|
||||||
|
unlockTokenObtained(unlockToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
if (e is NetworkConnectionException) {
|
||||||
|
restartUnlockUseCase(vault)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unlockTokenObtained(unlockToken: UnlockToken) {
|
||||||
|
pendingUnlockFor(unlockToken.vault)?.setUnlockToken(unlockToken, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUnlockClick(vault: VaultModel, password: String?) {
|
||||||
|
view?.showProgress(ProgressModel.GENERIC)
|
||||||
|
pendingUnlockFor(vault.toVault())?.setPassword(password, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doUnlock(token: UnlockToken, password: String, unverifiedVaultConfig: UnverifiedVaultConfig?) {
|
||||||
|
unlockVaultUsingMasterkeyUseCase //
|
||||||
|
.withVaultOrUnlockToken(VaultOrUnlockToken.from(token)) //
|
||||||
|
.andUnverifiedVaultConfig(Optional.ofNullable(unverifiedVaultConfig)) //
|
||||||
|
.andPassword(password) //
|
||||||
|
.run(object : DefaultResultHandler<Cloud>() {
|
||||||
|
override fun onSuccess(cloud: Cloud) {
|
||||||
|
when (intent.vaultAction()) {
|
||||||
|
UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> {
|
||||||
|
handleUnlockVaultSuccess(token.vault, cloud, password)
|
||||||
|
}
|
||||||
|
UnlockVaultIntent.VaultAction.UNLOCK -> finishWithResult(cloud)
|
||||||
|
else -> TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
super.onError(e)
|
||||||
|
// finish in case of biometric auth, otherwise show error in dialog
|
||||||
|
if(view?.isShowingDialog(EnterPasswordDialog::class) == false) {
|
||||||
|
finishWithResult(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUnlockVaultSuccess(vault: Vault, cloud: Cloud, password: String) {
|
||||||
|
lockVaultUseCase.withVault(vault).run(object : DefaultResultHandler<Vault>() {
|
||||||
|
override fun onSuccess(vault: Vault) {
|
||||||
|
finishWithResultAndExtra(cloud, PASSWORD, password)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startedUsingPrepareUnlock(): Boolean {
|
||||||
|
return startedUsingPrepareUnlock
|
||||||
|
}
|
||||||
|
|
||||||
|
fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean {
|
||||||
|
return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBiometricKeyInvalidated() {
|
||||||
|
removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler<Void?>() {
|
||||||
|
override fun onSuccess(void: Void?) {
|
||||||
|
view?.showBiometricAuthKeyInvalidatedDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
Timber.tag("VaultListPresenter").e(e, "Error while removing vault passwords")
|
||||||
|
finishWithResult(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBiometricAuthenticationSucceeded(vaultModel: VaultModel) {
|
||||||
|
when (intent.vaultAction()) {
|
||||||
|
UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> finishWithResult(vaultModel)
|
||||||
|
UnlockVaultIntent.VaultAction.UNLOCK, UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> {
|
||||||
|
if (startedUsingPrepareUnlock) {
|
||||||
|
onUnlockClick(vaultModel, vaultModel.password)
|
||||||
|
} else {
|
||||||
|
view?.showProgress(ProgressModel.GENERIC)
|
||||||
|
startPrepareUnlockUseCase(vaultModel.toVault())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> {
|
||||||
|
saveVaultUseCase //
|
||||||
|
.withVault(vaultModel.toVault()) //
|
||||||
|
.run(object : DefaultResultHandler<Vault>() {
|
||||||
|
override fun onSuccess(vault: Vault) {
|
||||||
|
finishWithResult(vaultModel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else -> TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String) {
|
||||||
|
view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD))
|
||||||
|
changePasswordUseCase.withVault(vaultModel.toVault()) //
|
||||||
|
.andUnverifiedVaultConfig(Optional.ofNullable(unverifiedVaultConfig)) //
|
||||||
|
.andOldPassword(oldPassword) //
|
||||||
|
.andNewPassword(newPassword) //
|
||||||
|
.run(object : DefaultResultHandler<Void?>() {
|
||||||
|
override fun onSuccess(void: Void?) {
|
||||||
|
view?.showProgress(ProgressModel.COMPLETED)
|
||||||
|
view?.showMessage(R.string.screen_vault_list_change_password_successful)
|
||||||
|
if (canUseBiometricOn(vaultModel)) {
|
||||||
|
view?.getEncryptedPasswordWithBiometricAuthentication(VaultModel( //
|
||||||
|
Vault.aCopyOf(vaultModel.toVault()) //
|
||||||
|
.withSavedPassword(newPassword) //
|
||||||
|
.build()))
|
||||||
|
} else {
|
||||||
|
finishWithResult(vaultModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
if (!authenticationExceptionHandler.handleAuthenticationException( //
|
||||||
|
this@UnlockVaultPresenter, e, //
|
||||||
|
ActivityResultCallbacks.changePasswordAfterAuthentication(vaultModel.toVault(), unverifiedVaultConfig, oldPassword, newPassword))) {
|
||||||
|
showError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Callback
|
||||||
|
fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault, unverifiedVaultConfig: UnverifiedVaultConfig, oldPassword: String, newPassword: String) {
|
||||||
|
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
||||||
|
val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build()
|
||||||
|
onChangePasswordClick(VaultModel(vaultWithUpdatedCloud), unverifiedVaultConfig, oldPassword, newPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveVaultAfterChangePasswordButFailedBiometricAuth(vault: Vault) {
|
||||||
|
Timber.tag("UnlockVaultPresenter").e("Save vault without password because biometric auth failed after changing vault password")
|
||||||
|
saveVaultUseCase //
|
||||||
|
.withVault(vault) //
|
||||||
|
.run(object : DefaultResultHandler<Vault>() {
|
||||||
|
override fun onSuccess(vault: Vault) {
|
||||||
|
finishWithResult(vault)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeleteMissingVaultClicked(vault: Vault) {
|
||||||
|
deleteVaultUseCase //
|
||||||
|
.withVault(vault) //
|
||||||
|
.run(object : DefaultResultHandler<Long>() {
|
||||||
|
override fun onSuccess(vaultId: Long) {
|
||||||
|
finishWithResult(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCancelMissingVaultClicked(vault: Vault) {
|
||||||
|
finishWithResult(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private open class PendingUnlock(private val vault: Vault?) : Serializable {
|
||||||
|
|
||||||
|
private var unlockToken: UnlockToken? = null
|
||||||
|
private var password: String? = null
|
||||||
|
|
||||||
|
var unverifiedVaultConfig: UnverifiedVaultConfig? = null
|
||||||
|
|
||||||
|
fun setUnlockToken(unlockToken: UnlockToken?, presenter: UnlockVaultPresenter) {
|
||||||
|
this.unlockToken = unlockToken
|
||||||
|
continueIfComplete(presenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPassword(password: String?, presenter: UnlockVaultPresenter) {
|
||||||
|
this.password = password
|
||||||
|
continueIfComplete(presenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun continueIfComplete(presenter: UnlockVaultPresenter) {
|
||||||
|
unlockToken?.let { token -> password?.let { password -> presenter.doUnlock(token, password, unverifiedVaultConfig) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun belongsTo(vault: Vault): Boolean {
|
||||||
|
return vault == this.vault
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val NO_OP_PENDING_UNLOCK: PendingUnlock = object : PendingUnlock(null) {
|
||||||
|
override fun continueIfComplete(presenter: UnlockVaultPresenter) {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val PASSWORD = "password"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
unsubscribeOnDestroy( //
|
||||||
|
changePasswordUseCase, //
|
||||||
|
deleteVaultUseCase, //
|
||||||
|
getUnverifiedVaultConfigUseCase, //
|
||||||
|
lockVaultUseCase, //
|
||||||
|
unlockVaultUsingMasterkeyUseCase, //
|
||||||
|
prepareUnlockUseCase, //
|
||||||
|
removeStoredVaultPasswordsUseCase, //
|
||||||
|
saveVaultUseCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,17 +6,13 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import org.cryptomator.data.cloud.crypto.CryptoCloud
|
import org.cryptomator.data.cloud.crypto.CryptoCloud
|
||||||
import org.cryptomator.data.util.NetworkConnectionCheck
|
import org.cryptomator.data.util.NetworkConnectionCheck
|
||||||
import org.cryptomator.domain.Cloud
|
import org.cryptomator.domain.Cloud
|
||||||
import org.cryptomator.domain.CloudFolder
|
import org.cryptomator.domain.CloudFolder
|
||||||
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.authentication.AuthenticationException
|
|
||||||
import org.cryptomator.domain.exception.license.LicenseNotValidException
|
import org.cryptomator.domain.exception.license.LicenseNotValidException
|
||||||
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
|
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
|
||||||
import org.cryptomator.domain.usecases.DoLicenseCheckUseCase
|
import org.cryptomator.domain.usecases.DoLicenseCheckUseCase
|
||||||
@ -27,27 +23,21 @@ import org.cryptomator.domain.usecases.LicenseCheck
|
|||||||
import org.cryptomator.domain.usecases.NoOpResultHandler
|
import org.cryptomator.domain.usecases.NoOpResultHandler
|
||||||
import org.cryptomator.domain.usecases.UpdateCheck
|
import org.cryptomator.domain.usecases.UpdateCheck
|
||||||
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase
|
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase
|
|
||||||
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase
|
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
|
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.LockVaultUseCase
|
import org.cryptomator.domain.usecases.vault.LockVaultUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase
|
import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase
|
||||||
import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase
|
|
||||||
import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase
|
|
||||||
import org.cryptomator.domain.usecases.vault.RenameVaultUseCase
|
import org.cryptomator.domain.usecases.vault.RenameVaultUseCase
|
||||||
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.UnlockVaultUseCase
|
|
||||||
import org.cryptomator.domain.usecases.vault.VaultOrUnlockToken
|
|
||||||
import org.cryptomator.generator.Callback
|
import org.cryptomator.generator.Callback
|
||||||
import org.cryptomator.presentation.BuildConfig
|
import org.cryptomator.presentation.BuildConfig
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||||
import org.cryptomator.presentation.intent.Intents
|
import org.cryptomator.presentation.intent.Intents
|
||||||
|
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||||
import org.cryptomator.presentation.model.CloudModel
|
import org.cryptomator.presentation.model.CloudModel
|
||||||
import org.cryptomator.presentation.model.CloudTypeModel
|
import org.cryptomator.presentation.model.CloudTypeModel
|
||||||
import org.cryptomator.presentation.model.ProgressModel
|
import org.cryptomator.presentation.model.ProgressModel
|
||||||
import org.cryptomator.presentation.model.ProgressStateModel
|
|
||||||
import org.cryptomator.presentation.model.VaultModel
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
||||||
import org.cryptomator.presentation.service.AutoUploadService
|
import org.cryptomator.presentation.service.AutoUploadService
|
||||||
@ -66,7 +56,6 @@ import org.cryptomator.presentation.workflow.CreateNewVaultWorkflow
|
|||||||
import org.cryptomator.presentation.workflow.Workflow
|
import org.cryptomator.presentation.workflow.Workflow
|
||||||
import org.cryptomator.util.Optional
|
import org.cryptomator.util.Optional
|
||||||
import org.cryptomator.util.SharedPreferencesHandler
|
import org.cryptomator.util.SharedPreferencesHandler
|
||||||
import java.io.Serializable
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -77,15 +66,11 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
private val renameVaultUseCase: RenameVaultUseCase, //
|
private val renameVaultUseCase: RenameVaultUseCase, //
|
||||||
private val lockVaultUseCase: LockVaultUseCase, //
|
private val lockVaultUseCase: LockVaultUseCase, //
|
||||||
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
||||||
private val prepareUnlockUseCase: PrepareUnlockUseCase, //
|
|
||||||
private val unlockVaultUseCase: UnlockVaultUseCase, //
|
|
||||||
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
||||||
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
|
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
|
||||||
private val createNewVaultWorkflow: CreateNewVaultWorkflow, //
|
private val createNewVaultWorkflow: CreateNewVaultWorkflow, //
|
||||||
private val saveVaultUseCase: SaveVaultUseCase, //
|
private val saveVaultUseCase: SaveVaultUseCase, //
|
||||||
private val moveVaultPositionUseCase: MoveVaultPositionUseCase, //
|
private val moveVaultPositionUseCase: MoveVaultPositionUseCase, //
|
||||||
private val changePasswordUseCase: ChangePasswordUseCase, //
|
|
||||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
|
|
||||||
private val licenseCheckUseCase: DoLicenseCheckUseCase, //
|
private val licenseCheckUseCase: DoLicenseCheckUseCase, //
|
||||||
private val updateCheckUseCase: DoUpdateCheckUseCase, //
|
private val updateCheckUseCase: DoUpdateCheckUseCase, //
|
||||||
private val updateUseCase: DoUpdateUseCase, //
|
private val updateUseCase: DoUpdateUseCase, //
|
||||||
@ -97,31 +82,14 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
exceptionMappings: ExceptionHandlers) : Presenter<VaultListView>(exceptionMappings) {
|
exceptionMappings: ExceptionHandlers) : Presenter<VaultListView>(exceptionMappings) {
|
||||||
|
|
||||||
private var vaultAction: VaultAction? = null
|
private var vaultAction: VaultAction? = null
|
||||||
private var changedVaultPassword = false
|
|
||||||
private var startedUsingPrepareUnlock = false
|
|
||||||
private var retryUnlockHandler: Handler? = null
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var running = false
|
|
||||||
override fun workflows(): Iterable<Workflow<*>> {
|
override fun workflows(): Iterable<Workflow<*>> {
|
||||||
return listOf(addExistingVaultWorkflow, createNewVaultWorkflow)
|
return listOf(addExistingVaultWorkflow, createNewVaultWorkflow)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun destroyed() {
|
|
||||||
super.destroyed()
|
|
||||||
if (retryUnlockHandler != null) {
|
|
||||||
running = false
|
|
||||||
retryUnlockHandler?.removeCallbacks(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onWindowFocusChanged(hasFocus: Boolean) {
|
fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
if (hasFocus) {
|
if (hasFocus) {
|
||||||
loadVaultList()
|
loadVaultList()
|
||||||
if (retryUnlockHandler != null) {
|
|
||||||
running = false
|
|
||||||
retryUnlockHandler?.removeCallbacks(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,12 +192,8 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteVault(vaultModel: VaultModel) {
|
fun deleteVault(vaultModel: VaultModel) {
|
||||||
deleteVault(vaultModel.toVault())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteVault(vault: Vault) {
|
|
||||||
deleteVaultUseCase //
|
deleteVaultUseCase //
|
||||||
.withVault(vault) //
|
.withVault(vaultModel.toVault()) //
|
||||||
.run(object : DefaultResultHandler<Long>() {
|
.run(object : DefaultResultHandler<Long>() {
|
||||||
override fun onSuccess(vaultId: Long) {
|
override fun onSuccess(vaultId: Long) {
|
||||||
view?.deleteVaultFromAdapter(vaultId)
|
view?.deleteVaultFromAdapter(vaultId)
|
||||||
@ -264,11 +228,6 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
renameVault(VaultModel(vaultWithUpdatedCloud), newVaultName)
|
renameVault(VaultModel(vaultWithUpdatedCloud), newVaultName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUnlockCanceled() {
|
|
||||||
prepareUnlockUseCase.unsubscribe()
|
|
||||||
unlockVaultUseCase.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun browseFilesOf(vault: VaultModel) {
|
private fun browseFilesOf(vault: VaultModel) {
|
||||||
getDecryptedCloudForVaultUseCase //
|
getDecryptedCloudForVaultUseCase //
|
||||||
.withVault(vault.toVault()) //
|
.withVault(vault.toVault()) //
|
||||||
@ -325,7 +284,6 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onVaultClicked(vault: VaultModel) {
|
fun onVaultClicked(vault: VaultModel) {
|
||||||
startedUsingPrepareUnlock = sharedPreferencesHandler.backgroundUnlockPreparation()
|
|
||||||
startVaultAction(vault, VaultAction.UNLOCK)
|
startVaultAction(vault, VaultAction.UNLOCK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,7 +343,6 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
when (vaultAction) {
|
when (vaultAction) {
|
||||||
VaultAction.UNLOCK -> requireUserAuthentication(authenticatedVaultModel)
|
VaultAction.UNLOCK -> requireUserAuthentication(authenticatedVaultModel)
|
||||||
VaultAction.RENAME -> view?.showRenameDialog(authenticatedVaultModel)
|
VaultAction.RENAME -> view?.showRenameDialog(authenticatedVaultModel)
|
||||||
VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(authenticatedVaultModel)
|
|
||||||
}
|
}
|
||||||
vaultAction = null
|
vaultAction = null
|
||||||
}
|
}
|
||||||
@ -394,83 +351,22 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
view?.addOrUpdateVault(authenticatedVault)
|
view?.addOrUpdateVault(authenticatedVault)
|
||||||
if (authenticatedVault.isLocked) {
|
if (authenticatedVault.isLocked) {
|
||||||
if (!isPaused) {
|
if (!isPaused) {
|
||||||
if (canUseBiometricOn(authenticatedVault)) {
|
requestActivityResult( //
|
||||||
if (startedUsingPrepareUnlock) {
|
ActivityResultCallbacks.vaultUnlockedVaultList(), //
|
||||||
startPrepareUnlockUseCase(authenticatedVault.toVault())
|
Intents.unlockVaultIntent().withVaultModel(authenticatedVault).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK))
|
||||||
}
|
|
||||||
view?.showBiometricDialog(authenticatedVault)
|
|
||||||
} else {
|
|
||||||
startPrepareUnlockUseCase(authenticatedVault.toVault())
|
|
||||||
view?.showEnterPasswordDialog(authenticatedVault)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
browseFilesOf(authenticatedVault)
|
browseFilesOf(authenticatedVault)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startPrepareUnlockUseCase(vault: Vault) {
|
@Callback
|
||||||
pendingUnlock = null
|
fun vaultUnlockedVaultList(result: ActivityResult) {
|
||||||
prepareUnlockUseCase //
|
val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud
|
||||||
.withVault(vault) //
|
when {
|
||||||
.run(object : DefaultResultHandler<UnlockToken>() {
|
result.isResultOk -> navigateToVaultContent(cloud)
|
||||||
override fun onSuccess(unlockToken: UnlockToken) {
|
else -> TODO("Not yet implemented")
|
||||||
if (!startedUsingPrepareUnlock && vault.password != null) {
|
}
|
||||||
doUnlock(unlockToken, vault.password)
|
|
||||||
} else {
|
|
||||||
unlockTokenObtained(unlockToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
if (e is AuthenticationException) {
|
|
||||||
view?.cancelBasicAuthIfRunning()
|
|
||||||
}
|
|
||||||
if (!authenticationExceptionHandler.handleAuthenticationException(this@VaultListPresenter, e, ActivityResultCallbacks.authenticatedAfterUnlock(vault))) {
|
|
||||||
super.onError(e)
|
|
||||||
if (e is NetworkConnectionException) {
|
|
||||||
running = true
|
|
||||||
retryUnlockHandler = Handler()
|
|
||||||
restartUnlockUseCase(vault)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restartUnlockUseCase(vault: Vault) {
|
|
||||||
retryUnlockHandler?.postDelayed({
|
|
||||||
if (running) {
|
|
||||||
prepareUnlockUseCase //
|
|
||||||
.withVault(vault) //
|
|
||||||
.run(object : DefaultResultHandler<UnlockToken>() {
|
|
||||||
override fun onSuccess(unlockToken: UnlockToken) {
|
|
||||||
if (!startedUsingPrepareUnlock && vault.password != null) {
|
|
||||||
doUnlock(unlockToken, vault.password)
|
|
||||||
} else {
|
|
||||||
unlockTokenObtained(unlockToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
if (e is NetworkConnectionException) {
|
|
||||||
restartUnlockUseCase(vault)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doUnlock(token: UnlockToken, password: String) {
|
|
||||||
unlockVaultUseCase //
|
|
||||||
.withVaultOrUnlockToken(VaultOrUnlockToken.from(token)) //
|
|
||||||
.andPassword(password) //
|
|
||||||
.run(object : DefaultResultHandler<Cloud>() {
|
|
||||||
override fun onSuccess(cloud: Cloud) {
|
|
||||||
navigateToVaultContent(cloud)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToVaultContent(cloud: Cloud) {
|
private fun navigateToVaultContent(cloud: Cloud) {
|
||||||
@ -495,53 +391,6 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unlockTokenObtained(unlockToken: UnlockToken) {
|
|
||||||
pendingUnlockFor(unlockToken.vault)?.setUnlockToken(unlockToken, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onUnlockClick(vault: VaultModel, password: String?) {
|
|
||||||
view?.showProgress(ProgressModel.GENERIC)
|
|
||||||
pendingUnlockFor(vault.toVault())?.setPassword(password, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var pendingUnlock: PendingUnlock? = null
|
|
||||||
private fun pendingUnlockFor(vault: Vault): PendingUnlock? {
|
|
||||||
if (pendingUnlock == null) {
|
|
||||||
pendingUnlock = PendingUnlock(vault)
|
|
||||||
}
|
|
||||||
return if (pendingUnlock?.belongsTo(vault) == true) {
|
|
||||||
pendingUnlock
|
|
||||||
} else {
|
|
||||||
PendingUnlock.NO_OP_PENDING_UNLOCK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Callback(dispatchResultOkOnly = false)
|
|
||||||
fun authenticatedAfterUnlock(result: ActivityResult, vault: Vault) {
|
|
||||||
if (result.isResultOk) {
|
|
||||||
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
|
||||||
if (startedUsingPrepareUnlock) {
|
|
||||||
startPrepareUnlockUseCase(Vault.aCopyOf(vault).withCloud(cloud).build())
|
|
||||||
if (view?.stoppedBiometricAuthDuringCloudAuthentication() == true) {
|
|
||||||
view?.showBiometricDialog(VaultModel(vault))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
view?.showProgress(ProgressModel.GENERIC)
|
|
||||||
startPrepareUnlockUseCase(vault)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
view?.closeDialog()
|
|
||||||
val error = result.getSingleResult(Throwable::class.java)
|
|
||||||
error?.let { showError(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun canUseBiometricOn(vault: VaultModel): Boolean {
|
|
||||||
return vault.password != null && BiometricManager //
|
|
||||||
.from(context()) //
|
|
||||||
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAddExistingVault() {
|
fun onAddExistingVault() {
|
||||||
addExistingVaultWorkflow.start()
|
addExistingVaultWorkflow.start()
|
||||||
}
|
}
|
||||||
@ -557,60 +406,11 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onChangePasswordClicked(vaultModel: VaultModel) {
|
fun onChangePasswordClicked(vaultModel: VaultModel) {
|
||||||
startVaultAction(vaultModel, VaultAction.CHANGE_PASSWORD)
|
Intents
|
||||||
}
|
.unlockVaultIntent()
|
||||||
|
.withVaultModel(vaultModel)
|
||||||
fun onChangePasswordClicked(vaultModel: VaultModel, oldPassword: String?, newPassword: String?) {
|
.withVaultAction(UnlockVaultIntent.VaultAction.CHANGE_PASSWORD)
|
||||||
view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD))
|
.startActivity(this)
|
||||||
changePasswordUseCase.withVault(vaultModel.toVault()) //
|
|
||||||
.andOldPassword(oldPassword) //
|
|
||||||
.andNewPassword(newPassword) //
|
|
||||||
.run(object : DefaultResultHandler<Void?>() {
|
|
||||||
override fun onSuccess(void: Void?) {
|
|
||||||
view?.showProgress(ProgressModel.COMPLETED)
|
|
||||||
view?.showMessage(R.string.screen_vault_list_change_password_successful)
|
|
||||||
if (canUseBiometricOn(vaultModel)) {
|
|
||||||
changedVaultPassword = true
|
|
||||||
view?.getEncryptedPasswordWithBiometricAuthentication(VaultModel( //
|
|
||||||
Vault.aCopyOf(vaultModel.toVault()) //
|
|
||||||
.withSavedPassword(newPassword) //
|
|
||||||
.build()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
if (!authenticationExceptionHandler.handleAuthenticationException( //
|
|
||||||
this@VaultListPresenter, e, //
|
|
||||||
ActivityResultCallbacks.changePasswordAfterAuthentication(vaultModel.toVault(), oldPassword, newPassword))) {
|
|
||||||
showError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Callback
|
|
||||||
fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault?, oldPassword: String?, newPassword: String?) {
|
|
||||||
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
|
||||||
val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build()
|
|
||||||
onChangePasswordClicked(VaultModel(vaultWithUpdatedCloud), oldPassword, newPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save(vaultModel: VaultModel) {
|
|
||||||
saveVaultUseCase //
|
|
||||||
.withVault(vaultModel.toVault()) //
|
|
||||||
.run(DefaultResultHandler())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBiometricKeyInvalidated() {
|
|
||||||
removeStoredVaultPasswordsUseCase.run(object : DefaultResultHandler<Void?>() {
|
|
||||||
override fun onSuccess(void: Void?) {
|
|
||||||
view?.showBiometricAuthKeyInvalidatedDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
Timber.tag("VaultListPresenter").e(e, "Error while removing vault passwords")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onVaultSettingsClicked(vaultModel: VaultModel) {
|
fun onVaultSettingsClicked(vaultModel: VaultModel) {
|
||||||
@ -636,10 +436,6 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeleteMissingVaultClicked(vault: Vault) {
|
|
||||||
deleteVault(vault)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFilteredTouchEventForSecurity() {
|
fun onFilteredTouchEventForSecurity() {
|
||||||
view?.showDialog(AppIsObscuredInfoDialog.newInstance())
|
view?.showDialog(AppIsObscuredInfoDialog.newInstance())
|
||||||
}
|
}
|
||||||
@ -663,26 +459,8 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBiometricAuthenticationSucceeded(vaultModel: VaultModel) {
|
|
||||||
if (changedVaultPassword) {
|
|
||||||
changedVaultPassword = false
|
|
||||||
save(vaultModel)
|
|
||||||
} else {
|
|
||||||
if (startedUsingPrepareUnlock) {
|
|
||||||
onUnlockClick(vaultModel, vaultModel.password)
|
|
||||||
} else {
|
|
||||||
view?.showProgress(ProgressModel.GENERIC)
|
|
||||||
startPrepareUnlockUseCase(vaultModel.toVault())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun useConfirmationInFaceUnlockBiometricAuthentication(): Boolean {
|
|
||||||
return sharedPreferencesHandler.useConfirmationInFaceUnlockBiometricAuthentication()
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class VaultAction {
|
private enum class VaultAction {
|
||||||
UNLOCK, RENAME, CHANGE_PASSWORD
|
UNLOCK, RENAME
|
||||||
}
|
}
|
||||||
|
|
||||||
fun installUpdate() {
|
fun installUpdate() {
|
||||||
@ -707,43 +485,6 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startedUsingPrepareUnlock(): Boolean {
|
|
||||||
return startedUsingPrepareUnlock
|
|
||||||
}
|
|
||||||
|
|
||||||
private open class PendingUnlock(private val vault: Vault?) : Serializable {
|
|
||||||
|
|
||||||
private var unlockToken: UnlockToken? = null
|
|
||||||
private var password: String? = null
|
|
||||||
|
|
||||||
fun setUnlockToken(unlockToken: UnlockToken?, presenter: VaultListPresenter) {
|
|
||||||
this.unlockToken = unlockToken
|
|
||||||
continueIfComplete(presenter)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPassword(password: String?, presenter: VaultListPresenter) {
|
|
||||||
this.password = password
|
|
||||||
continueIfComplete(presenter)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun continueIfComplete(presenter: VaultListPresenter) {
|
|
||||||
unlockToken?.let { token -> password?.let { password -> presenter.doUnlock(token, password) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun belongsTo(vault: Vault): Boolean {
|
|
||||||
return vault == this.vault
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val NO_OP_PENDING_UNLOCK: PendingUnlock = object : PendingUnlock(null) {
|
|
||||||
override fun continueIfComplete(presenter: VaultListPresenter) {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
unsubscribeOnDestroy( //
|
unsubscribeOnDestroy( //
|
||||||
deleteVaultUseCase, //
|
deleteVaultUseCase, //
|
||||||
@ -752,9 +493,6 @@ class VaultListPresenter @Inject constructor( //
|
|||||||
getVaultListUseCase, //
|
getVaultListUseCase, //
|
||||||
saveVaultUseCase, //
|
saveVaultUseCase, //
|
||||||
moveVaultPositionUseCase, //
|
moveVaultPositionUseCase, //
|
||||||
removeStoredVaultPasswordsUseCase, //
|
|
||||||
unlockVaultUseCase, //
|
|
||||||
prepareUnlockUseCase, //
|
|
||||||
licenseCheckUseCase, //
|
licenseCheckUseCase, //
|
||||||
updateCheckUseCase, //
|
updateCheckUseCase, //
|
||||||
updateUseCase)
|
updateUseCase)
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package org.cryptomator.presentation.ui.activity
|
package org.cryptomator.presentation.ui.activity
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import org.cryptomator.generator.Activity
|
import org.cryptomator.generator.Activity
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
@ -9,20 +7,15 @@ import org.cryptomator.presentation.model.CloudFolderModel
|
|||||||
import org.cryptomator.presentation.model.VaultModel
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
import org.cryptomator.presentation.presenter.AutoUploadChooseVaultPresenter
|
import org.cryptomator.presentation.presenter.AutoUploadChooseVaultPresenter
|
||||||
import org.cryptomator.presentation.ui.activity.view.AutoUploadChooseVaultView
|
import org.cryptomator.presentation.ui.activity.view.AutoUploadChooseVaultView
|
||||||
import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.NotEnoughVaultsDialog
|
import org.cryptomator.presentation.ui.dialog.NotEnoughVaultsDialog
|
||||||
import org.cryptomator.presentation.ui.fragment.AutoUploadChooseVaultFragment
|
import org.cryptomator.presentation.ui.fragment.AutoUploadChooseVaultFragment
|
||||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
||||||
|
|
||||||
@Activity
|
@Activity
|
||||||
class AutoUploadChooseVaultActivity : BaseActivity(), //
|
class AutoUploadChooseVaultActivity : BaseActivity(), //
|
||||||
AutoUploadChooseVaultView, //
|
AutoUploadChooseVaultView, //
|
||||||
NotEnoughVaultsDialog.Callback, //
|
NotEnoughVaultsDialog.Callback {
|
||||||
EnterPasswordDialog.Callback,
|
|
||||||
BiometricAuthentication.Callback {
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var presenter: AutoUploadChooseVaultPresenter
|
lateinit var presenter: AutoUploadChooseVaultPresenter
|
||||||
@ -40,7 +33,7 @@ class AutoUploadChooseVaultActivity : BaseActivity(), //
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createFragment(): Fragment? = AutoUploadChooseVaultFragment()
|
override fun createFragment(): Fragment = AutoUploadChooseVaultFragment()
|
||||||
|
|
||||||
|
|
||||||
override fun displayVaults(vaults: List<VaultModel>) {
|
override fun displayVaults(vaults: List<VaultModel>) {
|
||||||
@ -69,41 +62,5 @@ class AutoUploadChooseVaultActivity : BaseActivity(), //
|
|||||||
autoUploadChooseVaultFragment().showChosenLocation(location)
|
autoUploadChooseVaultFragment().showChosenLocation(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnlockCanceled() {
|
|
||||||
presenter.onUnlockCanceled()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnlockClick(vaultModel: VaultModel, password: String) {
|
|
||||||
presenter.onUnlockPressed(vaultModel, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
override fun showEnterPasswordDialog(vaultModel: VaultModel) {
|
|
||||||
if (vaultWithBiometricAuthEnabled(vaultModel)) {
|
|
||||||
BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.DECRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication())
|
|
||||||
.startListening(autoUploadChooseVaultFragment(), vaultModel)
|
|
||||||
} else {
|
|
||||||
showDialog(EnterPasswordDialog.newInstance(vaultModel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricAuthenticated(vault: VaultModel) {
|
|
||||||
presenter.onUnlockPressed(vault, vault.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricAuthenticationFailed(vault: VaultModel) {
|
|
||||||
showDialog(EnterPasswordDialog.newInstance(vault))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricKeyInvalidated(vault: VaultModel) {
|
|
||||||
presenter.onBiometricKeyInvalidated(vault)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showBiometricAuthKeyInvalidatedDialog() {
|
|
||||||
showDialog(BiometricAuthKeyInvalidatedDialog.newInstance())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun vaultWithBiometricAuthEnabled(vault: VaultModel): Boolean = vault.password != null
|
|
||||||
|
|
||||||
private fun autoUploadChooseVaultFragment(): AutoUploadChooseVaultFragment = getCurrentFragment(R.id.fragmentContainer) as AutoUploadChooseVaultFragment
|
private fun autoUploadChooseVaultFragment(): AutoUploadChooseVaultFragment = getCurrentFragment(R.id.fragmentContainer) as AutoUploadChooseVaultFragment
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,20 @@
|
|||||||
package org.cryptomator.presentation.ui.activity
|
package org.cryptomator.presentation.ui.activity
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import org.cryptomator.domain.Vault
|
|
||||||
import org.cryptomator.generator.Activity
|
import org.cryptomator.generator.Activity
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
import org.cryptomator.presentation.model.VaultModel
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
import org.cryptomator.presentation.presenter.BiometricAuthSettingsPresenter
|
import org.cryptomator.presentation.presenter.BiometricAuthSettingsPresenter
|
||||||
import org.cryptomator.presentation.ui.activity.view.BiometricAuthSettingsView
|
import org.cryptomator.presentation.ui.activity.view.BiometricAuthSettingsView
|
||||||
import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.EnrollSystemBiometricDialog
|
import org.cryptomator.presentation.ui.dialog.EnrollSystemBiometricDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
|
||||||
import org.cryptomator.presentation.ui.fragment.BiometricAuthSettingsFragment
|
import org.cryptomator.presentation.ui.fragment.BiometricAuthSettingsFragment
|
||||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
||||||
|
|
||||||
@Activity
|
@Activity
|
||||||
class BiometricAuthSettingsActivity : BaseActivity(), //
|
class BiometricAuthSettingsActivity : BaseActivity(), //
|
||||||
EnterPasswordDialog.Callback, //
|
|
||||||
BiometricAuthSettingsView, //
|
BiometricAuthSettingsView, //
|
||||||
BiometricAuthentication.Callback, //
|
|
||||||
EnrollSystemBiometricDialog.Callback {
|
EnrollSystemBiometricDialog.Callback {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@ -44,10 +36,6 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showBiometricAuthKeyInvalidatedDialog() {
|
|
||||||
showDialog(BiometricAuthKeyInvalidatedDialog.newInstance())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFragment(): Fragment? = BiometricAuthSettingsFragment()
|
override fun createFragment(): Fragment? = BiometricAuthSettingsFragment()
|
||||||
|
|
||||||
override fun renderVaultList(vaultModelCollection: List<VaultModel>) {
|
override fun renderVaultList(vaultModelCollection: List<VaultModel>) {
|
||||||
@ -58,30 +46,6 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
|
|||||||
biometricAuthSettingsFragment().clearVaultList()
|
biometricAuthSettingsFragment().clearVaultList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showEnterPasswordDialog(vaultModel: VaultModel) {
|
|
||||||
showDialog(EnterPasswordDialog.newInstance(vaultModel))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnlockClick(vaultModel: VaultModel, password: String) {
|
|
||||||
val vaultModelWithSavedPassword = VaultModel( //
|
|
||||||
Vault //
|
|
||||||
.aCopyOf(vaultModel.toVault()) //
|
|
||||||
.withSavedPassword(password) //
|
|
||||||
.build())
|
|
||||||
|
|
||||||
presenter.verifyPassword(vaultModelWithSavedPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnlockCanceled() {
|
|
||||||
presenter.onUnlockCanceled()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
override fun showBiometricAuthenticationDialog(vaultModel: VaultModel) {
|
|
||||||
BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.ENCRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication())
|
|
||||||
.startListening(biometricAuthSettingsFragment(), vaultModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun biometricAuthSettingsFragment(): BiometricAuthSettingsFragment = getCurrentFragment(R.id.fragmentContainer) as BiometricAuthSettingsFragment
|
private fun biometricAuthSettingsFragment(): BiometricAuthSettingsFragment = getCurrentFragment(R.id.fragmentContainer) as BiometricAuthSettingsFragment
|
||||||
|
|
||||||
override fun onSetupBiometricAuthInSystemClicked() {
|
override fun onSetupBiometricAuthInSystemClicked() {
|
||||||
@ -91,17 +55,4 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
|
|||||||
override fun onCancelSetupBiometricAuthInSystemClicked() {
|
override fun onCancelSetupBiometricAuthInSystemClicked() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBiometricAuthenticated(vault: VaultModel) {
|
|
||||||
presenter.saveVault(vault.toVault())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricAuthenticationFailed(vault: VaultModel) {
|
|
||||||
showError(getString(R.string.error_biometric_auth_aborted))
|
|
||||||
biometricAuthSettingsFragment().addOrUpdateVault(vault)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricKeyInvalidated(vault: VaultModel) {
|
|
||||||
presenter.onBiometricAuthKeyInvalidated(vault)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package org.cryptomator.presentation.ui.activity
|
package org.cryptomator.presentation.ui.activity
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
@ -228,9 +227,6 @@ class BrowseFilesActivity : BaseActivity(), //
|
|||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||||
if (isNavigationMode(SELECT_ITEMS)) {
|
if (isNavigationMode(SELECT_ITEMS)) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
menu.findItem(R.id.action_export_items).isVisible = false
|
|
||||||
}
|
|
||||||
menu.findItem(R.id.action_delete_items).isEnabled = enableGeneralSelectionActions
|
menu.findItem(R.id.action_delete_items).isEnabled = enableGeneralSelectionActions
|
||||||
menu.findItem(R.id.action_move_items).isEnabled = enableGeneralSelectionActions
|
menu.findItem(R.id.action_move_items).isEnabled = enableGeneralSelectionActions
|
||||||
menu.findItem(R.id.action_export_items).isEnabled = enableGeneralSelectionActions
|
menu.findItem(R.id.action_export_items).isEnabled = enableGeneralSelectionActions
|
||||||
|
@ -5,8 +5,6 @@ import android.content.Intent
|
|||||||
import android.content.Intent.ACTION_SEND
|
import android.content.Intent.ACTION_SEND
|
||||||
import android.content.Intent.ACTION_SEND_MULTIPLE
|
import android.content.Intent.ACTION_SEND_MULTIPLE
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import org.cryptomator.generator.Activity
|
import org.cryptomator.generator.Activity
|
||||||
import org.cryptomator.presentation.R
|
import org.cryptomator.presentation.R
|
||||||
@ -16,13 +14,10 @@ import org.cryptomator.presentation.model.SharedFileModel
|
|||||||
import org.cryptomator.presentation.model.VaultModel
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
import org.cryptomator.presentation.presenter.SharedFilesPresenter
|
import org.cryptomator.presentation.presenter.SharedFilesPresenter
|
||||||
import org.cryptomator.presentation.ui.activity.view.SharedFilesView
|
import org.cryptomator.presentation.ui.activity.view.SharedFilesView
|
||||||
import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.NotEnoughVaultsDialog
|
import org.cryptomator.presentation.ui.dialog.NotEnoughVaultsDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.ReplaceDialog
|
import org.cryptomator.presentation.ui.dialog.ReplaceDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog
|
import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog
|
||||||
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment
|
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment
|
||||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
|
||||||
import java.lang.String.format
|
import java.lang.String.format
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -32,8 +27,6 @@ import timber.log.Timber
|
|||||||
@Activity
|
@Activity
|
||||||
class SharedFilesActivity : BaseActivity(), //
|
class SharedFilesActivity : BaseActivity(), //
|
||||||
SharedFilesView, //
|
SharedFilesView, //
|
||||||
EnterPasswordDialog.Callback, //
|
|
||||||
BiometricAuthentication.Callback, //
|
|
||||||
ReplaceDialog.Callback, //
|
ReplaceDialog.Callback, //
|
||||||
NotEnoughVaultsDialog.Callback, //
|
NotEnoughVaultsDialog.Callback, //
|
||||||
UploadCloudFileDialog.Callback {
|
UploadCloudFileDialog.Callback {
|
||||||
@ -126,7 +119,7 @@ class SharedFilesActivity : BaseActivity(), //
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createFragment(): Fragment? = SharedFilesFragment()
|
override fun createFragment(): Fragment = SharedFilesFragment()
|
||||||
|
|
||||||
public override fun onMenuItemSelected(itemId: Int): Boolean = when (itemId) {
|
public override fun onMenuItemSelected(itemId: Int): Boolean = when (itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
@ -150,16 +143,6 @@ class SharedFilesActivity : BaseActivity(), //
|
|||||||
|
|
||||||
private fun sharedFilesFragment(): SharedFilesFragment = getCurrentFragment(R.id.fragmentContainer) as SharedFilesFragment
|
private fun sharedFilesFragment(): SharedFilesFragment = getCurrentFragment(R.id.fragmentContainer) as SharedFilesFragment
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
override fun showEnterPasswordDialog(vault: VaultModel) {
|
|
||||||
if (vaultWithBiometricAuthEnabled(vault)) {
|
|
||||||
BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.DECRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication())
|
|
||||||
.startListening(sharedFilesFragment(), vault)
|
|
||||||
} else {
|
|
||||||
showDialog(EnterPasswordDialog.newInstance(vault))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showReplaceDialog(existingFiles: List<String>, size: Int) {
|
override fun showReplaceDialog(existingFiles: List<String>, size: Int) {
|
||||||
ReplaceDialog.withContext(this).show(existingFiles, size)
|
ReplaceDialog.withContext(this).show(existingFiles, size)
|
||||||
}
|
}
|
||||||
@ -168,22 +151,10 @@ class SharedFilesActivity : BaseActivity(), //
|
|||||||
sharedFilesFragment().showChosenLocation(folder)
|
sharedFilesFragment().showChosenLocation(folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showBiometricAuthKeyInvalidatedDialog() {
|
|
||||||
showDialog(BiometricAuthKeyInvalidatedDialog.newInstance())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showUploadDialog(uploadingFiles: Int) {
|
override fun showUploadDialog(uploadingFiles: Int) {
|
||||||
showDialog(UploadCloudFileDialog.newInstance(uploadingFiles))
|
showDialog(UploadCloudFileDialog.newInstance(uploadingFiles))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnlockClick(vaultModel: VaultModel, password: String) {
|
|
||||||
presenter.onUnlockPressed(vaultModel, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnlockCanceled() {
|
|
||||||
presenter.onUnlockCanceled()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReplacePositiveClicked() {
|
override fun onReplacePositiveClicked() {
|
||||||
presenter.onReplaceExistingFilesPressed()
|
presenter.onReplaceExistingFilesPressed()
|
||||||
}
|
}
|
||||||
@ -209,20 +180,6 @@ class SharedFilesActivity : BaseActivity(), //
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBiometricAuthenticated(vault: VaultModel) {
|
|
||||||
presenter.onUnlockPressed(vault, vault.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricAuthenticationFailed(vault: VaultModel) {
|
|
||||||
showDialog(EnterPasswordDialog.newInstance(vault))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricKeyInvalidated(vault: VaultModel) {
|
|
||||||
presenter.onBiometricAuthKeyInvalidated()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun vaultWithBiometricAuthEnabled(vault: VaultModel): Boolean = vault.password != null
|
|
||||||
|
|
||||||
override fun onUploadCanceled() {
|
override fun onUploadCanceled() {
|
||||||
presenter.onUploadCanceled()
|
presenter.onUploadCanceled()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,121 @@
|
|||||||
|
package org.cryptomator.presentation.ui.activity
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
|
import org.cryptomator.domain.Vault
|
||||||
|
import org.cryptomator.generator.Activity
|
||||||
|
import org.cryptomator.generator.InjectIntent
|
||||||
|
import org.cryptomator.presentation.R
|
||||||
|
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||||
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
|
import org.cryptomator.presentation.presenter.UnlockVaultPresenter
|
||||||
|
import org.cryptomator.presentation.ui.activity.view.UnlockVaultView
|
||||||
|
import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog
|
||||||
|
import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog
|
||||||
|
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.util.BiometricAuthentication
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Activity(layout = R.layout.activity_unlock_vault)
|
||||||
|
class UnlockVaultActivity : BaseActivity(), //
|
||||||
|
UnlockVaultView, //
|
||||||
|
BiometricAuthentication.Callback,
|
||||||
|
ChangePasswordDialog.Callback,
|
||||||
|
VaultNotFoundDialog.Callback {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var presenter: UnlockVaultPresenter
|
||||||
|
|
||||||
|
@InjectIntent
|
||||||
|
lateinit var unlockVaultIntent: UnlockVaultIntent
|
||||||
|
|
||||||
|
|
||||||
|
private lateinit var biometricAuthentication: BiometricAuthentication
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
super.finish()
|
||||||
|
overridePendingTransition(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showEnterPasswordDialog(vault: VaultModel) {
|
||||||
|
showDialog(EnterPasswordDialog.newInstance(vault))
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
override fun showBiometricDialog(vault: VaultModel) {
|
||||||
|
biometricAuthentication = BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.DECRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication())
|
||||||
|
biometricAuthentication.startListening(unlockVaultFragment(), vault)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
override fun getEncryptedPasswordWithBiometricAuthentication(vaultModel: VaultModel) {
|
||||||
|
biometricAuthentication = BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.ENCRYPT, presenter.useConfirmationInFaceUnlockBiometricAuthentication())
|
||||||
|
biometricAuthentication.startListening(unlockVaultFragment(), vaultModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showBiometricAuthKeyInvalidatedDialog() {
|
||||||
|
showDialog(BiometricAuthKeyInvalidatedDialog.newInstance())
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
override fun cancelBasicAuthIfRunning() {
|
||||||
|
biometricAuthentication.stopListening()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
override fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean {
|
||||||
|
return biometricAuthentication.stoppedBiometricAuthDuringCloudAuthentication()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnlockClick(vaultModel: VaultModel, password: String) {
|
||||||
|
presenter.onUnlockClick(vaultModel, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnlockCanceled() {
|
||||||
|
presenter.onUnlockCanceled()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBiometricAuthenticated(vault: VaultModel) {
|
||||||
|
presenter.onBiometricAuthenticationSucceeded(vault)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBiometricAuthenticationFailed(vault: VaultModel) {
|
||||||
|
val vaultWithoutPassword = Vault.aCopyOf(vault.toVault()).withSavedPassword(null).build()
|
||||||
|
when(unlockVaultIntent.vaultAction()) {
|
||||||
|
UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> presenter.saveVaultAfterChangePasswordButFailedBiometricAuth(vaultWithoutPassword)
|
||||||
|
else -> {
|
||||||
|
if (!presenter.startedUsingPrepareUnlock()) {
|
||||||
|
presenter.startPrepareUnlockUseCase(vaultWithoutPassword)
|
||||||
|
}
|
||||||
|
showEnterPasswordDialog(VaultModel(vaultWithoutPassword))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBiometricKeyInvalidated(vault: VaultModel) {
|
||||||
|
presenter.onBiometricKeyInvalidated()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unlockVaultFragment(): UnlockVaultFragment = //
|
||||||
|
getCurrentFragment(R.id.fragmentContainer) as UnlockVaultFragment
|
||||||
|
|
||||||
|
override fun showChangePasswordDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?) {
|
||||||
|
showDialog(ChangePasswordDialog.newInstance(vaultModel, unverifiedVaultConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String) {
|
||||||
|
presenter.onChangePasswordClick(vaultModel, unverifiedVaultConfig, oldPassword, newPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteMissingVaultClicked(vault: Vault) {
|
||||||
|
presenter.onDeleteMissingVaultClicked(vault)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelMissingVaultClicked(vault: Vault) {
|
||||||
|
presenter.onCancelMissingVaultClicked(vault)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,11 +2,8 @@ package org.cryptomator.presentation.ui.activity
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
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
|
||||||
import org.cryptomator.presentation.CryptomatorApp
|
import org.cryptomator.presentation.CryptomatorApp
|
||||||
@ -25,17 +22,12 @@ import org.cryptomator.presentation.ui.bottomsheet.SettingsVaultBottomSheet
|
|||||||
import org.cryptomator.presentation.ui.callback.VaultListCallback
|
import org.cryptomator.presentation.ui.callback.VaultListCallback
|
||||||
import org.cryptomator.presentation.ui.dialog.AskForLockScreenDialog
|
import org.cryptomator.presentation.ui.dialog.AskForLockScreenDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.BetaConfirmationDialog
|
import org.cryptomator.presentation.ui.dialog.BetaConfirmationDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog
|
import org.cryptomator.presentation.ui.dialog.UpdateAppAvailableDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.UpdateAppDialog
|
import org.cryptomator.presentation.ui.dialog.UpdateAppDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog
|
import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.VaultNotFoundDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.VaultRenameDialog
|
import org.cryptomator.presentation.ui.dialog.VaultRenameDialog
|
||||||
import org.cryptomator.presentation.ui.fragment.VaultListFragment
|
import org.cryptomator.presentation.ui.fragment.VaultListFragment
|
||||||
import org.cryptomator.presentation.ui.layout.ObscuredAwareCoordinatorLayout.Listener
|
import org.cryptomator.presentation.ui.layout.ObscuredAwareCoordinatorLayout.Listener
|
||||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.android.synthetic.main.activity_layout_obscure_aware.activityRootView
|
import kotlinx.android.synthetic.main.activity_layout_obscure_aware.activityRootView
|
||||||
@ -45,10 +37,7 @@ import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
|||||||
class VaultListActivity : BaseActivity(), //
|
class VaultListActivity : BaseActivity(), //
|
||||||
VaultListView, //
|
VaultListView, //
|
||||||
VaultListCallback, //
|
VaultListCallback, //
|
||||||
BiometricAuthentication.Callback, //
|
|
||||||
AskForLockScreenDialog.Callback, //
|
AskForLockScreenDialog.Callback, //
|
||||||
ChangePasswordDialog.Callback, //
|
|
||||||
VaultNotFoundDialog.Callback,
|
|
||||||
UpdateAppAvailableDialog.Callback, //
|
UpdateAppAvailableDialog.Callback, //
|
||||||
UpdateAppDialog.Callback, //
|
UpdateAppDialog.Callback, //
|
||||||
BetaConfirmationDialog.Callback {
|
BetaConfirmationDialog.Callback {
|
||||||
@ -59,8 +48,6 @@ class VaultListActivity : BaseActivity(), //
|
|||||||
@InjectIntent
|
@InjectIntent
|
||||||
lateinit var vaultListIntent: VaultListIntent
|
lateinit var vaultListIntent: VaultListIntent
|
||||||
|
|
||||||
private var biometricAuthentication: BiometricAuthentication? = null
|
|
||||||
|
|
||||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
super.onWindowFocusChanged(hasFocus)
|
super.onWindowFocusChanged(hasFocus)
|
||||||
vaultListPresenter.onWindowFocusChanged(hasFocus)
|
vaultListPresenter.onWindowFocusChanged(hasFocus)
|
||||||
@ -125,40 +112,6 @@ class VaultListActivity : BaseActivity(), //
|
|||||||
showDialog(VaultRenameDialog.newInstance(vaultModel))
|
showDialog(VaultRenameDialog.newInstance(vaultModel))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showEnterPasswordDialog(vault: VaultModel) {
|
|
||||||
showDialog(EnterPasswordDialog.newInstance(vault))
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
override fun showBiometricDialog(vault: VaultModel) {
|
|
||||||
biometricAuthentication = BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.DECRYPT, vaultListPresenter.useConfirmationInFaceUnlockBiometricAuthentication())
|
|
||||||
biometricAuthentication?.startListening(vaultListFragment(), vault)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showChangePasswordDialog(vaultModel: VaultModel) {
|
|
||||||
showDialog(ChangePasswordDialog.newInstance(vaultModel))
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
override fun getEncryptedPasswordWithBiometricAuthentication(vaultModel: VaultModel) {
|
|
||||||
biometricAuthentication = BiometricAuthentication(this, context(), BiometricAuthentication.CryptoMode.ENCRYPT, vaultListPresenter.useConfirmationInFaceUnlockBiometricAuthentication())
|
|
||||||
biometricAuthentication?.startListening(vaultListFragment(), vaultModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showBiometricAuthKeyInvalidatedDialog() {
|
|
||||||
showDialog(BiometricAuthKeyInvalidatedDialog.newInstance())
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
override fun cancelBasicAuthIfRunning() {
|
|
||||||
biometricAuthentication?.stopListening()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
override fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean {
|
|
||||||
return biometricAuthentication?.stoppedBiometricAuthDuringCloudAuthentication() == true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun rowMoved(fromPosition: Int, toPosition: Int) {
|
override fun rowMoved(fromPosition: Int, toPosition: Int) {
|
||||||
vaultListFragment().rowMoved(fromPosition, toPosition)
|
vaultListFragment().rowMoved(fromPosition, toPosition)
|
||||||
}
|
}
|
||||||
@ -209,14 +162,6 @@ class VaultListActivity : BaseActivity(), //
|
|||||||
vaultListPresenter.onCreateVault()
|
vaultListPresenter.onCreateVault()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnlockClick(vaultModel: VaultModel, password: String) {
|
|
||||||
vaultListPresenter.onUnlockClick(vaultModel, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnlockCanceled() {
|
|
||||||
vaultListPresenter.onUnlockCanceled()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeleteVaultClick(vaultModel: VaultModel) {
|
override fun onDeleteVaultClick(vaultModel: VaultModel) {
|
||||||
VaultDeleteConfirmationDialog.newInstance(vaultModel) //
|
VaultDeleteConfirmationDialog.newInstance(vaultModel) //
|
||||||
.show(supportFragmentManager, "VaultDeleteConfirmationDialog")
|
.show(supportFragmentManager, "VaultDeleteConfirmationDialog")
|
||||||
@ -249,14 +194,6 @@ class VaultListActivity : BaseActivity(), //
|
|||||||
private fun vaultListFragment(): VaultListFragment = //
|
private fun vaultListFragment(): VaultListFragment = //
|
||||||
getCurrentFragment(R.id.fragmentContainer) as VaultListFragment
|
getCurrentFragment(R.id.fragmentContainer) as VaultListFragment
|
||||||
|
|
||||||
override fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String) {
|
|
||||||
vaultListPresenter.onChangePasswordClicked(vaultModel, oldPassword, newPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeleteMissingVaultClicked(vault: Vault) {
|
|
||||||
vaultListPresenter.onDeleteMissingVaultClicked(vault)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpdateAppDialogLoaded() {
|
override fun onUpdateAppDialogLoaded() {
|
||||||
showProgress(ProgressModel.GENERIC)
|
showProgress(ProgressModel.GENERIC)
|
||||||
}
|
}
|
||||||
@ -279,21 +216,4 @@ class VaultListActivity : BaseActivity(), //
|
|||||||
override fun onAskForBetaConfirmationFinished() {
|
override fun onAskForBetaConfirmationFinished() {
|
||||||
sharedPreferencesHandler.setBetaScreenDialogAlreadyShown()
|
sharedPreferencesHandler.setBetaScreenDialogAlreadyShown()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBiometricAuthenticated(vault: VaultModel) {
|
|
||||||
vaultListPresenter.onBiometricAuthenticationSucceeded(vault)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricAuthenticationFailed(vault: VaultModel) {
|
|
||||||
val vaultWithoutPassword = Vault.aCopyOf(vault.toVault()).withSavedPassword(null).build()
|
|
||||||
if (!vaultListPresenter.startedUsingPrepareUnlock()) {
|
|
||||||
vaultListPresenter.startPrepareUnlockUseCase(vaultWithoutPassword)
|
|
||||||
}
|
|
||||||
showEnterPasswordDialog(VaultModel(vaultWithoutPassword))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBiometricKeyInvalidated(vault: VaultModel) {
|
|
||||||
vaultListPresenter.onBiometricKeyInvalidated()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,5 @@ interface AutoUploadChooseVaultView : View {
|
|||||||
fun displayDialogUnableToUploadFiles()
|
fun displayDialogUnableToUploadFiles()
|
||||||
fun displayVaults(vaults: List<VaultModel>)
|
fun displayVaults(vaults: List<VaultModel>)
|
||||||
fun showChosenLocation(location: CloudFolderModel)
|
fun showChosenLocation(location: CloudFolderModel)
|
||||||
fun showEnterPasswordDialog(vaultModel: VaultModel)
|
|
||||||
fun showBiometricAuthKeyInvalidatedDialog()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,6 @@ interface BiometricAuthSettingsView : View {
|
|||||||
|
|
||||||
fun renderVaultList(vaultModelCollection: List<VaultModel>)
|
fun renderVaultList(vaultModelCollection: List<VaultModel>)
|
||||||
fun clearVaultList()
|
fun clearVaultList()
|
||||||
fun showBiometricAuthenticationDialog(vaultModel: VaultModel)
|
|
||||||
fun showEnterPasswordDialog(vaultModel: VaultModel)
|
|
||||||
fun showSetupBiometricAuthDialog()
|
fun showSetupBiometricAuthDialog()
|
||||||
fun showBiometricAuthKeyInvalidatedDialog()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,8 @@ interface SharedFilesView : View {
|
|||||||
fun displayVaults(vaults: List<VaultModel>)
|
fun displayVaults(vaults: List<VaultModel>)
|
||||||
fun displayFilesToUpload(sharedFiles: List<SharedFileModel>)
|
fun displayFilesToUpload(sharedFiles: List<SharedFileModel>)
|
||||||
fun displayDialogUnableToUploadFiles()
|
fun displayDialogUnableToUploadFiles()
|
||||||
fun showEnterPasswordDialog(vault: VaultModel)
|
|
||||||
fun showReplaceDialog(existingFiles: List<String>, size: Int)
|
fun showReplaceDialog(existingFiles: List<String>, size: Int)
|
||||||
fun showChosenLocation(folder: CloudFolderModel)
|
fun showChosenLocation(folder: CloudFolderModel)
|
||||||
fun showBiometricAuthKeyInvalidatedDialog()
|
|
||||||
fun showUploadDialog(uploadingFiles: Int)
|
fun showUploadDialog(uploadingFiles: Int)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
package org.cryptomator.presentation.ui.activity.view
|
||||||
|
|
||||||
|
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||||
|
import org.cryptomator.presentation.model.VaultModel
|
||||||
|
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
||||||
|
|
||||||
|
interface UnlockVaultView : View, EnterPasswordDialog.Callback {
|
||||||
|
|
||||||
|
fun showEnterPasswordDialog(vault: VaultModel)
|
||||||
|
fun showBiometricDialog(vault: VaultModel)
|
||||||
|
fun getEncryptedPasswordWithBiometricAuthentication(vaultModel: VaultModel)
|
||||||
|
fun showBiometricAuthKeyInvalidatedDialog()
|
||||||
|
fun cancelBasicAuthIfRunning()
|
||||||
|
fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean
|
||||||
|
fun showChangePasswordDialog(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?)
|
||||||
|
|
||||||
|
}
|
@ -12,17 +12,10 @@ interface VaultListView : View {
|
|||||||
fun addOrUpdateVault(vault: VaultModel)
|
fun addOrUpdateVault(vault: VaultModel)
|
||||||
fun renameVault(vaultModel: VaultModel)
|
fun renameVault(vaultModel: VaultModel)
|
||||||
fun navigateToVaultContent(vault: VaultModel, decryptedRoot: CloudFolderModel)
|
fun navigateToVaultContent(vault: VaultModel, decryptedRoot: CloudFolderModel)
|
||||||
fun showEnterPasswordDialog(vault: VaultModel)
|
|
||||||
fun showBiometricDialog(vault: VaultModel)
|
|
||||||
fun showChangePasswordDialog(vaultModel: VaultModel)
|
|
||||||
fun getEncryptedPasswordWithBiometricAuthentication(vaultModel: VaultModel)
|
|
||||||
fun showVaultSettingsDialog(vaultModel: VaultModel)
|
fun showVaultSettingsDialog(vaultModel: VaultModel)
|
||||||
fun showAddVaultBottomSheet()
|
fun showAddVaultBottomSheet()
|
||||||
fun showRenameDialog(vaultModel: VaultModel)
|
fun showRenameDialog(vaultModel: VaultModel)
|
||||||
fun showBiometricAuthKeyInvalidatedDialog()
|
|
||||||
fun isVaultLocked(vaultModel: VaultModel): Boolean
|
fun isVaultLocked(vaultModel: VaultModel): Boolean
|
||||||
fun cancelBasicAuthIfRunning()
|
|
||||||
fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean
|
|
||||||
fun rowMoved(fromPosition: Int, toPosition: Int)
|
fun rowMoved(fromPosition: Int, toPosition: Int)
|
||||||
fun vaultMoved(vaults: List<VaultModel>)
|
fun vaultMoved(vaults: List<VaultModel>)
|
||||||
|
|
||||||
|
@ -2,8 +2,11 @@ package org.cryptomator.presentation.ui.callback
|
|||||||
|
|
||||||
import org.cryptomator.presentation.ui.bottomsheet.AddVaultBottomSheet
|
import org.cryptomator.presentation.ui.bottomsheet.AddVaultBottomSheet
|
||||||
import org.cryptomator.presentation.ui.bottomsheet.SettingsVaultBottomSheet
|
import org.cryptomator.presentation.ui.bottomsheet.SettingsVaultBottomSheet
|
||||||
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
|
||||||
import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog
|
import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog
|
||||||
import org.cryptomator.presentation.ui.dialog.VaultRenameDialog
|
import org.cryptomator.presentation.ui.dialog.VaultRenameDialog
|
||||||
|
|
||||||
interface VaultListCallback : AddVaultBottomSheet.Callback, EnterPasswordDialog.Callback, SettingsVaultBottomSheet.Callback, VaultDeleteConfirmationDialog.Callback, VaultRenameDialog.Callback
|
// FIXME delete this file and add this interfaces to VaultListView.kt
|
||||||
|
interface VaultListCallback : AddVaultBottomSheet.Callback, //
|
||||||
|
SettingsVaultBottomSheet.Callback, //
|
||||||
|
VaultDeleteConfirmationDialog.Callback, //
|
||||||
|
VaultRenameDialog.Callback
|
||||||
|
@ -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)
|
||||||
@ -101,9 +104,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
|
||||||
|
@ -14,6 +14,8 @@ class VaultNotFoundDialog private constructor(private val context: Context) {
|
|||||||
interface Callback {
|
interface Callback {
|
||||||
|
|
||||||
fun onDeleteMissingVaultClicked(vault: Vault)
|
fun onDeleteMissingVaultClicked(vault: Vault)
|
||||||
|
fun onCancelMissingVaultClicked(vault: Vault)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show(vault: Vault) {
|
fun show(vault: Vault) {
|
||||||
@ -21,7 +23,8 @@ class VaultNotFoundDialog private constructor(private val context: Context) {
|
|||||||
.setTitle(String.format(ResourceHelper.getString(R.string.dialog_vault_not_found_title), vault.name)) //
|
.setTitle(String.format(ResourceHelper.getString(R.string.dialog_vault_not_found_title), vault.name)) //
|
||||||
.setMessage(ResourceHelper.getString(R.string.dialog_vault_not_found_message)) //
|
.setMessage(ResourceHelper.getString(R.string.dialog_vault_not_found_message)) //
|
||||||
.setPositiveButton(ResourceHelper.getString(R.string.dialog_vault_not_found_positive_button_text)) { _: DialogInterface, _: Int -> callback.onDeleteMissingVaultClicked(vault) } //
|
.setPositiveButton(ResourceHelper.getString(R.string.dialog_vault_not_found_positive_button_text)) { _: DialogInterface, _: Int -> callback.onDeleteMissingVaultClicked(vault) } //
|
||||||
.setNegativeButton(ResourceHelper.getString(R.string.dialog_button_cancel)) { dialog: DialogInterface, _: Int -> dialog.dismiss() } //
|
.setNegativeButton(ResourceHelper.getString(R.string.dialog_button_cancel)) { _: DialogInterface, _: Int -> callback.onCancelMissingVaultClicked(vault) } //
|
||||||
|
.setOnCancelListener { callback.onCancelMissingVaultClicked(vault) }
|
||||||
.create().show()
|
.create().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
package org.cryptomator.presentation.ui.fragment
|
||||||
|
|
||||||
|
import org.cryptomator.generator.Fragment
|
||||||
|
import org.cryptomator.presentation.R
|
||||||
|
import org.cryptomator.presentation.presenter.UnlockVaultPresenter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Fragment(R.layout.fragment_unlock_vault)
|
||||||
|
class UnlockVaultFragment : BaseFragment() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var presenter: UnlockVaultPresenter
|
||||||
|
|
||||||
|
override fun setupView() {
|
||||||
|
presenter.setup()
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import org.cryptomator.presentation.model.mappers.CloudModelMapper;
|
|||||||
import org.cryptomator.presentation.presenter.VaultListPresenter;
|
import org.cryptomator.presentation.presenter.VaultListPresenter;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@ -89,9 +90,9 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
|
|||||||
.withExtraText(presenter() //
|
.withExtraText(presenter() //
|
||||||
.context() //
|
.context() //
|
||||||
.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(Arrays.asList("masterkey.cryptomator", "vault.cryptomator")) //
|
||||||
.build()), //
|
.build()), //
|
||||||
SerializableResultCallbacks.masterkeyFileChosen());
|
SerializableResultCallbacks.cryptomatorFileChosen());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -112,7 +113,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);
|
||||||
|
15
presentation/src/main/res/layout/activity_unlock_vault.xml
Normal file
15
presentation/src/main/res/layout/activity_unlock_vault.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/activityRootView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/fragmentContainer"
|
||||||
|
android:name="org.cryptomator.presentation.ui.fragment.UnlockVaultFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:tag="UnlockVaultFragment" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -33,8 +33,12 @@
|
|||||||
<string name="error_failed_to_decrypt_webdav_password">Failed to decrypt WebDAV password, please re add in settings</string>
|
<string name="error_failed_to_decrypt_webdav_password">Failed to decrypt WebDAV password, please re add in settings</string>
|
||||||
<string name="error_play_services_not_available">Play Services not installed</string>
|
<string name="error_play_services_not_available">Play Services not installed</string>
|
||||||
<string name="error_biometric_auth_aborted">Biometric authentication aborted</string>
|
<string name="error_biometric_auth_aborted">Biometric authentication aborted</string>
|
||||||
|
<string name="error_vault_version_mismatch">Version specified in vault.cryptomator is different to masterkey.cryptomator</string>
|
||||||
|
<string name="error_vault_key_invalid">vault.cryptomator does not match with this masterkey.cryptomator</string>
|
||||||
|
<string name="error_vault_config_loading">General error while loading the vault config</string>
|
||||||
<string name="error_file_not_found_after_opening_using_3party">Local file isn\'t present anymore after switching back to Cryptomator. Possible changes cannot be propagated back to the cloud.</string>
|
<string name="error_file_not_found_after_opening_using_3party">Local file isn\'t present anymore after switching back to Cryptomator. Possible changes cannot be propagated back to the cloud.</string>
|
||||||
<string name="error_no_such_bucket">No such bucket</string>
|
<string name="error_no_such_bucket">No such bucket</string>
|
||||||
|
<string name="error_masterkey_location_not_supported">Custom Masterkey location not supported yet</string>
|
||||||
|
|
||||||
<!-- # clouds -->
|
<!-- # clouds -->
|
||||||
|
|
||||||
|
@ -29,6 +29,10 @@
|
|||||||
<item name="colorAccent">@color/colorPrimary</item>
|
<item name="colorAccent">@color/colorPrimary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TransparentAlertDialogCustom" parent="AlertDialogCustom">
|
||||||
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Toolbar.Theme" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
|
<style name="Toolbar.Theme" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||||
<item name="colorAccent">@color/textColorWhite</item>
|
<item name="colorAccent">@color/textColorWhite</item>
|
||||||
</style>
|
</style>
|
||||||
|
@ -13,17 +13,13 @@ import org.cryptomator.domain.usecases.DoUpdateUseCase;
|
|||||||
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase;
|
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase;
|
||||||
import org.cryptomator.domain.usecases.ResultHandler;
|
import org.cryptomator.domain.usecases.ResultHandler;
|
||||||
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase;
|
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase;
|
||||||
import org.cryptomator.domain.usecases.vault.ChangePasswordUseCase;
|
|
||||||
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase;
|
import org.cryptomator.domain.usecases.vault.DeleteVaultUseCase;
|
||||||
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase;
|
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase;
|
||||||
import org.cryptomator.domain.usecases.vault.LockVaultUseCase;
|
import org.cryptomator.domain.usecases.vault.LockVaultUseCase;
|
||||||
import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase;
|
import org.cryptomator.domain.usecases.vault.MoveVaultPositionUseCase;
|
||||||
import org.cryptomator.domain.usecases.vault.PrepareUnlockUseCase;
|
|
||||||
import org.cryptomator.domain.usecases.vault.RemoveStoredVaultPasswordsUseCase;
|
|
||||||
import org.cryptomator.domain.usecases.vault.RenameVaultUseCase;
|
import org.cryptomator.domain.usecases.vault.RenameVaultUseCase;
|
||||||
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.presentation.exception.ExceptionHandlers;
|
import org.cryptomator.presentation.exception.ExceptionHandlers;
|
||||||
import org.cryptomator.presentation.model.VaultModel;
|
import org.cryptomator.presentation.model.VaultModel;
|
||||||
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper;
|
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper;
|
||||||
@ -98,17 +94,12 @@ public class VaultListPresenterTest {
|
|||||||
private LockVaultUseCase lockVaultUseCase = Mockito.mock(LockVaultUseCase.class);
|
private LockVaultUseCase lockVaultUseCase = Mockito.mock(LockVaultUseCase.class);
|
||||||
private LockVaultUseCase.Launcher lockVaultUseCaseLauncher = Mockito.mock(LockVaultUseCase.Launcher.class);
|
private LockVaultUseCase.Launcher lockVaultUseCaseLauncher = Mockito.mock(LockVaultUseCase.Launcher.class);
|
||||||
private GetDecryptedCloudForVaultUseCase getDecryptedCloudForVaultUseCase = Mockito.mock(GetDecryptedCloudForVaultUseCase.class);
|
private GetDecryptedCloudForVaultUseCase getDecryptedCloudForVaultUseCase = Mockito.mock(GetDecryptedCloudForVaultUseCase.class);
|
||||||
private PrepareUnlockUseCase prepareUnlockUseCase = Mockito.mock(PrepareUnlockUseCase.class);
|
|
||||||
private PrepareUnlockUseCase.Launcher prepareUnlockUseCaseLauncher = Mockito.mock(PrepareUnlockUseCase.Launcher.class);
|
|
||||||
private UnlockToken unlockToken = Mockito.mock(UnlockToken.class);
|
private UnlockToken unlockToken = Mockito.mock(UnlockToken.class);
|
||||||
private UnlockVaultUseCase unlockVaultUseCase = Mockito.mock(UnlockVaultUseCase.class);
|
|
||||||
private GetRootFolderUseCase getRootFolderUseCase = Mockito.mock(GetRootFolderUseCase.class);
|
private GetRootFolderUseCase getRootFolderUseCase = Mockito.mock(GetRootFolderUseCase.class);
|
||||||
private AddExistingVaultWorkflow addExistingVaultWorkflow = Mockito.mock(AddExistingVaultWorkflow.class);
|
private AddExistingVaultWorkflow addExistingVaultWorkflow = Mockito.mock(AddExistingVaultWorkflow.class);
|
||||||
private CreateNewVaultWorkflow createNewVaultWorkflow = Mockito.mock(CreateNewVaultWorkflow.class);
|
private CreateNewVaultWorkflow createNewVaultWorkflow = Mockito.mock(CreateNewVaultWorkflow.class);
|
||||||
private SaveVaultUseCase saveVaultUseCase = Mockito.mock(SaveVaultUseCase.class);
|
private SaveVaultUseCase saveVaultUseCase = Mockito.mock(SaveVaultUseCase.class);
|
||||||
private MoveVaultPositionUseCase moveVaultPositionUseCase = Mockito.mock(MoveVaultPositionUseCase.class);
|
private MoveVaultPositionUseCase moveVaultPositionUseCase = Mockito.mock(MoveVaultPositionUseCase.class);
|
||||||
private ChangePasswordUseCase changePasswordUseCase = Mockito.mock(ChangePasswordUseCase.class);
|
|
||||||
private RemoveStoredVaultPasswordsUseCase removeStoredVaultPasswordsUseCase = Mockito.mock(RemoveStoredVaultPasswordsUseCase.class);
|
|
||||||
private DoLicenseCheckUseCase doLicenceCheckUsecase = Mockito.mock(DoLicenseCheckUseCase.class);
|
private DoLicenseCheckUseCase doLicenceCheckUsecase = Mockito.mock(DoLicenseCheckUseCase.class);
|
||||||
private DoUpdateCheckUseCase updateCheckUseCase = Mockito.mock(DoUpdateCheckUseCase.class);
|
private DoUpdateCheckUseCase updateCheckUseCase = Mockito.mock(DoUpdateCheckUseCase.class);
|
||||||
private DoUpdateUseCase updateUseCase = Mockito.mock(DoUpdateUseCase.class);
|
private DoUpdateUseCase updateUseCase = Mockito.mock(DoUpdateUseCase.class);
|
||||||
@ -126,15 +117,11 @@ public class VaultListPresenterTest {
|
|||||||
renameVaultUseCase, //
|
renameVaultUseCase, //
|
||||||
lockVaultUseCase, //
|
lockVaultUseCase, //
|
||||||
getDecryptedCloudForVaultUseCase, //
|
getDecryptedCloudForVaultUseCase, //
|
||||||
prepareUnlockUseCase, //
|
|
||||||
unlockVaultUseCase, //
|
|
||||||
getRootFolderUseCase, //
|
getRootFolderUseCase, //
|
||||||
addExistingVaultWorkflow, //
|
addExistingVaultWorkflow, //
|
||||||
createNewVaultWorkflow, //
|
createNewVaultWorkflow, //
|
||||||
saveVaultUseCase, //
|
saveVaultUseCase, //
|
||||||
moveVaultPositionUseCase, //
|
moveVaultPositionUseCase, //
|
||||||
changePasswordUseCase, //
|
|
||||||
removeStoredVaultPasswordsUseCase, //
|
|
||||||
doLicenceCheckUsecase, //
|
doLicenceCheckUsecase, //
|
||||||
updateCheckUseCase, //
|
updateCheckUseCase, //
|
||||||
updateUseCase, //
|
updateUseCase, //
|
||||||
@ -241,43 +228,4 @@ public class VaultListPresenterTest {
|
|||||||
Mockito.any());
|
Mockito.any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testOnUnlockCanceled() {
|
|
||||||
inTest.onUnlockCanceled();
|
|
||||||
|
|
||||||
verify(prepareUnlockUseCase).unsubscribe();
|
|
||||||
verify(unlockVaultUseCase).cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testOnVaultLockedClicked() {
|
|
||||||
ArgumentCaptor<ResultHandler<Vault>> captor = ArgumentCaptor.forClass(ResultHandler.class);
|
|
||||||
|
|
||||||
when(lockVaultUseCase.withVault(AN_UNLOCKED_VAULT_MODEL.toVault())).thenReturn(lockVaultUseCaseLauncher);
|
|
||||||
|
|
||||||
inTest.onVaultLockClicked(AN_UNLOCKED_VAULT_MODEL);
|
|
||||||
|
|
||||||
verify(lockVaultUseCaseLauncher).run(captor.capture());
|
|
||||||
captor.getValue().onSuccess(AN_UNLOCKED_VAULT_MODEL.toVault());
|
|
||||||
verify(vaultListView).addOrUpdateVault(AN_UNLOCKED_VAULT_MODEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void onVaultClickedWithCloudAndLocked() {
|
|
||||||
ArgumentCaptor<ResultHandler<UnlockToken>> captor = ArgumentCaptor.forClass(ResultHandler.class);
|
|
||||||
|
|
||||||
when(prepareUnlockUseCase.withVault(ANOTHER_VAULT_MODEL_WITH_CLOUD.toVault())) //
|
|
||||||
.thenReturn(prepareUnlockUseCaseLauncher);
|
|
||||||
when(unlockToken.getVault()) //
|
|
||||||
.thenReturn(ANOTHER_VAULT_MODEL_WITH_CLOUD.toVault());
|
|
||||||
|
|
||||||
inTest.onVaultClicked(ANOTHER_VAULT_MODEL_WITH_CLOUD);
|
|
||||||
|
|
||||||
verify(prepareUnlockUseCaseLauncher).run(captor.capture());
|
|
||||||
captor.getValue().onSuccess(unlockToken);
|
|
||||||
|
|
||||||
verify(vaultListView).addOrUpdateVault(ANOTHER_VAULT_MODEL_WITH_CLOUD);
|
|
||||||
verify(vaultListView).showEnterPasswordDialog(ANOTHER_VAULT_MODEL_WITH_CLOUD);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -73,3 +73,16 @@ dependencies {
|
|||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(Test) {
|
||||||
|
testLogging {
|
||||||
|
events "failed"
|
||||||
|
|
||||||
|
showExceptions true
|
||||||
|
exceptionFormat "full"
|
||||||
|
showCauses true
|
||||||
|
showStackTraces true
|
||||||
|
|
||||||
|
showStandardStreams = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -52,7 +52,7 @@ public class BiometricAuthCryptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String decrypt(javax.crypto.Cipher cipher, String password) throws IllegalBlockSizeException, BadPaddingException {
|
public String decrypt(javax.crypto.Cipher cipher, String password) throws IllegalBlockSizeException, BadPaddingException {
|
||||||
byte[] ciphered = cipher.doFinal(CipherFromApi23.getBytes(password.getBytes(ISO_8859_1)));
|
byte[] ciphered = cipher.doFinal(CipherImpl.getBytes(password.getBytes(ISO_8859_1)));
|
||||||
return new String(ciphered, UTF_8);
|
return new String(ciphered, UTF_8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,14 @@ import javax.crypto.spec.IvParameterSpec;
|
|||||||
|
|
||||||
import static java.lang.System.arraycopy;
|
import static java.lang.System.arraycopy;
|
||||||
|
|
||||||
class CipherFromApi23 implements Cipher {
|
class CipherImpl implements Cipher {
|
||||||
|
|
||||||
private static final int IV_LENGTH = 16;
|
private static final int IV_LENGTH = 16;
|
||||||
|
|
||||||
private final javax.crypto.Cipher cipher;
|
private final javax.crypto.Cipher cipher;
|
||||||
private final SecretKey key;
|
private final SecretKey key;
|
||||||
|
|
||||||
CipherFromApi23(javax.crypto.Cipher cipher, SecretKey key) {
|
CipherImpl(javax.crypto.Cipher cipher, SecretKey key) {
|
||||||
this.cipher = cipher;
|
this.cipher = cipher;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
}
|
}
|
@ -16,7 +16,7 @@ class CryptoOperationsFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static CryptoOperations createCryptoOperations() {
|
private static CryptoOperations createCryptoOperations() {
|
||||||
return new CryptoOperationsFromApi23();
|
return new CryptoOperationsImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import java.security.UnrecoverableKeyException;
|
|||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
class CryptoOperationsFromApi23 implements CryptoOperations {
|
class CryptoOperationsImpl implements CryptoOperations {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cipher cryptor(KeyStore keyStore, String alias) throws UnrecoverableStorageKeyException {
|
public Cipher cryptor(KeyStore keyStore, String alias) throws UnrecoverableStorageKeyException {
|
||||||
@ -19,7 +19,7 @@ class CryptoOperationsFromApi23 implements CryptoOperations {
|
|||||||
final javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" //
|
final javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" //
|
||||||
+ KeyProperties.BLOCK_MODE_CBC + "/" //
|
+ KeyProperties.BLOCK_MODE_CBC + "/" //
|
||||||
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
|
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
|
||||||
return new CipherFromApi23(cipher, key);
|
return new CipherImpl(cipher, key);
|
||||||
} catch (UnrecoverableKeyException e) {
|
} catch (UnrecoverableKeyException e) {
|
||||||
throw new UnrecoverableStorageKeyException(e);
|
throw new UnrecoverableStorageKeyException(e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
Loading…
x
Reference in New Issue
Block a user