Merge branch 'feature/vault-format-8' into develop
This commit is contained in:
commit
cd986981c2
@ -6,7 +6,7 @@ allprojects {
|
||||
|
||||
ext {
|
||||
androidBuildToolsVersion = "29.0.3"
|
||||
androidMinSdkVersion = 23
|
||||
androidMinSdkVersion = 24
|
||||
androidTargetSdkVersion = 29
|
||||
androidCompileSdkVersion = 29
|
||||
|
||||
@ -49,7 +49,7 @@ ext {
|
||||
// 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
|
||||
cryptolibVersion = '1.3.0'
|
||||
cryptolibVersion = '2.0.0-rc1'
|
||||
|
||||
awsAndroidSdkS3 = '2.23.0'
|
||||
|
||||
|
@ -17,6 +17,8 @@ android {
|
||||
|
||||
buildConfigField 'int', 'VERSION_CODE', "${globalConfiguration["androidVersionCode"]}"
|
||||
buildConfigField "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
||||
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@ -98,6 +100,11 @@ dependencies {
|
||||
// dagger
|
||||
annotationProcessor dependencies.daggerCompiler
|
||||
implementation dependencies.dagger
|
||||
|
||||
api dependencies.jsonWebTokenApi
|
||||
implementation dependencies.jsonWebTokenImpl
|
||||
implementation dependencies.jsonWebTokenJson
|
||||
|
||||
// cloud
|
||||
implementation dependencies.awsAndroidS3
|
||||
implementation dependencies.dropbox
|
||||
@ -151,6 +158,7 @@ dependencies {
|
||||
testRuntimeOnly dependencies.junit4Engine
|
||||
|
||||
testImplementation dependencies.mockito
|
||||
testImplementation dependencies.mockitoInline
|
||||
testImplementation dependencies.hamcrest
|
||||
}
|
||||
|
||||
@ -161,3 +169,16 @@ configurations {
|
||||
static def getApiKey(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 {
|
||||
try {
|
||||
return delegate.root(cloud);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -43,10 +40,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public DirType resolve(CloudType cloud, String path) throws BackendException {
|
||||
try {
|
||||
return delegate.resolve(cloud, path);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -56,10 +50,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public FileType file(DirType parent, String name) throws BackendException {
|
||||
try {
|
||||
return delegate.file(parent, name);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(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 {
|
||||
try {
|
||||
return delegate.file(parent, name, size);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -82,10 +70,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public DirType folder(DirType parent, String name) throws BackendException {
|
||||
try {
|
||||
return delegate.folder(parent, name);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -95,10 +80,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public boolean exists(NodeType node) throws BackendException {
|
||||
try {
|
||||
return delegate.exists(node);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -108,10 +90,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public List<? extends CloudNode> list(DirType folder) throws BackendException {
|
||||
try {
|
||||
return delegate.list(folder);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -121,10 +100,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public DirType create(DirType folder) throws BackendException {
|
||||
try {
|
||||
return delegate.create(folder);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -134,10 +110,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public DirType move(DirType source, DirType target) throws BackendException {
|
||||
try {
|
||||
return delegate.move(source, target);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -147,10 +120,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public FileType move(FileType source, FileType target) throws BackendException {
|
||||
try {
|
||||
return delegate.move(source, target);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(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 {
|
||||
try {
|
||||
return delegate.write(file, data, progressAware, replace, size);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(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 {
|
||||
try {
|
||||
delegate.read(file, encryptedTmpFile, data, progressAware);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -186,10 +150,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public void delete(NodeType node) throws BackendException {
|
||||
try {
|
||||
delegate.delete(node);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -199,10 +160,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException {
|
||||
try {
|
||||
return delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
@ -212,10 +170,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
|
||||
public void logout(CloudType cloud) throws BackendException {
|
||||
try {
|
||||
delegate.logout(cloud);
|
||||
} catch (BackendException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
} catch (BackendException | RuntimeException e) {
|
||||
throwWrappedIfRequired(e);
|
||||
throw e;
|
||||
}
|
||||
|
@ -33,16 +33,19 @@ class CryptoCloudContentRepository implements CloudContentRepository<CryptoCloud
|
||||
throw new FatalBackendException(e);
|
||||
}
|
||||
|
||||
switch (cloud.getVault().getVersion()) {
|
||||
switch (cloud.getVault().getFormat()) {
|
||||
case 7:
|
||||
this.cryptoImpl = new CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7());
|
||||
break;
|
||||
case 8:
|
||||
this.cryptoImpl = new CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7(), cloud.getVault().getShorteningThreshold());
|
||||
break;
|
||||
case 6:
|
||||
case 5:
|
||||
this.cryptoImpl = new CryptoImplVaultFormatPre7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormatPre7());
|
||||
break;
|
||||
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;
|
||||
|
||||
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.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.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.ByteArrayOutputStream;
|
||||
import java.text.Normalizer;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import static android.R.attr.version;
|
||||
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.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.data.cloud.crypto.CryptoConstants.MASTERKEY_SCHEME;
|
||||
import static org.cryptomator.data.cloud.crypto.CryptoConstants.VAULT_FILE_NAME;
|
||||
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;
|
||||
|
||||
@Singleton
|
||||
public class CryptoCloudFactory {
|
||||
|
||||
private final CryptorProvider cryptorProvider;
|
||||
private final CloudContentRepository cloudContentRepository;
|
||||
private final CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
@Inject
|
||||
public CryptoCloudFactory( //
|
||||
CloudContentRepository cloudContentRepository, //
|
||||
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory, //
|
||||
CryptorProvider cryptorProvider) {
|
||||
this.cryptorProvider = cryptorProvider;
|
||||
public CryptoCloudFactory(CloudContentRepository cloudContentRepository, //
|
||||
CryptoCloudContentRepositoryFactory cryptoCloudContentRepositoryFactory) {
|
||||
this.cloudContentRepository = cloudContentRepository;
|
||||
this.cryptoCloudContentRepositoryFactory = cryptoCloudContentRepositoryFactory;
|
||||
}
|
||||
|
||||
public void create(CloudFolder location, CharSequence password) throws BackendException {
|
||||
Cryptor cryptor = cryptorProvider.createNew();
|
||||
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);
|
||||
cryptoCloudProvider(Optional.empty()).create(location, password);
|
||||
}
|
||||
|
||||
public Cloud decryptedViewOf(Vault vault) throws BackendException {
|
||||
return new CryptoCloud(aCopyOf(vault).build());
|
||||
}
|
||||
|
||||
public Vault unlock(Vault vault, CharSequence password, Flag cancelledFlag) 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 {
|
||||
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
||||
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 {
|
||||
byte[] keyFileData = readKeyFileData(location);
|
||||
UnlockTokenImpl unlockToken = new UnlockTokenImpl(vault, keyFileData);
|
||||
assertVaultVersionIsSupported(unlockToken.getKeyFile().getVersion());
|
||||
return unlockToken;
|
||||
private byte[] readConfigFileData(CloudFolder location) throws BackendException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
CloudFile vaultFile = cloudContentRepository.file(location, VAULT_FILE_NAME);
|
||||
cloudContentRepository.read(vaultFile, Optional.empty(), data, NO_OP_PROGRESS_AWARE);
|
||||
return data.toByteArray();
|
||||
}
|
||||
|
||||
private Cryptor cryptorFor(KeyFile keyFile, CharSequence password) {
|
||||
return cryptorProvider.createFromKeyFile(keyFile, normalizePassword(password, keyFile.getVersion()), keyFile.getVersion());
|
||||
public Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
return cryptoCloudProvider(unverifiedVaultConfig).unlock(createUnlockToken(vault, unverifiedVaultConfig), unverifiedVaultConfig, password, cancelledFlag);
|
||||
}
|
||||
|
||||
private CloudFolder vaultLocation(Vault vault) throws BackendException {
|
||||
return cloudContentRepository.resolve(vault.getCloud(), vault.getPath());
|
||||
public Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
return cryptoCloudProvider(unverifiedVaultConfig).unlock(token, unverifiedVaultConfig, password, cancelledFlag);
|
||||
}
|
||||
|
||||
public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException {
|
||||
try {
|
||||
// create a cryptor, which checks the password, then destroy it immediately
|
||||
cryptorFor(createUnlockToken(vault).getKeyFile(), password).destroy();
|
||||
return true;
|
||||
} catch (InvalidPassphraseException e) {
|
||||
return false;
|
||||
}
|
||||
public UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||
return cryptoCloudProvider(unverifiedVaultConfig).createUnlockToken(vault, unverifiedVaultConfig);
|
||||
}
|
||||
|
||||
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||
return cryptoCloudProvider(unverifiedVaultConfig).isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
|
||||
cryptoCloudProvider(unverifiedVaultConfig).changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
|
||||
}
|
||||
|
||||
private void writeKeyFile(CloudFolder location, KeyFile keyFile) throws BackendException {
|
||||
byte[] data = keyFile.serialize();
|
||||
cloudContentRepository.write(masterkeyFile(location), ByteArrayDataSource.from(data), NO_OP_PROGRESS_AWARE, false, data.length);
|
||||
}
|
||||
|
||||
private byte[] readKeyFileData(CloudFolder location) throws BackendException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
cloudContentRepository.read(masterkeyFile(location), Optional.empty(), data, NO_OP_PROGRESS_AWARE);
|
||||
return data.toByteArray();
|
||||
}
|
||||
|
||||
private CloudFile masterkeyFile(CloudFolder location) throws BackendException {
|
||||
return cloudContentRepository.file(location, MASTERKEY_FILE_NAME);
|
||||
}
|
||||
|
||||
private CloudFile masterkeyBackupFile(CloudFolder location, byte[] data) throws BackendException {
|
||||
String fileName = MASTERKEY_FILE_NAME + BackupFileIdSuffixGenerator.generate(data) + MASTERKEY_BACKUP_FILE_EXT;
|
||||
return cloudContentRepository.file(location, fileName);
|
||||
}
|
||||
|
||||
public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException {
|
||||
CloudFolder vaultLocation = vaultLocation(vault);
|
||||
ByteArrayOutputStream dataOutputStream = new ByteArrayOutputStream();
|
||||
cloudContentRepository.read(masterkeyFile(vaultLocation), Optional.empty(), dataOutputStream, NO_OP_PROGRESS_AWARE);
|
||||
|
||||
byte[] data = dataOutputStream.toByteArray();
|
||||
int vaultVersion = KeyFile.parse(data).getVersion();
|
||||
|
||||
createBackupMasterKeyFile(data, vaultLocation);
|
||||
createNewMasterKeyFile(data, vaultVersion, oldPassword, newPassword, vaultLocation);
|
||||
}
|
||||
|
||||
private void createBackupMasterKeyFile(byte[] data, CloudFolder vaultLocation) throws BackendException {
|
||||
cloudContentRepository.write( //
|
||||
masterkeyBackupFile(vaultLocation, data), //
|
||||
ByteArrayDataSource.from(data), //
|
||||
NO_OP_PROGRESS_AWARE, //
|
||||
true, //
|
||||
data.length);
|
||||
}
|
||||
|
||||
private void createNewMasterKeyFile(byte[] data, int vaultVersion, String oldPassword, String newPassword, CloudFolder vaultLocation) throws BackendException {
|
||||
byte[] newMasterKeyFile = Cryptors.changePassphrase(cryptorProvider, //
|
||||
data, //
|
||||
normalizePassword(oldPassword, vaultVersion), //
|
||||
normalizePassword(newPassword, vaultVersion));
|
||||
cloudContentRepository.write(masterkeyFile(vaultLocation), //
|
||||
ByteArrayDataSource.from(newMasterKeyFile), //
|
||||
NO_OP_PROGRESS_AWARE, //
|
||||
true, //
|
||||
newMasterKeyFile.length);
|
||||
}
|
||||
|
||||
private CharSequence normalizePassword(CharSequence password, int vaultVersion) {
|
||||
if (vaultVersion >= VERSION_WITH_NORMALIZED_PASSWORDS) {
|
||||
return normalize(password, Normalizer.Form.NFC);
|
||||
private CryptoCloudProvider cryptoCloudProvider(Optional<UnverifiedVaultConfig> unverifiedVaultConfigOptional) {
|
||||
if (unverifiedVaultConfigOptional.isPresent()) {
|
||||
switch (unverifiedVaultConfigOptional.get().getKeyId().getScheme()) {
|
||||
case MASTERKEY_SCHEME: {
|
||||
return new MasterkeyCryptoCloudProvider(cloudContentRepository, cryptoCloudContentRepositoryFactory, secureRandom);
|
||||
}
|
||||
default: throw new IllegalStateException(String.format("Provider with scheme %s not supported", unverifiedVaultConfigOptional.get().getKeyId().getScheme()));
|
||||
}
|
||||
} 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;
|
||||
|
||||
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 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 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 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 Context context;
|
||||
final DirIdCache dirIdCache;
|
||||
final int shorteningThreshold;
|
||||
|
||||
private final Supplier<Cryptor> cryptor;
|
||||
private final CloudFolder storageLocation;
|
||||
|
||||
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.cryptor = cryptor;
|
||||
this.cloudContentRepository = cloudContentRepository;
|
||||
this.storageLocation = storageLocation;
|
||||
this.dirIdCache = dirIdCache;
|
||||
this.shorteningThreshold = shorteningThreshold;
|
||||
}
|
||||
|
||||
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.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 LONG_NODE_FILE_EXT = ".c9s";
|
||||
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();
|
||||
|
||||
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
|
||||
@ -82,7 +85,7 @@ final class CryptoImplVaultFormat7 extends CryptoImplDecorator {
|
||||
.fileNameCryptor() //
|
||||
.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);
|
||||
}
|
||||
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 {
|
||||
|
||||
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 SYMLINK_PREFIX = "1S";
|
||||
private static final String LONG_NAME_FILE_EXT = ".lng";
|
||||
private static final String METADATA_DIR_NAME = "m";
|
||||
|
||||
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})$");
|
||||
|
||||
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
|
||||
@ -75,7 +74,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
|
||||
|
||||
private String encryptName(CryptoFolder cryptoParent, String name, String prefix) throws BackendException {
|
||||
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);
|
||||
}
|
||||
return ciphertextName;
|
||||
@ -140,7 +139,7 @@ final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
|
||||
if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
|
||||
try {
|
||||
ciphertextName = inflate(ciphertextName);
|
||||
if (ciphertextName.length() <= SHORT_NAMES_MAX_LENGTH) {
|
||||
if (ciphertextName.length() <= shorteningThreshold) {
|
||||
cloudFile = inflatePermanently(cloudFile, ciphertextName);
|
||||
}
|
||||
} 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 {
|
||||
if (exists(file) && !replace) {
|
||||
if (!replace && exists(file)) {
|
||||
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 {
|
||||
if (exists(file) && !replace) {
|
||||
if (!replace && exists(file)) {
|
||||
throw new CloudNodeAlreadyExistsException("CloudNode already exists and replace is false");
|
||||
}
|
||||
|
||||
|
@ -427,7 +427,7 @@ class LocalStorageAccessFrameworkImpl {
|
||||
|
||||
progressAware.onProgress(Progress.started(UploadState.upload(file)));
|
||||
Optional<Uri> fileUri = existingFileUri(file);
|
||||
if (fileUri.isPresent() && !replace) {
|
||||
if (!replace && fileUri.isPresent()) {
|
||||
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 {
|
||||
if (exists(file) && !replace) {
|
||||
if (!replace && exists(file)) {
|
||||
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) //
|
||||
throws BackendException, IOException {
|
||||
if (exists(uploadFile) && !replace) {
|
||||
if (!replace && exists(uploadFile)) {
|
||||
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.CloudFolder;
|
||||
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.CloudRepository;
|
||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -92,26 +94,30 @@ class CloudRepositoryImpl implements CloudRepository {
|
||||
return cryptoCloudFactory.decryptedViewOf(vault);
|
||||
}
|
||||
|
||||
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
|
||||
return cryptoCloudFactory.unverifiedVaultConfig(vault);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cloud unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, password, cancelledFlag);
|
||||
public Cloud unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, unverifiedVaultConfig, password, cancelledFlag);
|
||||
return decryptedViewOf(vaultWithVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cloud unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
Vault vaultWithVersion = cryptoCloudFactory.unlock(token, password, cancelledFlag);
|
||||
public Cloud unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
|
||||
Vault vaultWithVersion = cryptoCloudFactory.unlock(token, unverifiedVaultConfig, password, cancelledFlag);
|
||||
return decryptedViewOf(vaultWithVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnlockToken prepareUnlock(Vault vault) throws BackendException {
|
||||
return cryptoCloudFactory.createUnlockToken(vault);
|
||||
public UnlockToken prepareUnlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
|
||||
return cryptoCloudFactory.createUnlockToken(vault, unverifiedVaultConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException {
|
||||
return cryptoCloudFactory.isVaultPasswordValid(vault, password);
|
||||
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
|
||||
return cryptoCloudFactory.isVaultPasswordValid(vault, unverifiedVaultConfig, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -121,8 +127,8 @@ class CloudRepositoryImpl implements CloudRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException {
|
||||
cryptoCloudFactory.changePassword(vault, oldPassword, newPassword);
|
||||
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
|
||||
cryptoCloudFactory.changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,14 +1,10 @@
|
||||
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.CloudRepository;
|
||||
import org.cryptomator.domain.repository.UpdateCheckRepository;
|
||||
import org.cryptomator.domain.repository.VaultRepository;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
@ -17,12 +13,6 @@ import dagger.Provides;
|
||||
@Module
|
||||
public class RepositoryModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
public CryptorProvider provideCryptorProvider() {
|
||||
return Cryptors.version1(new SecureRandom());
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
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) //
|
||||
throws IOException, BackendException {
|
||||
if (exists(file) && !replace) {
|
||||
if (!replace && exists(file)) {
|
||||
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 "String", "VERSION_NAME", "\"${globalConfiguration["androidVersionName"]}\""
|
||||
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@ -50,10 +52,8 @@ dependencies {
|
||||
implementation dependencies.appcompat
|
||||
|
||||
api dependencies.jsonWebTokenApi
|
||||
runtimeOnly dependencies.jsonWebTokenImpl
|
||||
runtimeOnly(dependencies.jsonWebTokenJson) {
|
||||
exclude group: 'org.json', module: 'json' //provided by Android natively
|
||||
}
|
||||
implementation dependencies.jsonWebTokenImpl
|
||||
implementation dependencies.jsonWebTokenJson
|
||||
|
||||
// test
|
||||
testImplementation dependencies.junit
|
||||
@ -65,6 +65,7 @@ dependencies {
|
||||
testRuntimeOnly dependencies.junit4Engine
|
||||
|
||||
testImplementation dependencies.mockito
|
||||
testImplementation dependencies.mockitoInline
|
||||
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 boolean unlocked;
|
||||
private final String password;
|
||||
private final int version;
|
||||
private final int format;
|
||||
private final int shorteningThreshold;
|
||||
private final int position;
|
||||
|
||||
private Vault(Builder builder) {
|
||||
@ -23,7 +24,8 @@ public class Vault implements Serializable {
|
||||
this.unlocked = builder.unlocked;
|
||||
this.cloudType = builder.cloudType;
|
||||
this.password = builder.password;
|
||||
this.version = builder.version;
|
||||
this.format = builder.format;
|
||||
this.shorteningThreshold = builder.shorteningThreshold;
|
||||
this.position = builder.position;
|
||||
}
|
||||
|
||||
@ -40,7 +42,8 @@ public class Vault implements Serializable {
|
||||
.withPath(vault.getPath()) //
|
||||
.withUnlocked(vault.isUnlocked()) //
|
||||
.withSavedPassword(vault.getPassword()) //
|
||||
.withVersion(vault.getVersion()) //
|
||||
.withFormat(vault.getFormat()) //
|
||||
.withShorteningThreshold(vault.getShorteningThreshold()) //
|
||||
.withPosition(vault.getPosition());
|
||||
}
|
||||
|
||||
@ -72,8 +75,12 @@ public class Vault implements Serializable {
|
||||
return password;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
public int getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public int getShorteningThreshold() {
|
||||
return shorteningThreshold;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
@ -109,7 +116,8 @@ public class Vault implements Serializable {
|
||||
private CloudType cloudType;
|
||||
private boolean unlocked;
|
||||
private String password;
|
||||
private int version = -1;
|
||||
private int format = -1;
|
||||
private int shorteningThreshold = -1;
|
||||
private int position = -1;
|
||||
|
||||
private Builder() {
|
||||
@ -176,8 +184,13 @@ public class Vault implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withVersion(int version) {
|
||||
this.version = version;
|
||||
public Builder withFormat(int version) {
|
||||
this.format = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withShorteningThreshold(int shorteningThreshold) {
|
||||
this.shorteningThreshold = shorteningThreshold;
|
||||
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.CloudFolder;
|
||||
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.usecases.cloud.Flag;
|
||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -24,16 +26,18 @@ public interface CloudRepository {
|
||||
|
||||
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 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 {
|
||||
final Claims claims = Jwts //
|
||||
.parserBuilder().setSigningKey(getPublicKey()) //
|
||||
.build().parseClaimsJws(license) //
|
||||
.parserBuilder() //
|
||||
.setSigningKey(getPublicKey()) //
|
||||
.build() //
|
||||
.parseClaimsJws(license) //
|
||||
.getBody();
|
||||
|
||||
return claims::getSubject;
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.cryptomator.domain.usecases.vault;
|
||||
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||
import org.cryptomator.domain.Vault;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
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.generator.Parameter;
|
||||
import org.cryptomator.generator.UseCase;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
||||
|
||||
@ -16,23 +18,26 @@ class ChangePassword {
|
||||
|
||||
private final CloudRepository cloudRepository;
|
||||
private final Vault vault;
|
||||
private final Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||
private final String oldPassword;
|
||||
private final String newPassword;
|
||||
private final String newPassword;;
|
||||
|
||||
public ChangePassword(CloudRepository cloudRepository, //
|
||||
@Parameter Vault vault, //
|
||||
@Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig, //
|
||||
@Parameter String oldPassword, //
|
||||
@Parameter String newPassword) {
|
||||
this.cloudRepository = cloudRepository;
|
||||
this.vault = vault;
|
||||
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||
this.oldPassword = oldPassword;
|
||||
this.newPassword = newPassword;
|
||||
}
|
||||
|
||||
public void execute() throws BackendException {
|
||||
try {
|
||||
if (cloudRepository.isVaultPasswordValid(vault, oldPassword)) {
|
||||
cloudRepository.changePassword(vault, oldPassword, newPassword);
|
||||
if (cloudRepository.isVaultPasswordValid(vault, unverifiedVaultConfig, oldPassword)) {
|
||||
cloudRepository.changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
|
||||
} else {
|
||||
throw new InvalidPassphraseException();
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
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.repository.CloudRepository;
|
||||
import org.cryptomator.generator.Parameter;
|
||||
import org.cryptomator.generator.UseCase;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
@UseCase
|
||||
class CheckVaultPassword {
|
||||
@ -12,15 +14,17 @@ class CheckVaultPassword {
|
||||
private final CloudRepository cloudRepository;
|
||||
private final Vault vault;
|
||||
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.vault = vault;
|
||||
this.password = password;
|
||||
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||
import org.cryptomator.domain.Vault;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
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.generator.Parameter;
|
||||
import org.cryptomator.generator.UseCase;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
import static org.cryptomator.util.ExceptionUtil.contains;
|
||||
|
||||
@ -15,15 +17,17 @@ class PrepareUnlock {
|
||||
|
||||
private final CloudRepository cloudRepository;
|
||||
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.vault = vault;
|
||||
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||
}
|
||||
|
||||
public UnlockToken execute() throws BackendException {
|
||||
try {
|
||||
return cloudRepository.prepareUnlock(vault);
|
||||
return cloudRepository.prepareUnlock(vault, unverifiedVaultConfig);
|
||||
} catch (BackendException e) {
|
||||
if (contains(e, NoSuchCloudFileException.class)) {
|
||||
throw new NoSuchVaultException(vault, e);
|
||||
|
@ -1,17 +1,20 @@
|
||||
package org.cryptomator.domain.usecases.vault;
|
||||
|
||||
import org.cryptomator.domain.Cloud;
|
||||
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
import org.cryptomator.domain.repository.CloudRepository;
|
||||
import org.cryptomator.domain.usecases.cloud.Flag;
|
||||
import org.cryptomator.generator.Parameter;
|
||||
import org.cryptomator.generator.UseCase;
|
||||
import org.cryptomator.util.Optional;
|
||||
|
||||
@UseCase
|
||||
class UnlockVault {
|
||||
class UnlockVaultUsingMasterkey {
|
||||
|
||||
private final CloudRepository cloudRepository;
|
||||
private final VaultOrUnlockToken vaultOrUnlockToken;
|
||||
private Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||
private final String password;
|
||||
|
||||
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.vaultOrUnlockToken = vaultOrUnlockToken;
|
||||
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@ -34,9 +38,9 @@ class UnlockVault {
|
||||
|
||||
public Cloud execute() throws BackendException {
|
||||
if (vaultOrUnlockToken.getVault().isPresent()) {
|
||||
return cloudRepository.unlock(vaultOrUnlockToken.getVault().get(), password, cancelledFlag);
|
||||
return cloudRepository.unlock(vaultOrUnlockToken.getVault().get(), unverifiedVaultConfig, password, cancelledFlag);
|
||||
} 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;
|
||||
|
||||
import org.cryptomator.domain.UnverifiedVaultConfig;
|
||||
import org.cryptomator.domain.Vault;
|
||||
import org.cryptomator.domain.exception.BackendException;
|
||||
import org.cryptomator.domain.repository.CloudRepository;
|
||||
import org.cryptomator.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class UnlockVaultTest {
|
||||
public class UnlockVaultUsingMasterkeyTest {
|
||||
|
||||
private static final String A_STRING = "89dfhsjdhfjsd";
|
||||
|
||||
@ -19,30 +21,33 @@ public class UnlockVaultTest {
|
||||
|
||||
private CloudRepository cloudRepository;
|
||||
|
||||
private UnlockVault inTest;
|
||||
private UnlockVaultUsingMasterkey inTest;
|
||||
|
||||
private Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
unlockToken = Mockito.mock(UnlockToken.class);
|
||||
vault = Mockito.mock(Vault.class);
|
||||
cloudRepository = Mockito.mock(CloudRepository.class);
|
||||
inTest = Mockito.mock(UnlockVault.class);
|
||||
unverifiedVaultConfig = Mockito.mock(Optional.class);
|
||||
inTest = Mockito.mock(UnlockVaultUsingMasterkey.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
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();
|
||||
|
||||
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
|
||||
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();
|
||||
|
||||
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.rxBinding
|
||||
|
||||
api dependencies.jsonWebTokenApi
|
||||
runtimeOnly dependencies.jsonWebTokenImpl
|
||||
runtimeOnly(dependencies.jsonWebTokenJson) {
|
||||
exclude group: 'org.json', module: 'json' //provided by Android natively
|
||||
}
|
||||
|
||||
// multidex
|
||||
implementation dependencies.multidex
|
||||
|
||||
@ -237,3 +231,16 @@ androidExtensions {
|
||||
static def getApiKey(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>
|
||||
</activity>
|
||||
|
||||
|
||||
<activity android:name=".ui.activity.UnlockVaultActivity"
|
||||
android:theme="@style/TransparentAlertDialogCustom"
|
||||
android:label=""/>
|
||||
<activity android:name=".ui.activity.EmptyDirIdFileInfoActivity" />
|
||||
|
||||
|
||||
<!-- Settings -->
|
||||
<activity android:name=".ui.activity.BiometricAuthSettingsActivity" />
|
||||
<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.SplashActivity;
|
||||
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.WebDavAddOrChangeActivity;
|
||||
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.SharedFilesFragment;
|
||||
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.WebDavAddOrChangeFragment;
|
||||
import org.cryptomator.presentation.workflow.AddExistingVaultWorkflow;
|
||||
@ -117,6 +119,10 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(LicenseCheckActivity licenseCheckActivity);
|
||||
|
||||
void inject(UnlockVaultActivity unlockVaultActivity);
|
||||
|
||||
void inject(UnlockVaultFragment unlockVaultFragment);
|
||||
|
||||
void inject(S3AddOrChangeActivity s3AddOrChangeActivity);
|
||||
|
||||
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.HashMismatchUpdateCheckException
|
||||
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.ui.activity.view.View
|
||||
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(GeneralUpdateErrorException::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)
|
||||
exceptionHandlers.add(MissingCryptorExceptionHandler())
|
||||
exceptionHandlers.add(CancellationExceptionHandler())
|
||||
|
@ -160,12 +160,19 @@ public class ChooseCloudNodeSettings implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder selectingFilesWithNameOnly(String name) {
|
||||
public Builder selectingFileWithNameOnly(String name) {
|
||||
this.selectionMode = FILES_ONLY;
|
||||
this.namePattern = Pattern.compile(Pattern.quote(name));
|
||||
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) {
|
||||
this.selectionMode = FOLDERS_ONLY;
|
||||
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
|
||||
val position: Int
|
||||
get() = vault.position
|
||||
val format: Int
|
||||
get() = vault.format
|
||||
val shorteningThreshold: Int
|
||||
get() = vault.shorteningThreshold
|
||||
|
||||
fun toVault(): Vault {
|
||||
return vault
|
||||
|
@ -7,17 +7,14 @@ import org.cryptomator.domain.di.PerView
|
||||
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase
|
||||
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase
|
||||
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.presentation.R
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.intent.ChooseCloudNodeSettings
|
||||
import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||
import org.cryptomator.presentation.model.CloudFolderModel
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.VaultModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
||||
import org.cryptomator.presentation.ui.activity.view.AutoUploadChooseVaultView
|
||||
@ -32,8 +29,6 @@ class AutoUploadChooseVaultPresenter @Inject constructor( //
|
||||
private val getVaultListUseCase: GetVaultListUseCase, //
|
||||
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
||||
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
||||
private val unlockVaultUseCase: UnlockVaultUseCase, //
|
||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
|
||||
private val cloudFolderModelMapper: CloudFolderModelMapper, //
|
||||
private val sharedPreferencesHandler: SharedPreferencesHandler, //
|
||||
private val authenticationExceptionHandler: AuthenticationExceptionHandler, //
|
||||
@ -83,11 +78,24 @@ class AutoUploadChooseVaultPresenter @Inject constructor( //
|
||||
decryptedCloudFor(authenticatedVault)
|
||||
} else {
|
||||
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) {
|
||||
getDecryptedCloudForVaultUseCase //
|
||||
.withVault(vault) //
|
||||
@ -151,49 +159,11 @@ class AutoUploadChooseVaultPresenter @Inject constructor( //
|
||||
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 {
|
||||
CHOOSE_LOCATION, INIT_ROOT
|
||||
}
|
||||
|
||||
init {
|
||||
unsubscribeOnDestroy(getVaultListUseCase)
|
||||
unsubscribeOnDestroy(getVaultListUseCase, getRootFolderUseCase, getDecryptedCloudForVaultUseCase)
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,21 @@ package org.cryptomator.presentation.presenter
|
||||
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.Vault
|
||||
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.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||
import org.cryptomator.presentation.model.VaultModel
|
||||
import org.cryptomator.presentation.ui.activity.view.BiometricAuthSettingsView
|
||||
import org.cryptomator.presentation.workflow.ActivityResult
|
||||
import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler
|
||||
import org.cryptomator.util.SharedPreferencesHandler
|
||||
import java.util.*
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
@ -24,13 +24,9 @@ import timber.log.Timber
|
||||
class BiometricAuthSettingsPresenter @Inject constructor( //
|
||||
private val getVaultListUseCase: GetVaultListUseCase, //
|
||||
private val saveVaultUseCase: SaveVaultUseCase, //
|
||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
|
||||
private val checkVaultPasswordUseCase: CheckVaultPasswordUseCase, //
|
||||
private val unlockVaultUseCase: UnlockVaultUseCase, //
|
||||
private val lockVaultUseCase: LockVaultUseCase, //
|
||||
exceptionMappings: ExceptionHandlers, //
|
||||
private val sharedPreferencesHandler: SharedPreferencesHandler, //
|
||||
private val authenticationExceptionHandler: AuthenticationExceptionHandler) : Presenter<BiometricAuthSettingsView>(exceptionMappings) {
|
||||
private val sharedPreferencesHandler: SharedPreferencesHandler) : Presenter<BiometricAuthSettingsView>(exceptionMappings) {
|
||||
|
||||
fun loadVaultList() {
|
||||
updateVaultListView()
|
||||
@ -49,92 +45,56 @@ class BiometricAuthSettingsPresenter @Inject constructor( //
|
||||
|
||||
fun updateVaultEntityWithChangedBiometricAuthSettings(vaultModel: VaultModel, useBiometricAuth: Boolean) {
|
||||
if (useBiometricAuth) {
|
||||
view?.showEnterPasswordDialog(VaultModel(vaultModel.toVault()))
|
||||
verifyPassword(vaultModel)
|
||||
} else {
|
||||
removePasswordAndSave(vaultModel.toVault())
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyPassword(vaultModel: VaultModel) {
|
||||
private fun verifyPassword(vaultModel: VaultModel) {
|
||||
Timber.tag("BiomtricAuthSettngsPres").i("Checking entered vault password")
|
||||
if (vaultModel.isLocked) {
|
||||
unlockVault(vaultModel)
|
||||
requestActivityResult( //
|
||||
ActivityResultCallbacks.vaultUnlockedBiometricAuthPres(vaultModel), //
|
||||
Intents.unlockVaultIntent().withVaultModel(vaultModel).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH))
|
||||
} 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) {
|
||||
view?.showProgress(ProgressModel.GENERIC)
|
||||
checkVaultPasswordUseCase //
|
||||
.withVault(vaultModel.toVault()) //
|
||||
.andPassword(vaultModel.password) //
|
||||
.run(object : DefaultResultHandler<Boolean>() {
|
||||
override fun onSuccess(passwordCorrect: Boolean) {
|
||||
if (passwordCorrect) {
|
||||
Timber.tag("BiomtricAuthSettngsPres").i("Password is correct")
|
||||
onPasswordCheckSucceeded(vaultModel)
|
||||
} 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
|
||||
fun vaultUnlockedBiometricAuthPres(result: ActivityResult, vaultModel: VaultModel) {
|
||||
val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud
|
||||
val password = result.intent().getStringExtra(UnlockVaultPresenter.PASSWORD)
|
||||
val vault = Vault.aCopyOf(vaultModel.toVault()).withCloud(cloud).withSavedPassword(password).build()
|
||||
when {
|
||||
result.isResultOk -> requestActivityResult( //
|
||||
ActivityResultCallbacks.encryptVaultPassword(vaultModel), //
|
||||
Intents.unlockVaultIntent().withVaultModel(VaultModel(vault)).withVaultAction(UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD))
|
||||
else -> TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun unlockVaultAfterAuth(result: ActivityResult, vault: Vault?) {
|
||||
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
|
||||
val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build()
|
||||
unlockVault(VaultModel(vaultWithUpdatedCloud))
|
||||
fun encryptVaultPassword(result: ActivityResult, vaultModel: VaultModel) {
|
||||
val tmpVault = result.intent().getSerializableExtra(SINGLE_RESULT) as VaultModel
|
||||
val vault = Vault.aCopyOf(vaultModel.toVault()).withSavedPassword(tmpVault.password).build()
|
||||
when {
|
||||
result.isResultOk -> saveVault(vault)
|
||||
else -> TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPasswordCheckSucceeded(vaultModel: VaultModel) {
|
||||
view?.showBiometricAuthenticationDialog(vaultModel)
|
||||
}
|
||||
|
||||
fun saveVault(vault: Vault?) {
|
||||
private fun saveVault(vault: Vault?) {
|
||||
saveVaultUseCase //
|
||||
.withVault(vault) //
|
||||
.run(object : ProgressCompletingResultHandler<Vault>() {
|
||||
@ -145,8 +105,7 @@ class BiometricAuthSettingsPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
fun switchedGeneralBiometricAuthSettings(isChecked: Boolean) {
|
||||
sharedPreferencesHandler //
|
||||
.changeUseBiometricAuthentication(isChecked)
|
||||
sharedPreferencesHandler.changeUseBiometricAuthentication(isChecked)
|
||||
if (isChecked) {
|
||||
loadVaultList()
|
||||
} else {
|
||||
@ -173,32 +132,15 @@ class BiometricAuthSettingsPresenter @Inject constructor( //
|
||||
|
||||
fun onSetupBiometricAuthInSystemClicked() {
|
||||
val openSecuritySettings = Intent(Settings.ACTION_SECURITY_SETTINGS)
|
||||
requestActivityResult(ActivityResultCallbacks.onSetupFingerCompleted(), openSecuritySettings)
|
||||
requestActivityResult(ActivityResultCallbacks.onSetupBiometricAuthInSystemCompleted(), openSecuritySettings)
|
||||
}
|
||||
|
||||
@Callback
|
||||
fun onSetupFingerCompleted(result: ActivityResult?) {
|
||||
fun onSetupBiometricAuthInSystemCompleted(result: ActivityResult?) {
|
||||
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 {
|
||||
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.cloud.*
|
||||
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.InstanceState
|
||||
import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.intent.ChooseCloudNodeSettings
|
||||
import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||
import org.cryptomator.presentation.model.*
|
||||
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
||||
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.PermissionsResult
|
||||
import org.cryptomator.util.Optional
|
||||
import org.cryptomator.util.SharedPreferencesHandler
|
||||
import org.cryptomator.util.file.FileCacheUtils
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
@ -36,14 +33,11 @@ import timber.log.Timber
|
||||
@PerView
|
||||
class SharedFilesPresenter @Inject constructor( //
|
||||
private val getVaultListUseCase: GetVaultListUseCase, //
|
||||
private val unlockVaultUseCase: UnlockVaultUseCase, //
|
||||
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
||||
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
||||
private val uploadFilesUseCase: UploadFilesUseCase, //
|
||||
private val getCloudListUseCase: GetCloudListUseCase, //
|
||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
|
||||
private val contentResolverUtil: ContentResolverUtil, //
|
||||
private val sharedPreferencesHandler: SharedPreferencesHandler, //
|
||||
private val fileCacheUtils: FileCacheUtils, //
|
||||
private val authenticationExceptionHandler: AuthenticationExceptionHandler, //
|
||||
private val cloudFolderModelMapper: CloudFolderModelMapper, //
|
||||
@ -128,6 +122,28 @@ class SharedFilesPresenter @Inject constructor( //
|
||||
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) {
|
||||
getDecryptedCloudForVaultUseCase //
|
||||
.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) {
|
||||
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() {
|
||||
authenticate(selectedVault)
|
||||
}
|
||||
@ -410,25 +390,6 @@ class SharedFilesPresenter @Inject constructor( //
|
||||
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 {
|
||||
CHOOSE_LOCATION, INIT_ROOT
|
||||
}
|
||||
@ -444,11 +405,9 @@ class SharedFilesPresenter @Inject constructor( //
|
||||
init {
|
||||
unsubscribeOnDestroy( //
|
||||
getRootFolderUseCase, //
|
||||
unlockVaultUseCase, //
|
||||
getVaultListUseCase, //
|
||||
getDecryptedCloudForVaultUseCase, //
|
||||
uploadFilesUseCase, //
|
||||
getCloudListUseCase, //
|
||||
removeStoredVaultPasswordsUseCase)
|
||||
getCloudListUseCase)
|
||||
}
|
||||
}
|
||||
|
@ -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.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.widget.Toast
|
||||
import androidx.biometric.BiometricManager
|
||||
import org.cryptomator.data.cloud.crypto.CryptoCloud
|
||||
import org.cryptomator.data.util.NetworkConnectionCheck
|
||||
import org.cryptomator.domain.Cloud
|
||||
import org.cryptomator.domain.CloudFolder
|
||||
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.exception.license.LicenseNotValidException
|
||||
import org.cryptomator.domain.exception.update.SSLHandshakePreAndroid5UpdateCheckException
|
||||
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.UpdateCheck
|
||||
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.GetVaultListUseCase
|
||||
import org.cryptomator.domain.usecases.vault.LockVaultUseCase
|
||||
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.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.presentation.BuildConfig
|
||||
import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers
|
||||
import org.cryptomator.presentation.intent.Intents
|
||||
import org.cryptomator.presentation.intent.UnlockVaultIntent
|
||||
import org.cryptomator.presentation.model.CloudModel
|
||||
import org.cryptomator.presentation.model.CloudTypeModel
|
||||
import org.cryptomator.presentation.model.ProgressModel
|
||||
import org.cryptomator.presentation.model.ProgressStateModel
|
||||
import org.cryptomator.presentation.model.VaultModel
|
||||
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper
|
||||
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.util.Optional
|
||||
import org.cryptomator.util.SharedPreferencesHandler
|
||||
import java.io.Serializable
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
@ -77,15 +66,11 @@ class VaultListPresenter @Inject constructor( //
|
||||
private val renameVaultUseCase: RenameVaultUseCase, //
|
||||
private val lockVaultUseCase: LockVaultUseCase, //
|
||||
private val getDecryptedCloudForVaultUseCase: GetDecryptedCloudForVaultUseCase, //
|
||||
private val prepareUnlockUseCase: PrepareUnlockUseCase, //
|
||||
private val unlockVaultUseCase: UnlockVaultUseCase, //
|
||||
private val getRootFolderUseCase: GetRootFolderUseCase, //
|
||||
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
|
||||
private val createNewVaultWorkflow: CreateNewVaultWorkflow, //
|
||||
private val saveVaultUseCase: SaveVaultUseCase, //
|
||||
private val moveVaultPositionUseCase: MoveVaultPositionUseCase, //
|
||||
private val changePasswordUseCase: ChangePasswordUseCase, //
|
||||
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
|
||||
private val licenseCheckUseCase: DoLicenseCheckUseCase, //
|
||||
private val updateCheckUseCase: DoUpdateCheckUseCase, //
|
||||
private val updateUseCase: DoUpdateUseCase, //
|
||||
@ -97,31 +82,14 @@ class VaultListPresenter @Inject constructor( //
|
||||
exceptionMappings: ExceptionHandlers) : Presenter<VaultListView>(exceptionMappings) {
|
||||
|
||||
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<*>> {
|
||||
return listOf(addExistingVaultWorkflow, createNewVaultWorkflow)
|
||||
}
|
||||
|
||||
override fun destroyed() {
|
||||
super.destroyed()
|
||||
if (retryUnlockHandler != null) {
|
||||
running = false
|
||||
retryUnlockHandler?.removeCallbacks(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
if (hasFocus) {
|
||||
loadVaultList()
|
||||
if (retryUnlockHandler != null) {
|
||||
running = false
|
||||
retryUnlockHandler?.removeCallbacks(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,12 +192,8 @@ class VaultListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
fun deleteVault(vaultModel: VaultModel) {
|
||||
deleteVault(vaultModel.toVault())
|
||||
}
|
||||
|
||||
private fun deleteVault(vault: Vault) {
|
||||
deleteVaultUseCase //
|
||||
.withVault(vault) //
|
||||
.withVault(vaultModel.toVault()) //
|
||||
.run(object : DefaultResultHandler<Long>() {
|
||||
override fun onSuccess(vaultId: Long) {
|
||||
view?.deleteVaultFromAdapter(vaultId)
|
||||
@ -264,11 +228,6 @@ class VaultListPresenter @Inject constructor( //
|
||||
renameVault(VaultModel(vaultWithUpdatedCloud), newVaultName)
|
||||
}
|
||||
|
||||
fun onUnlockCanceled() {
|
||||
prepareUnlockUseCase.unsubscribe()
|
||||
unlockVaultUseCase.cancel()
|
||||
}
|
||||
|
||||
private fun browseFilesOf(vault: VaultModel) {
|
||||
getDecryptedCloudForVaultUseCase //
|
||||
.withVault(vault.toVault()) //
|
||||
@ -325,7 +284,6 @@ class VaultListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
fun onVaultClicked(vault: VaultModel) {
|
||||
startedUsingPrepareUnlock = sharedPreferencesHandler.backgroundUnlockPreparation()
|
||||
startVaultAction(vault, VaultAction.UNLOCK)
|
||||
}
|
||||
|
||||
@ -385,7 +343,6 @@ class VaultListPresenter @Inject constructor( //
|
||||
when (vaultAction) {
|
||||
VaultAction.UNLOCK -> requireUserAuthentication(authenticatedVaultModel)
|
||||
VaultAction.RENAME -> view?.showRenameDialog(authenticatedVaultModel)
|
||||
VaultAction.CHANGE_PASSWORD -> view?.showChangePasswordDialog(authenticatedVaultModel)
|
||||
}
|
||||
vaultAction = null
|
||||
}
|
||||
@ -394,83 +351,22 @@ class VaultListPresenter @Inject constructor( //
|
||||
view?.addOrUpdateVault(authenticatedVault)
|
||||
if (authenticatedVault.isLocked) {
|
||||
if (!isPaused) {
|
||||
if (canUseBiometricOn(authenticatedVault)) {
|
||||
if (startedUsingPrepareUnlock) {
|
||||
startPrepareUnlockUseCase(authenticatedVault.toVault())
|
||||
}
|
||||
view?.showBiometricDialog(authenticatedVault)
|
||||
} else {
|
||||
startPrepareUnlockUseCase(authenticatedVault.toVault())
|
||||
view?.showEnterPasswordDialog(authenticatedVault)
|
||||
}
|
||||
requestActivityResult( //
|
||||
ActivityResultCallbacks.vaultUnlockedVaultList(), //
|
||||
Intents.unlockVaultIntent().withVaultModel(authenticatedVault).withVaultAction(UnlockVaultIntent.VaultAction.UNLOCK))
|
||||
}
|
||||
} else {
|
||||
browseFilesOf(authenticatedVault)
|
||||
}
|
||||
}
|
||||
|
||||
fun startPrepareUnlockUseCase(vault: Vault) {
|
||||
pendingUnlock = null
|
||||
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 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)
|
||||
}
|
||||
})
|
||||
@Callback
|
||||
fun vaultUnlockedVaultList(result: ActivityResult) {
|
||||
val cloud = result.intent().getSerializableExtra(SINGLE_RESULT) as Cloud
|
||||
when {
|
||||
result.isResultOk -> navigateToVaultContent(cloud)
|
||||
else -> TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToVaultContent(cloud: Cloud) {
|
||||
@ -495,53 +391,6 @@ class VaultListPresenter @Inject constructor( //
|
||||
} 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() {
|
||||
addExistingVaultWorkflow.start()
|
||||
}
|
||||
@ -557,60 +406,11 @@ class VaultListPresenter @Inject constructor( //
|
||||
}
|
||||
|
||||
fun onChangePasswordClicked(vaultModel: VaultModel) {
|
||||
startVaultAction(vaultModel, VaultAction.CHANGE_PASSWORD)
|
||||
}
|
||||
|
||||
fun onChangePasswordClicked(vaultModel: VaultModel, oldPassword: String?, newPassword: String?) {
|
||||
view?.showProgress(ProgressModel(ProgressStateModel.CHANGING_PASSWORD))
|
||||
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")
|
||||
}
|
||||
})
|
||||
Intents
|
||||
.unlockVaultIntent()
|
||||
.withVaultModel(vaultModel)
|
||||
.withVaultAction(UnlockVaultIntent.VaultAction.CHANGE_PASSWORD)
|
||||
.startActivity(this)
|
||||
}
|
||||
|
||||
fun onVaultSettingsClicked(vaultModel: VaultModel) {
|
||||
@ -636,10 +436,6 @@ class VaultListPresenter @Inject constructor( //
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteMissingVaultClicked(vault: Vault) {
|
||||
deleteVault(vault)
|
||||
}
|
||||
|
||||
fun onFilteredTouchEventForSecurity() {
|
||||
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 {
|
||||
UNLOCK, RENAME, CHANGE_PASSWORD
|
||||
UNLOCK, RENAME
|
||||
}
|
||||
|
||||
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 {
|
||||
unsubscribeOnDestroy( //
|
||||
deleteVaultUseCase, //
|
||||
@ -752,9 +493,6 @@ class VaultListPresenter @Inject constructor( //
|
||||
getVaultListUseCase, //
|
||||
saveVaultUseCase, //
|
||||
moveVaultPositionUseCase, //
|
||||
removeStoredVaultPasswordsUseCase, //
|
||||
unlockVaultUseCase, //
|
||||
prepareUnlockUseCase, //
|
||||
licenseCheckUseCase, //
|
||||
updateCheckUseCase, //
|
||||
updateUseCase)
|
||||
|
@ -1,7 +1,5 @@
|
||||
package org.cryptomator.presentation.ui.activity
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.cryptomator.generator.Activity
|
||||
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.presenter.AutoUploadChooseVaultPresenter
|
||||
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.fragment.AutoUploadChooseVaultFragment
|
||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
||||
import javax.inject.Inject
|
||||
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
||||
|
||||
@Activity
|
||||
class AutoUploadChooseVaultActivity : BaseActivity(), //
|
||||
AutoUploadChooseVaultView, //
|
||||
NotEnoughVaultsDialog.Callback, //
|
||||
EnterPasswordDialog.Callback,
|
||||
BiometricAuthentication.Callback {
|
||||
NotEnoughVaultsDialog.Callback {
|
||||
|
||||
@Inject
|
||||
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>) {
|
||||
@ -69,41 +62,5 @@ class AutoUploadChooseVaultActivity : BaseActivity(), //
|
||||
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
|
||||
}
|
||||
|
@ -1,28 +1,20 @@
|
||||
package org.cryptomator.presentation.ui.activity
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.cryptomator.domain.Vault
|
||||
import org.cryptomator.generator.Activity
|
||||
import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.model.VaultModel
|
||||
import org.cryptomator.presentation.presenter.BiometricAuthSettingsPresenter
|
||||
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.EnterPasswordDialog
|
||||
import org.cryptomator.presentation.ui.fragment.BiometricAuthSettingsFragment
|
||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
||||
import javax.inject.Inject
|
||||
import kotlinx.android.synthetic.main.toolbar_layout.toolbar
|
||||
|
||||
@Activity
|
||||
class BiometricAuthSettingsActivity : BaseActivity(), //
|
||||
EnterPasswordDialog.Callback, //
|
||||
BiometricAuthSettingsView, //
|
||||
BiometricAuthentication.Callback, //
|
||||
EnrollSystemBiometricDialog.Callback {
|
||||
|
||||
@Inject
|
||||
@ -44,10 +36,6 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
|
||||
}
|
||||
}
|
||||
|
||||
override fun showBiometricAuthKeyInvalidatedDialog() {
|
||||
showDialog(BiometricAuthKeyInvalidatedDialog.newInstance())
|
||||
}
|
||||
|
||||
override fun createFragment(): Fragment? = BiometricAuthSettingsFragment()
|
||||
|
||||
override fun renderVaultList(vaultModelCollection: List<VaultModel>) {
|
||||
@ -58,30 +46,6 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
|
||||
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
|
||||
|
||||
override fun onSetupBiometricAuthInSystemClicked() {
|
||||
@ -91,17 +55,4 @@ class BiometricAuthSettingsActivity : BaseActivity(), //
|
||||
override fun onCancelSetupBiometricAuthInSystemClicked() {
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SearchView
|
||||
@ -228,9 +227,6 @@ class BrowseFilesActivity : BaseActivity(), //
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
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_move_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_MULTIPLE
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.cryptomator.generator.Activity
|
||||
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.presenter.SharedFilesPresenter
|
||||
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.ReplaceDialog
|
||||
import org.cryptomator.presentation.ui.dialog.UploadCloudFileDialog
|
||||
import org.cryptomator.presentation.ui.fragment.SharedFilesFragment
|
||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
||||
import java.lang.String.format
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
@ -32,8 +27,6 @@ import timber.log.Timber
|
||||
@Activity
|
||||
class SharedFilesActivity : BaseActivity(), //
|
||||
SharedFilesView, //
|
||||
EnterPasswordDialog.Callback, //
|
||||
BiometricAuthentication.Callback, //
|
||||
ReplaceDialog.Callback, //
|
||||
NotEnoughVaultsDialog.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) {
|
||||
android.R.id.home -> {
|
||||
@ -150,16 +143,6 @@ class SharedFilesActivity : BaseActivity(), //
|
||||
|
||||
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) {
|
||||
ReplaceDialog.withContext(this).show(existingFiles, size)
|
||||
}
|
||||
@ -168,22 +151,10 @@ class SharedFilesActivity : BaseActivity(), //
|
||||
sharedFilesFragment().showChosenLocation(folder)
|
||||
}
|
||||
|
||||
override fun showBiometricAuthKeyInvalidatedDialog() {
|
||||
showDialog(BiometricAuthKeyInvalidatedDialog.newInstance())
|
||||
}
|
||||
|
||||
override fun showUploadDialog(uploadingFiles: Int) {
|
||||
showDialog(UploadCloudFileDialog.newInstance(uploadingFiles))
|
||||
}
|
||||
|
||||
override fun onUnlockClick(vaultModel: VaultModel, password: String) {
|
||||
presenter.onUnlockPressed(vaultModel, password)
|
||||
}
|
||||
|
||||
override fun onUnlockCanceled() {
|
||||
presenter.onUnlockCanceled()
|
||||
}
|
||||
|
||||
override fun onReplacePositiveClicked() {
|
||||
presenter.onReplaceExistingFilesPressed()
|
||||
}
|
||||
@ -209,20 +180,6 @@ class SharedFilesActivity : BaseActivity(), //
|
||||
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() {
|
||||
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.net.Uri
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.cryptomator.domain.Vault
|
||||
import org.cryptomator.generator.Activity
|
||||
import org.cryptomator.generator.InjectIntent
|
||||
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.dialog.AskForLockScreenDialog
|
||||
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.UpdateAppDialog
|
||||
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.fragment.VaultListFragment
|
||||
import org.cryptomator.presentation.ui.layout.ObscuredAwareCoordinatorLayout.Listener
|
||||
import org.cryptomator.presentation.util.BiometricAuthentication
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
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(), //
|
||||
VaultListView, //
|
||||
VaultListCallback, //
|
||||
BiometricAuthentication.Callback, //
|
||||
AskForLockScreenDialog.Callback, //
|
||||
ChangePasswordDialog.Callback, //
|
||||
VaultNotFoundDialog.Callback,
|
||||
UpdateAppAvailableDialog.Callback, //
|
||||
UpdateAppDialog.Callback, //
|
||||
BetaConfirmationDialog.Callback {
|
||||
@ -59,8 +48,6 @@ class VaultListActivity : BaseActivity(), //
|
||||
@InjectIntent
|
||||
lateinit var vaultListIntent: VaultListIntent
|
||||
|
||||
private var biometricAuthentication: BiometricAuthentication? = null
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
vaultListPresenter.onWindowFocusChanged(hasFocus)
|
||||
@ -125,40 +112,6 @@ class VaultListActivity : BaseActivity(), //
|
||||
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) {
|
||||
vaultListFragment().rowMoved(fromPosition, toPosition)
|
||||
}
|
||||
@ -209,14 +162,6 @@ class VaultListActivity : BaseActivity(), //
|
||||
vaultListPresenter.onCreateVault()
|
||||
}
|
||||
|
||||
override fun onUnlockClick(vaultModel: VaultModel, password: String) {
|
||||
vaultListPresenter.onUnlockClick(vaultModel, password)
|
||||
}
|
||||
|
||||
override fun onUnlockCanceled() {
|
||||
vaultListPresenter.onUnlockCanceled()
|
||||
}
|
||||
|
||||
override fun onDeleteVaultClick(vaultModel: VaultModel) {
|
||||
VaultDeleteConfirmationDialog.newInstance(vaultModel) //
|
||||
.show(supportFragmentManager, "VaultDeleteConfirmationDialog")
|
||||
@ -249,14 +194,6 @@ class VaultListActivity : BaseActivity(), //
|
||||
private fun vaultListFragment(): 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() {
|
||||
showProgress(ProgressModel.GENERIC)
|
||||
}
|
||||
@ -279,21 +216,4 @@ class VaultListActivity : BaseActivity(), //
|
||||
override fun onAskForBetaConfirmationFinished() {
|
||||
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 displayVaults(vaults: List<VaultModel>)
|
||||
fun showChosenLocation(location: CloudFolderModel)
|
||||
fun showEnterPasswordDialog(vaultModel: VaultModel)
|
||||
fun showBiometricAuthKeyInvalidatedDialog()
|
||||
|
||||
}
|
||||
|
@ -6,9 +6,6 @@ interface BiometricAuthSettingsView : View {
|
||||
|
||||
fun renderVaultList(vaultModelCollection: List<VaultModel>)
|
||||
fun clearVaultList()
|
||||
fun showBiometricAuthenticationDialog(vaultModel: VaultModel)
|
||||
fun showEnterPasswordDialog(vaultModel: VaultModel)
|
||||
fun showSetupBiometricAuthDialog()
|
||||
fun showBiometricAuthKeyInvalidatedDialog()
|
||||
|
||||
}
|
||||
|
@ -11,10 +11,8 @@ interface SharedFilesView : View {
|
||||
fun displayVaults(vaults: List<VaultModel>)
|
||||
fun displayFilesToUpload(sharedFiles: List<SharedFileModel>)
|
||||
fun displayDialogUnableToUploadFiles()
|
||||
fun showEnterPasswordDialog(vault: VaultModel)
|
||||
fun showReplaceDialog(existingFiles: List<String>, size: Int)
|
||||
fun showChosenLocation(folder: CloudFolderModel)
|
||||
fun showBiometricAuthKeyInvalidatedDialog()
|
||||
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 renameVault(vaultModel: VaultModel)
|
||||
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 showAddVaultBottomSheet()
|
||||
fun showRenameDialog(vaultModel: VaultModel)
|
||||
fun showBiometricAuthKeyInvalidatedDialog()
|
||||
fun isVaultLocked(vaultModel: VaultModel): Boolean
|
||||
fun cancelBasicAuthIfRunning()
|
||||
fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean
|
||||
fun rowMoved(fromPosition: Int, toPosition: Int)
|
||||
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.SettingsVaultBottomSheet
|
||||
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
|
||||
import org.cryptomator.presentation.ui.dialog.VaultDeleteConfirmationDialog
|
||||
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.widget.Button
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import org.cryptomator.domain.UnverifiedVaultConfig
|
||||
import org.cryptomator.generator.Dialog
|
||||
import org.cryptomator.presentation.R
|
||||
import org.cryptomator.presentation.model.VaultModel
|
||||
@ -23,7 +24,7 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
||||
|
||||
interface Callback {
|
||||
|
||||
fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String)
|
||||
fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@ -33,10 +34,12 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
||||
changePasswordButton = dialog.getButton(android.app.Dialog.BUTTON_POSITIVE)
|
||||
changePasswordButton?.setOnClickListener {
|
||||
val vaultModel = requireArguments().getSerializable(VAULT_ARG) as VaultModel
|
||||
val unverifiedVaultConfig = requireArguments().getSerializable(VAULT_CONFIG_ARG) as UnverifiedVaultConfig?
|
||||
if (valid(et_old_password.text.toString(), //
|
||||
et_new_password.text.toString(), //
|
||||
et_new_retype_password.text.toString())) {
|
||||
callback?.onChangePasswordClick(vaultModel, //
|
||||
unverifiedVaultConfig, //
|
||||
et_old_password.text.toString(), //
|
||||
et_new_password.text.toString())
|
||||
onWaitForResponse(et_old_password)
|
||||
@ -101,9 +104,11 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
|
||||
companion object {
|
||||
|
||||
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()
|
||||
args.putSerializable(VAULT_ARG, vaultModel)
|
||||
args.putSerializable(VAULT_CONFIG_ARG, unverifiedVaultConfig)
|
||||
val fragment = ChangePasswordDialog()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
|
@ -14,6 +14,8 @@ class VaultNotFoundDialog private constructor(private val context: Context) {
|
||||
interface Callback {
|
||||
|
||||
fun onDeleteMissingVaultClicked(vault: Vault)
|
||||
fun onCancelMissingVaultClicked(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)) //
|
||||
.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) } //
|
||||
.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()
|
||||
}
|
||||
|
||||
|
@ -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 java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -89,9 +90,9 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
|
||||
.withExtraText(presenter() //
|
||||
.context() //
|
||||
.getString(R.string.screen_file_browser_add_existing_vault_extra_text)) //
|
||||
.selectingFilesWithNameOnly("masterkey.cryptomator") //
|
||||
.selectingFilesWithNameOnly(Arrays.asList("masterkey.cryptomator", "vault.cryptomator")) //
|
||||
.build()), //
|
||||
SerializableResultCallbacks.masterkeyFileChosen());
|
||||
SerializableResultCallbacks.cryptomatorFileChosen());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -112,7 +113,7 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
|
||||
}
|
||||
|
||||
@Callback
|
||||
void masterkeyFileChosen(SerializableResult<CloudFileModel> result) {
|
||||
void cryptomatorFileChosen(SerializableResult<CloudFileModel> result) {
|
||||
CloudFileModel masterkeyFile = result.getResult();
|
||||
state().masterkeyFile = masterkeyFile.toCloudNode();
|
||||
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_play_services_not_available">Play Services not installed</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_no_such_bucket">No such bucket</string>
|
||||
<string name="error_masterkey_location_not_supported">Custom Masterkey location not supported yet</string>
|
||||
|
||||
<!-- # clouds -->
|
||||
|
||||
|
@ -29,6 +29,10 @@
|
||||
<item name="colorAccent">@color/colorPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="TransparentAlertDialogCustom" parent="AlertDialogCustom">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="Toolbar.Theme" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
<item name="colorAccent">@color/textColorWhite</item>
|
||||
</style>
|
||||
|
@ -13,17 +13,13 @@ import org.cryptomator.domain.usecases.DoUpdateUseCase;
|
||||
import org.cryptomator.domain.usecases.GetDecryptedCloudForVaultUseCase;
|
||||
import org.cryptomator.domain.usecases.ResultHandler;
|
||||
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.GetVaultListUseCase;
|
||||
import org.cryptomator.domain.usecases.vault.LockVaultUseCase;
|
||||
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.SaveVaultUseCase;
|
||||
import org.cryptomator.domain.usecases.vault.UnlockToken;
|
||||
import org.cryptomator.domain.usecases.vault.UnlockVaultUseCase;
|
||||
import org.cryptomator.presentation.exception.ExceptionHandlers;
|
||||
import org.cryptomator.presentation.model.VaultModel;
|
||||
import org.cryptomator.presentation.model.mappers.CloudFolderModelMapper;
|
||||
@ -98,17 +94,12 @@ public class VaultListPresenterTest {
|
||||
private LockVaultUseCase lockVaultUseCase = Mockito.mock(LockVaultUseCase.class);
|
||||
private LockVaultUseCase.Launcher lockVaultUseCaseLauncher = Mockito.mock(LockVaultUseCase.Launcher.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 UnlockVaultUseCase unlockVaultUseCase = Mockito.mock(UnlockVaultUseCase.class);
|
||||
private GetRootFolderUseCase getRootFolderUseCase = Mockito.mock(GetRootFolderUseCase.class);
|
||||
private AddExistingVaultWorkflow addExistingVaultWorkflow = Mockito.mock(AddExistingVaultWorkflow.class);
|
||||
private CreateNewVaultWorkflow createNewVaultWorkflow = Mockito.mock(CreateNewVaultWorkflow.class);
|
||||
private SaveVaultUseCase saveVaultUseCase = Mockito.mock(SaveVaultUseCase.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 DoUpdateCheckUseCase updateCheckUseCase = Mockito.mock(DoUpdateCheckUseCase.class);
|
||||
private DoUpdateUseCase updateUseCase = Mockito.mock(DoUpdateUseCase.class);
|
||||
@ -126,15 +117,11 @@ public class VaultListPresenterTest {
|
||||
renameVaultUseCase, //
|
||||
lockVaultUseCase, //
|
||||
getDecryptedCloudForVaultUseCase, //
|
||||
prepareUnlockUseCase, //
|
||||
unlockVaultUseCase, //
|
||||
getRootFolderUseCase, //
|
||||
addExistingVaultWorkflow, //
|
||||
createNewVaultWorkflow, //
|
||||
saveVaultUseCase, //
|
||||
moveVaultPositionUseCase, //
|
||||
changePasswordUseCase, //
|
||||
removeStoredVaultPasswordsUseCase, //
|
||||
doLicenceCheckUsecase, //
|
||||
updateCheckUseCase, //
|
||||
updateUseCase, //
|
||||
@ -241,43 +228,4 @@ public class VaultListPresenterTest {
|
||||
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"
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -8,14 +8,14 @@ import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
import static java.lang.System.arraycopy;
|
||||
|
||||
class CipherFromApi23 implements Cipher {
|
||||
class CipherImpl implements Cipher {
|
||||
|
||||
private static final int IV_LENGTH = 16;
|
||||
|
||||
private final javax.crypto.Cipher cipher;
|
||||
private final SecretKey key;
|
||||
|
||||
CipherFromApi23(javax.crypto.Cipher cipher, SecretKey key) {
|
||||
CipherImpl(javax.crypto.Cipher cipher, SecretKey key) {
|
||||
this.cipher = cipher;
|
||||
this.key = key;
|
||||
}
|
@ -16,7 +16,7 @@ class CryptoOperationsFactory {
|
||||
}
|
||||
|
||||
private static CryptoOperations createCryptoOperations() {
|
||||
return new CryptoOperationsFromApi23();
|
||||
return new CryptoOperationsImpl();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import java.security.UnrecoverableKeyException;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
class CryptoOperationsFromApi23 implements CryptoOperations {
|
||||
class CryptoOperationsImpl implements CryptoOperations {
|
||||
|
||||
@Override
|
||||
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 + "/" //
|
||||
+ KeyProperties.BLOCK_MODE_CBC + "/" //
|
||||
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
|
||||
return new CipherFromApi23(cipher, key);
|
||||
return new CipherImpl(cipher, key);
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
throw new UnrecoverableStorageKeyException(e);
|
||||
} catch (Exception e) {
|
Loading…
x
Reference in New Issue
Block a user