Merge branch 'feature/vault-format-8' into develop

This commit is contained in:
Julian Raufelder 2021-05-06 15:26:52 +02:00
commit cd986981c2
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
77 changed files with 2016 additions and 1122 deletions

View File

@ -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'

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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()));
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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");
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,6 @@
package org.cryptomator.domain.exception.vaultconfig;
import org.cryptomator.domain.exception.BackendException;
public class VaultKeyInvalidException extends BackendException {
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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
}
}

View File

@ -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" />

View File

@ -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);

View File

@ -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())

View File

@ -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;

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -8,7 +8,5 @@ interface AutoUploadChooseVaultView : View {
fun displayDialogUnableToUploadFiles()
fun displayVaults(vaults: List<VaultModel>)
fun showChosenLocation(location: CloudFolderModel)
fun showEnterPasswordDialog(vaultModel: VaultModel)
fun showBiometricAuthKeyInvalidatedDialog()
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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?)
}

View File

@ -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>)

View File

@ -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

View File

@ -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

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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);

View 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>

View File

@ -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>

View File

@ -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 -->

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -16,7 +16,7 @@ class CryptoOperationsFactory {
}
private static CryptoOperations createCryptoOperations() {
return new CryptoOperationsFromApi23();
return new CryptoOperationsImpl();
}
}

View File

@ -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) {