Add vault format 8 support

This commit is contained in:
Julian Raufelder 2021-04-07 16:46:24 +02:00
parent 6df05fd95b
commit 3fef796546
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
36 changed files with 887 additions and 332 deletions

View File

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

View File

@ -30,10 +30,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public DirType root(CloudType cloud) throws BackendException {
try {
return delegate.root(cloud);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -43,10 +40,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public DirType resolve(CloudType cloud, String path) throws BackendException {
try {
return delegate.resolve(cloud, path);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -56,10 +50,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public FileType file(DirType parent, String name) throws BackendException {
try {
return delegate.file(parent, name);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -69,10 +60,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public FileType file(DirType parent, String name, Optional<Long> size) throws BackendException {
try {
return delegate.file(parent, name, size);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -82,10 +70,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public DirType folder(DirType parent, String name) throws BackendException {
try {
return delegate.folder(parent, name);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -95,10 +80,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public boolean exists(NodeType node) throws BackendException {
try {
return delegate.exists(node);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -108,10 +90,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public List<? extends CloudNode> list(DirType folder) throws BackendException {
try {
return delegate.list(folder);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -121,10 +100,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public DirType create(DirType folder) throws BackendException {
try {
return delegate.create(folder);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -134,10 +110,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public DirType move(DirType source, DirType target) throws BackendException {
try {
return delegate.move(source, target);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -147,10 +120,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public FileType move(FileType source, FileType target) throws BackendException {
try {
return delegate.move(source, target);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -160,10 +130,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public FileType write(FileType file, DataSource data, ProgressAware<UploadState> progressAware, boolean replace, long size) throws BackendException {
try {
return delegate.write(file, data, progressAware, replace, size);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -173,10 +140,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public void read(FileType file, Optional<File> encryptedTmpFile, OutputStream data, ProgressAware<DownloadState> progressAware) throws BackendException {
try {
delegate.read(file, encryptedTmpFile, data, progressAware);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -186,10 +150,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public void delete(NodeType node) throws BackendException {
try {
delegate.delete(node);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -199,10 +160,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public String checkAuthenticationAndRetrieveCurrentAccount(CloudType cloud) throws BackendException {
try {
return delegate.checkAuthenticationAndRetrieveCurrentAccount(cloud);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}
@ -212,10 +170,7 @@ public abstract class InterceptingCloudContentRepository<CloudType extends Cloud
public void logout(CloudType cloud) throws BackendException {
try {
delegate.logout(cloud);
} catch (BackendException e) {
throwWrappedIfRequired(e);
throw e;
} catch (RuntimeException e) {
} catch (BackendException | RuntimeException e) {
throwWrappedIfRequired(e);
throw e;
}

View File

@ -33,16 +33,19 @@ class CryptoCloudContentRepository implements CloudContentRepository<CryptoCloud
throw new FatalBackendException(e);
}
switch (cloud.getVault().getVersion()) {
switch (cloud.getVault().getFormat()) {
case 7:
this.cryptoImpl = new CryptoImplVaultFormat7(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7());
break;
case 8:
this.cryptoImpl = new CryptoImplVaultFormat8(context, cryptor, cloudContentRepository, vaultLocation, new DirIdCacheFormat7(), cloud.getVault().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()));
}
}

View File

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

View File

@ -0,0 +1,27 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.usecases.cloud.Flag;
import org.cryptomator.domain.usecases.vault.UnlockToken;
import org.cryptomator.util.Optional;
public interface CryptoCloudProvider {
void create(CloudFolder location, CharSequence password) throws BackendException;
Vault unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException;
UnlockToken createUnlockToken(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException;
Vault unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException;
boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException;
void lock(Vault vault);
void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException;
}

View File

@ -1,14 +1,26 @@
package org.cryptomator.data.cloud.crypto;
class CryptoConstants {
public class CryptoConstants {
public static final String MASTERKEY_SCHEME = "masterkeyfile";
static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator";
static final String ROOT_DIR_ID = "";
static final String DATA_DIR_NAME = "d";
static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator";
static final String VAULT_FILE_NAME = "vault.cryptomator";
static final String MASTERKEY_BACKUP_FILE_EXT = ".bkup";
static final int MAX_VAULT_VERSION = 7;
static final int DEFAULT_MASTERKEY_FILE_VERSION = 999;
static final int MAX_VAULT_VERSION = 8;
static final int MAX_VAULT_VERSION_WITHOUT_VAULT_CONFIG = 7;
static final int VERSION_WITH_NORMALIZED_PASSWORDS = 6;
static final int MIN_VAULT_VERSION = 5;
static final int DEFAULT_MAX_FILE_NAME = 220;
static final byte[] PEPPER = new byte[0];
static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_CTRMAC;
}

View File

@ -54,18 +54,20 @@ abstract class CryptoImplDecorator {
final CloudContentRepository cloudContentRepository;
final Context context;
final DirIdCache dirIdCache;
final int 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;

View File

@ -51,9 +51,8 @@ import static org.cryptomator.domain.usecases.ProgressAware.NO_OP_PROGRESS_AWARE
import static org.cryptomator.domain.usecases.cloud.Progress.progress;
import static org.cryptomator.util.Encodings.UTF_8;
final class CryptoImplVaultFormat7 extends CryptoImplDecorator {
class CryptoImplVaultFormat7 extends CryptoImplDecorator {
private static final int SHORT_NAMES_MAX_LENGTH = 220;
private static final String CLOUD_NODE_EXT = ".c9r";
private static final String LONG_NODE_FILE_EXT = ".c9s";
private static final String CLOUD_FOLDER_DIR_FILE_PRE = "dir";
@ -65,7 +64,11 @@ final class CryptoImplVaultFormat7 extends CryptoImplDecorator {
private static final BaseEncoding BASE64 = BaseEncoding.base64Url();
CryptoImplVaultFormat7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache) {
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache);
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, CryptoConstants.DEFAULT_MAX_FILE_NAME);
}
CryptoImplVaultFormat7(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int 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;

View File

@ -0,0 +1,16 @@
package org.cryptomator.data.cloud.crypto;
import android.content.Context;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.util.Supplier;
public class CryptoImplVaultFormat8 extends CryptoImplVaultFormat7 {
CryptoImplVaultFormat8(Context context, Supplier<Cryptor> cryptor, CloudContentRepository cloudContentRepository, CloudFolder storageLocation, DirIdCache dirIdCache, int maxFileNameLength) {
super(context, cryptor, cloudContentRepository, storageLocation, dirIdCache, maxFileNameLength);
}
}

View File

@ -36,17 +36,16 @@ import static org.cryptomator.util.Encodings.UTF_8;
final class CryptoImplVaultFormatPre7 extends CryptoImplDecorator {
private static final int SHORT_NAMES_MAX_LENGTH = 129;
static final int 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) {

View File

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

View File

@ -0,0 +1,34 @@
package org.cryptomator.data.cloud.crypto;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.CryptorProvider;
import java.security.SecureRandom;
import java.util.function.Function;
/**
* A combination of different ciphers and/or cipher modes in a Cryptomator vault.
*/
public enum VaultCipherCombo {
/**
* AES-SIV for file name encryption
* AES-CTR + HMAC for content encryption
*/
SIV_CTRMAC(Cryptors::version1),
/**
* AES-SIV for file name encryption
* AES-GCM for content encryption
*/
SIV_GCM(Cryptors::version2);
private final Function<SecureRandom, CryptorProvider> cryptorProvider;
VaultCipherCombo(Function<SecureRandom, CryptorProvider> cryptorProvider) {
this.cryptorProvider = cryptorProvider;
}
public CryptorProvider getCryptorProvider(SecureRandom csprng) {
return cryptorProvider.apply(csprng);
}
}

View File

@ -0,0 +1,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
}
}

View File

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

View File

@ -7,11 +7,13 @@ import org.cryptomator.data.db.mappers.CloudEntityMapper;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudType;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.repository.CloudRepository;
import org.cryptomator.domain.usecases.cloud.Flag;
import org.cryptomator.domain.usecases.vault.UnlockToken;
import org.cryptomator.util.Optional;
import java.util.ArrayList;
import java.util.List;
@ -92,26 +94,30 @@ class CloudRepositoryImpl implements CloudRepository {
return cryptoCloudFactory.decryptedViewOf(vault);
}
public Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException {
return cryptoCloudFactory.unverifiedVaultConfig(vault);
}
@Override
public Cloud unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException {
Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, password, cancelledFlag);
public Cloud unlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
Vault vaultWithVersion = cryptoCloudFactory.unlock(vault, unverifiedVaultConfig, password, cancelledFlag);
return decryptedViewOf(vaultWithVersion);
}
@Override
public Cloud unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException {
Vault vaultWithVersion = cryptoCloudFactory.unlock(token, password, cancelledFlag);
public Cloud unlock(UnlockToken token, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password, Flag cancelledFlag) throws BackendException {
Vault vaultWithVersion = cryptoCloudFactory.unlock(token, unverifiedVaultConfig, password, cancelledFlag);
return decryptedViewOf(vaultWithVersion);
}
@Override
public UnlockToken prepareUnlock(Vault vault) throws BackendException {
return cryptoCloudFactory.createUnlockToken(vault);
public UnlockToken prepareUnlock(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig) throws BackendException {
return cryptoCloudFactory.createUnlockToken(vault, unverifiedVaultConfig);
}
@Override
public boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException {
return cryptoCloudFactory.isVaultPasswordValid(vault, password);
public boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException {
return cryptoCloudFactory.isVaultPasswordValid(vault, unverifiedVaultConfig, password);
}
@Override
@ -121,8 +127,8 @@ class CloudRepositoryImpl implements CloudRepository {
}
@Override
public void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException {
cryptoCloudFactory.changePassword(vault, oldPassword, newPassword);
public void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException {
cryptoCloudFactory.changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
}
}

View File

@ -1,14 +1,10 @@
package org.cryptomator.data.repository;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.domain.repository.CloudContentRepository;
import org.cryptomator.domain.repository.CloudRepository;
import org.cryptomator.domain.repository.UpdateCheckRepository;
import org.cryptomator.domain.repository.VaultRepository;
import java.security.SecureRandom;
import javax.inject.Singleton;
import dagger.Module;
@ -17,12 +13,6 @@ import dagger.Provides;
@Module
public class RepositoryModule {
@Singleton
@Provides
public CryptorProvider provideCryptorProvider() {
return Cryptors.version1(new SecureRandom());
}
@Singleton
@Provides
public CloudRepository provideCloudRepository(CloudRepositoryImpl cloudRepository) {

View File

@ -0,0 +1,6 @@
package org.cryptomator.domain
import java.io.Serializable
import java.net.URI
class UnverifiedVaultConfig(val jwt: String, val keyId: URI, val vaultFormat: Int) : Serializable

View File

@ -12,7 +12,8 @@ public class Vault implements Serializable {
private final CloudType cloudType;
private final boolean unlocked;
private final String password;
private final int version;
private final int format;
private final int 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;
}

View File

@ -0,0 +1,19 @@
package org.cryptomator.domain.exception.vaultconfig;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.exception.BackendException;
import io.jsonwebtoken.JwtException;
public class UnsupportedMasterkeyLocationException extends BackendException {
UnverifiedVaultConfig unverifiedVaultConfig;
public UnsupportedMasterkeyLocationException(UnverifiedVaultConfig unverifiedVaultConfig) {
this.unverifiedVaultConfig = unverifiedVaultConfig;
}
public UnsupportedMasterkeyLocationException(String message, JwtException e) {
super(message, e);
}
}

View File

@ -0,0 +1,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);
}
}

View File

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

View File

@ -0,0 +1,11 @@
package org.cryptomator.domain.exception.vaultconfig;
import org.cryptomator.domain.exception.BackendException;
public class VaultVersionMismatchException extends BackendException {
public VaultVersionMismatchException(String message) {
super(message);
}
}

View File

@ -3,10 +3,12 @@ package org.cryptomator.domain.repository;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.CloudType;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.usecases.cloud.Flag;
import org.cryptomator.domain.usecases.vault.UnlockToken;
import org.cryptomator.util.Optional;
import java.util.List;
@ -24,16 +26,18 @@ public interface CloudRepository {
Cloud decryptedViewOf(Vault vault) throws BackendException;
boolean isVaultPasswordValid(Vault vault, CharSequence password) throws BackendException;
boolean isVaultPasswordValid(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, CharSequence password) throws BackendException;
void lock(Vault vault) throws BackendException;
void changePassword(Vault vault, String oldPassword, String newPassword) throws BackendException;
void changePassword(Vault vault, Optional<UnverifiedVaultConfig> unverifiedVaultConfig, String oldPassword, String newPassword) throws BackendException;
UnlockToken prepareUnlock(Vault vault) throws BackendException;
Optional<UnverifiedVaultConfig> unverifiedVaultConfig(Vault vault) throws BackendException;
Cloud unlock(UnlockToken token, CharSequence password, Flag cancelledFlag) throws BackendException;
UnlockToken prepareUnlock(Vault vault, Optional<UnverifiedVaultConfig> vaultFile) throws BackendException;
Cloud unlock(Vault vault, CharSequence password, Flag cancelledFlag) throws BackendException;
Cloud unlock(UnlockToken token, Optional<UnverifiedVaultConfig> vaultFile, CharSequence password, Flag cancelledFlag) throws BackendException;
Cloud unlock(Vault vault, Optional<UnverifiedVaultConfig> vaultFile, CharSequence password, Flag cancelledFlag) throws BackendException;
}

View File

@ -37,8 +37,10 @@ public class DoLicenseCheck {
try {
final Claims claims = Jwts //
.parserBuilder().setSigningKey(getPublicKey()) //
.build().parseClaimsJws(license) //
.parserBuilder() //
.setSigningKey(getPublicKey()) //
.build() //
.parseClaimsJws(license) //
.getBody();
return claims::getSubject;

View File

@ -1,6 +1,7 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
@ -8,6 +9,7 @@ import org.cryptomator.domain.exception.NoSuchVaultException;
import org.cryptomator.domain.repository.CloudRepository;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;
import org.cryptomator.util.Optional;
import static org.cryptomator.util.ExceptionUtil.contains;
@ -16,23 +18,26 @@ class ChangePassword {
private final CloudRepository cloudRepository;
private final Vault vault;
private final Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
private final String oldPassword;
private final String newPassword;
private final String newPassword;;
public ChangePassword(CloudRepository cloudRepository, //
@Parameter Vault vault, //
@Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig, //
@Parameter String oldPassword, //
@Parameter String newPassword) {
this.cloudRepository = cloudRepository;
this.vault = vault;
this.unverifiedVaultConfig = unverifiedVaultConfig;
this.oldPassword = oldPassword;
this.newPassword = newPassword;
}
public void execute() throws BackendException {
try {
if (cloudRepository.isVaultPasswordValid(vault, oldPassword)) {
cloudRepository.changePassword(vault, oldPassword, newPassword);
if (cloudRepository.isVaultPasswordValid(vault, unverifiedVaultConfig, oldPassword)) {
cloudRepository.changePassword(vault, unverifiedVaultConfig, oldPassword, newPassword);
} else {
throw new InvalidPassphraseException();
}

View File

@ -1,10 +1,12 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.repository.CloudRepository;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;
import org.cryptomator.util.Optional;
@UseCase
class CheckVaultPassword {
@ -12,15 +14,17 @@ class CheckVaultPassword {
private final CloudRepository cloudRepository;
private final Vault vault;
private final String password;
private final Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
public CheckVaultPassword(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter String password) {
public CheckVaultPassword(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter String password, @Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig) {
this.cloudRepository = cloudRepository;
this.vault = vault;
this.password = password;
this.unverifiedVaultConfig = unverifiedVaultConfig;
}
public Boolean execute() throws BackendException {
return cloudRepository.isVaultPasswordValid(vault, password);
return cloudRepository.isVaultPasswordValid(vault, unverifiedVaultConfig, password);
}
}

View File

@ -0,0 +1,36 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
import org.cryptomator.domain.repository.CloudRepository;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;
import org.cryptomator.util.Optional;
import static org.cryptomator.util.ExceptionUtil.contains;
@UseCase
public class GetUnverifiedVaultConfig {
private final CloudRepository cloudRepository;
private final Vault vault;
public GetUnverifiedVaultConfig(CloudRepository cloudRepository, @Parameter Vault vault) {
this.cloudRepository = cloudRepository;
this.vault = vault;
}
public Optional<UnverifiedVaultConfig> execute() throws BackendException {
try {
return cloudRepository.unverifiedVaultConfig(vault);
} catch (BackendException e) {
if (contains(e, NoSuchCloudFileException.class)) {
return Optional.empty();
}
throw e;
}
}
}

View File

@ -1,5 +1,6 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.exception.NoSuchCloudFileException;
@ -7,6 +8,7 @@ import org.cryptomator.domain.exception.NoSuchVaultException;
import org.cryptomator.domain.repository.CloudRepository;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;
import org.cryptomator.util.Optional;
import static org.cryptomator.util.ExceptionUtil.contains;
@ -15,15 +17,17 @@ class PrepareUnlock {
private final CloudRepository cloudRepository;
private final Vault vault;
private final Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
public PrepareUnlock(CloudRepository cloudRepository, @Parameter Vault vault) {
public PrepareUnlock(CloudRepository cloudRepository, @Parameter Vault vault, @Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig) {
this.cloudRepository = cloudRepository;
this.vault = vault;
this.unverifiedVaultConfig = unverifiedVaultConfig;
}
public UnlockToken execute() throws BackendException {
try {
return cloudRepository.prepareUnlock(vault);
return cloudRepository.prepareUnlock(vault, unverifiedVaultConfig);
} catch (BackendException e) {
if (contains(e, NoSuchCloudFileException.class)) {
throw new NoSuchVaultException(vault, e);

View File

@ -1,17 +1,20 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.Cloud;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.repository.CloudRepository;
import org.cryptomator.domain.usecases.cloud.Flag;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;
import org.cryptomator.util.Optional;
@UseCase
class UnlockVault {
class UnlockVaultUsingMasterkey {
private final CloudRepository cloudRepository;
private final VaultOrUnlockToken vaultOrUnlockToken;
private Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
private final String password;
private volatile boolean cancelled;
@ -22,9 +25,10 @@ class UnlockVault {
}
};
public UnlockVault(CloudRepository cloudRepository, @Parameter VaultOrUnlockToken vaultOrUnlockToken, @Parameter String password) {
public UnlockVaultUsingMasterkey(CloudRepository cloudRepository, @Parameter VaultOrUnlockToken vaultOrUnlockToken, @Parameter Optional<UnverifiedVaultConfig> unverifiedVaultConfig, @Parameter String password) {
this.cloudRepository = cloudRepository;
this.vaultOrUnlockToken = vaultOrUnlockToken;
this.unverifiedVaultConfig = unverifiedVaultConfig;
this.password = password;
}
@ -34,9 +38,9 @@ class UnlockVault {
public Cloud execute() throws BackendException {
if (vaultOrUnlockToken.getVault().isPresent()) {
return cloudRepository.unlock(vaultOrUnlockToken.getVault().get(), password, cancelledFlag);
return cloudRepository.unlock(vaultOrUnlockToken.getVault().get(), unverifiedVaultConfig, password, cancelledFlag);
} else {
return cloudRepository.unlock(vaultOrUnlockToken.getUnlockToken().get(), password, cancelledFlag);
return cloudRepository.unlock(vaultOrUnlockToken.getUnlockToken().get(), unverifiedVaultConfig, password, cancelledFlag);
}
}

View File

@ -1,15 +1,17 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.UnverifiedVaultConfig;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.repository.CloudRepository;
import org.cryptomator.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.verify;
public class UnlockVaultTest {
public class UnlockVaultUsingMasterkeyTest {
private static final String A_STRING = "89dfhsjdhfjsd";
@ -19,30 +21,33 @@ public class UnlockVaultTest {
private CloudRepository cloudRepository;
private UnlockVault inTest;
private UnlockVaultUsingMasterkey inTest;
private Optional<UnverifiedVaultConfig> unverifiedVaultConfig;
@BeforeEach
public void setup() {
unlockToken = Mockito.mock(UnlockToken.class);
vault = Mockito.mock(Vault.class);
cloudRepository = Mockito.mock(CloudRepository.class);
inTest = Mockito.mock(UnlockVault.class);
unverifiedVaultConfig = Mockito.mock(Optional.class);
inTest = Mockito.mock(UnlockVaultUsingMasterkey.class);
}
@Test
public void testExecuteDelegatesToUnlockWhenInvokedWithVault() throws BackendException {
inTest = new UnlockVault(cloudRepository, VaultOrUnlockToken.from(vault), A_STRING);
inTest = new UnlockVaultUsingMasterkey(cloudRepository, VaultOrUnlockToken.from(vault), unverifiedVaultConfig, A_STRING);
inTest.execute();
verify(cloudRepository).unlock(Mockito.eq(vault), Mockito.eq(A_STRING), Mockito.any());
verify(cloudRepository).unlock(Mockito.eq(vault), Mockito.any(), Mockito.eq(A_STRING), Mockito.any());
}
@Test
public void testExecuteDelegatesToUnlockWhenInvokedWithUnlockToken() throws BackendException {
inTest = new UnlockVault(cloudRepository, VaultOrUnlockToken.from(unlockToken), A_STRING);
inTest = new UnlockVaultUsingMasterkey(cloudRepository, VaultOrUnlockToken.from(unlockToken), unverifiedVaultConfig, A_STRING);
inTest.execute();
verify(cloudRepository).unlock(Mockito.eq(unlockToken), Mockito.eq(A_STRING), Mockito.any());
verify(cloudRepository).unlock(Mockito.eq(unlockToken), Mockito.any(), Mockito.eq(A_STRING), Mockito.any());
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import org.cryptomator.domain.UnverifiedVaultConfig
import org.cryptomator.generator.Dialog
import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.VaultModel
@ -23,7 +24,7 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
interface Callback {
fun onChangePasswordClick(vaultModel: VaultModel, oldPassword: String, newPassword: String)
fun onChangePasswordClick(vaultModel: VaultModel, unverifiedVaultConfig: UnverifiedVaultConfig?, oldPassword: String, newPassword: String)
}
override fun onStart() {
@ -33,10 +34,12 @@ class ChangePasswordDialog : BaseProgressErrorDialog<ChangePasswordDialog.Callba
changePasswordButton = dialog.getButton(android.app.Dialog.BUTTON_POSITIVE)
changePasswordButton?.setOnClickListener {
val vaultModel = requireArguments().getSerializable(VAULT_ARG) as VaultModel
val unverifiedVaultConfig = requireArguments().getSerializable(VAULT_CONFIG_ARG) as UnverifiedVaultConfig?
if (valid(et_old_password.text.toString(), //
et_new_password.text.toString(), //
et_new_retype_password.text.toString())) {
callback?.onChangePasswordClick(vaultModel, //
unverifiedVaultConfig, //
et_old_password.text.toString(), //
et_new_password.text.toString())
onWaitForResponse(et_old_password)
@ -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

View File

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