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