Skip to content

Commit

Permalink
Add support for Cryptomator vault format 8.
Browse files Browse the repository at this point in the history
  • Loading branch information
ylangisc committed Nov 30, 2021
1 parent 5041343 commit 20d1a7f
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 112 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml → .github/workflows/build.yml
Expand Up @@ -20,8 +20,8 @@ jobs:
- name: Set up JDK 15
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 15
distribution: 'temurin'
java-version: 17
- name: Cache local Maven repository
uses: actions/cache@v2
with:
Expand Down
Expand Up @@ -1133,7 +1133,7 @@ Integrated Windows Authentication (IWA)
this.setDefault("threading.pool.keepalive.seconds", String.valueOf(60L));

this.setDefault("cryptomator.enable", String.valueOf(true));
this.setDefault("cryptomator.vault.version", String.valueOf(7));
this.setDefault("cryptomator.vault.version", String.valueOf(8));
this.setDefault("cryptomator.vault.autodetect", String.valueOf(true));
this.setDefault("cryptomator.vault.masterkey.filename", "masterkey.cryptomator");
this.setDefault("cryptomator.vault.pepper", "");
Expand Down
2 changes: 1 addition & 1 deletion cryptomator/pom.xml
Expand Up @@ -63,7 +63,7 @@
<dependency>
<groupId>ch.iterate.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>1.3.0</version>
<version>2.0.3</version>
</dependency>
</dependencies>
</project>
Expand Up @@ -27,6 +27,8 @@

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;

public class ContentReader {
Expand All @@ -46,4 +48,9 @@ public String read(final Path file) throws BackgroundException {
throw new DefaultIOExceptionMappingService().map(e);
}
}

public Reader getReader(final Path file) throws BackgroundException {
final Read read = session._getFeature(Read.class);
return new InputStreamReader(read.read(file, new TransferStatus().withLength(file.attributes().getSize()), new DisabledConnectionCallback()), StandardCharsets.UTF_8);
}
}
Expand Up @@ -86,7 +86,9 @@ public void write(final byte[] b, final int off, final int len) throws IOExcepti
final ByteBuffer encryptedChunk = cryptor.encryptChunk(
ByteBuffer.wrap(Arrays.copyOfRange(b, chunkOffset, chunkOffset + chunkLen)),
chunkIndexOffset++, header, nonces.next());
super.write(encryptedChunk.array());
final byte[] encrypted = new byte[encryptedChunk.remaining()];
encryptedChunk.get(encrypted);
super.write(encrypted);
}
}
catch(CryptoException e) {
Expand Down
Expand Up @@ -39,27 +39,31 @@
import ch.cyberduck.core.exception.LocalAccessDeniedException;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.core.features.*;
import ch.cyberduck.core.preferences.HostPreferences;
import ch.cyberduck.core.preferences.Preferences;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.shared.DefaultTouchFeature;
import ch.cyberduck.core.shared.DefaultUrlProvider;
import ch.cyberduck.core.transfer.TransferStatus;
import ch.cyberduck.core.unicode.NFCNormalizer;
import ch.cyberduck.core.vault.DefaultVaultRegistry;
import ch.cyberduck.core.vault.VaultCredentials;
import ch.cyberduck.core.vault.VaultException;

import org.apache.log4j.Logger;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.FileContentCryptor;
import org.cryptomator.cryptolib.api.FileHeaderCryptor;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.KeyFile;

import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.MasterkeyFile;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.EnumSet;
Expand Down Expand Up @@ -123,7 +127,6 @@ public CryptoVault(final Path home, final String masterkey, final byte[] pepper)
}

public synchronized Path create(final Session<?> session, final String region, final VaultCredentials credentials, final PasswordStore keychain, final int version) throws BackgroundException {
final CryptorProvider provider = Cryptors.version1(FastSecureRandomProvider.get().provide());
final Host bookmark = session.getHost();
if(credentials.isSaved()) {
try {
Expand All @@ -135,7 +138,17 @@ public synchronized Path create(final Session<?> session, final String region, f
}
}
final String passphrase = credentials.getPassword();
final KeyFile masterKeyFileContent = provider.createNew().writeKeysToMasterkeyFile(passphrase, pepper, version);
final ByteArrayOutputStream mkArray = new ByteArrayOutputStream();
final Masterkey mk = Masterkey.generate(FastSecureRandomProvider.get().provide());
final MasterkeyFileAccess access = new MasterkeyFileAccess(pepper, FastSecureRandomProvider.get().provide());
final MasterkeyFile masterkeyFile;
try {
access.persist(mk, mkArray, passphrase, version);
masterkeyFile = MasterkeyFile.read(new StringReader(new String(mkArray.toByteArray(), StandardCharsets.UTF_8)));
}
catch(IOException e) {
throw new VaultException("Failure creating master key", e);
}
if(log.isDebugEnabled()) {
log.debug(String.format("Write master key to %s", masterkey));
}
Expand All @@ -147,8 +160,8 @@ public synchronized Path create(final Session<?> session, final String region, f
status.setEncryption(encryption.getDefault(home));
}
final Path vault = directory.mkdir(home, status);
new ContentWriter(session).write(masterkey, masterKeyFileContent.serialize());
this.open(KeyFile.parse(masterKeyFileContent.serialize()), passphrase);
new ContentWriter(session).write(masterkey, mkArray.toByteArray());
this.open(masterkeyFile, passphrase);
final Path secondLevel = directoryProvider.toEncrypted(session, home.attributes().getDirectoryId(), home);
final Path firstLevel = secondLevel.getParent();
final Path dataDir = firstLevel.getParent();
Expand All @@ -175,17 +188,9 @@ public synchronized CryptoVault load(final Session<?> session, final PasswordCal
if(log.isDebugEnabled()) {
log.debug(String.format("Attempt to read master key from %s", masterkey));
}
final String json = new ContentReader(session).read(masterkey);
if(log.isDebugEnabled()) {
log.debug(String.format("Read master key %s", masterkey));
}
final KeyFile masterKeyFileContent;
try {
masterKeyFileContent = KeyFile.parse(json.getBytes());
}
catch(JsonParseException | IllegalArgumentException | IllegalStateException e) {
throw new VaultException(String.format("Failure reading vault master key file %s", masterkey.getName()), e);
}
final Host bookmark = session.getHost();
String passphrase = keychain.getPassword(String.format("Cryptomator Passphrase (%s)", bookmark.getCredentials().getUsername()),
new DefaultUrlProvider(bookmark).toUrl(masterkey).find(DescriptiveUrl.Type.provider).getUrl());
Expand All @@ -194,14 +199,21 @@ public synchronized CryptoVault load(final Session<?> session, final PasswordCal
passphrase = keychain.getPassword(String.format("Cryptomator Passphrase %s", bookmark.getHostname()),
new DefaultUrlProvider(bookmark).toUrl(masterkey).find(DescriptiveUrl.Type.provider).getUrl());
}
this.unlock(session, masterkey, masterKeyFileContent, passphrase, bookmark, prompt,
final MasterkeyFile mkFile;
try {
mkFile = MasterkeyFile.read(new ContentReader(session).getReader(masterkey));
}
catch(JsonParseException | IllegalArgumentException | IllegalStateException | IOException e) {
throw new VaultException(String.format("Failure reading vault master key file %s", masterkey.getName()), e);
}
this.unlock(session, mkFile, passphrase, bookmark, prompt,
MessageFormat.format(LocaleFactory.localizedString("Provide your passphrase to unlock the Cryptomator Vault {0}", "Cryptomator"), home.getName()),
keychain);
return this;
}

private void unlock(final Session<?> session, final Path masterKeyFile, final KeyFile masterKeyFileContent,
final String passphrase, final Host bookmark, final PasswordCallback prompt, final String message, final PasswordStore keychain) throws BackgroundException {
private void unlock(final Session<?> session, final MasterkeyFile mkFile, final String passphrase, final Host bookmark, final PasswordCallback prompt,
final String message, final PasswordStore keychain) throws BackgroundException {
final Credentials credentials;
if(null == passphrase) {
credentials = prompt.prompt(
Expand All @@ -220,23 +232,19 @@ private void unlock(final Session<?> session, final Path masterKeyFile, final Ke
credentials = new VaultCredentials(passphrase).withSaved(preferences.getBoolean("vault.keychain"));
}
try {
this.open(this.upgrade(session, masterKeyFileContent, credentials.getPassword()), credentials.getPassword());
this.open(mkFile, credentials.getPassword());
if(credentials.isSaved()) {
if(log.isInfoEnabled()) {
log.info(String.format("Save passphrase for %s", masterKeyFile));
log.info(String.format("Save passphrase for %s", masterkey));
}
// Save password with hostname and path to masterkey.cryptomator in keychain
keychain.addPassword(String.format("Cryptomator Passphrase (%s)", bookmark.getCredentials().getUsername()),
new DefaultUrlProvider(bookmark).toUrl(masterKeyFile).find(DescriptiveUrl.Type.provider).getUrl(), credentials.getPassword());
// Save masterkey.cryptomator content in preferences
preferences.setProperty(new DefaultUrlProvider(bookmark).toUrl(masterKeyFile).find(DescriptiveUrl.Type.provider).getUrl(),
new String(masterKeyFileContent.serialize()));
new DefaultUrlProvider(bookmark).toUrl(masterkey).find(DescriptiveUrl.Type.provider).getUrl(), credentials.getPassword());
}
}
catch(CryptoAuthenticationException e) {
this.unlock(session, masterKeyFile, masterKeyFileContent, null, bookmark,
prompt, String.format("%s %s.", e.getDetail(),
MessageFormat.format(LocaleFactory.localizedString("Provide your passphrase to unlock the Cryptomator Vault {0}", "Cryptomator"), home.getName())), keychain);
this.unlock(session, mkFile, null, bookmark, prompt, String.format("%s %s.", e.getDetail(),
MessageFormat.format(LocaleFactory.localizedString("Provide your passphrase to unlock the Cryptomator Vault {0}", "Cryptomator"), home.getName())), keychain);
}
}

Expand All @@ -260,75 +268,45 @@ public synchronized void close() {
fileNameCryptor = null;
}

private KeyFile upgrade(final Session<?> session, final KeyFile keyFile, final CharSequence passphrase) throws BackgroundException {
int version = keyFile.getVersion();
if(version == VAULT_VERSION || version == VAULT_VERSION_DEPRECATED) {
return keyFile;
}
else if(version == 5) {
log.warn(String.format("Upgrade vault version %d to %d", keyFile.getVersion(), VAULT_VERSION));
try {
final CryptorProvider provider = Cryptors.version1(FastSecureRandomProvider.get().provide());
final Cryptor cryptor = provider.createFromKeyFile(keyFile, passphrase, pepper, keyFile.getVersion());
// Create backup, as soon as we know the password was correct
final Path masterKeyFileBackup = new Path(home,
new HostPreferences(session.getHost()).getProperty("cryptomator.vault.masterkey.filename"), EnumSet.of(Path.Type.file, Path.Type.vault));
new ContentWriter(session).write(masterKeyFileBackup, keyFile.serialize());
if(log.isInfoEnabled()) {
log.info(String.format("Master key backup saved in %s", masterKeyFileBackup));
}
// Write updated masterkey file
final KeyFile upgradedMasterKeyFile = cryptor.writeKeysToMasterkeyFile(passphrase, pepper, VAULT_VERSION_DEPRECATED);
final Path masterKeyFile = new Path(home,
new HostPreferences(session.getHost()).getProperty("cryptomator.vault.masterkey.filename"), EnumSet.of(Path.Type.file, Path.Type.vault));
final byte[] masterKeyFileContent = upgradedMasterKeyFile.serialize();
new ContentWriter(session).write(masterKeyFile, masterKeyFileContent, new TransferStatus().exists(true).withLength(masterKeyFileContent.length));
log.warn(String.format("Updated masterkey %s to version %d", masterKeyFile, VAULT_VERSION_DEPRECATED));
return KeyFile.parse(upgradedMasterKeyFile.serialize());
}
catch(IllegalArgumentException e) {
throw new VaultException("Failure reading key file", e);
}
catch(InvalidPassphraseException e) {
throw new CryptoAuthenticationException("Failure to decrypt master key file", e);
}
}
log.error(String.format("Unsupported vault version %d", keyFile.getVersion()));
return keyFile;
}

protected void open(final KeyFile keyFile, final CharSequence passphrase) throws VaultException, CryptoAuthenticationException {
switch(keyFile.getVersion()) {
private void open(final MasterkeyFile mkFile, final CharSequence passphrase) throws VaultException, CryptoAuthenticationException {
switch(mkFile.version) {
case VAULT_VERSION_DEPRECATED:
this.open(keyFile, passphrase, new CryptoFilenameV6Provider(vault), new CryptoDirectoryV6Provider(vault, this));
this.open(mkFile, passphrase, new CryptoFilenameV6Provider(vault), new CryptoDirectoryV6Provider(vault, this));
break;
default:
this.open(keyFile, passphrase, new CryptoFilenameV7Provider(), new CryptoDirectoryV7Provider(vault, this));
this.open(mkFile, passphrase, new CryptoFilenameV7Provider(), new CryptoDirectoryV7Provider(vault, this));
break;
}
}

protected void open(final KeyFile keyFile, final CharSequence passphrase,
final CryptoFilename filenameProvider, final CryptoDirectory directoryProvider) throws VaultException, CryptoAuthenticationException {
this.vaultVersion = keyFile.getVersion();
final CryptorProvider provider = Cryptors.version1(FastSecureRandomProvider.get().provide());
private void open(final MasterkeyFile mkFile, final CharSequence passphrase, final CryptoFilename filenameProvider,
final CryptoDirectory directoryProvider) throws VaultException, CryptoAuthenticationException {
this.vaultVersion = mkFile.version;
final CryptorProvider provider = CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC);
if(log.isDebugEnabled()) {
log.debug(String.format("Initialized crypto provider %s", provider));
}
try {
this.cryptor = provider.createFromKeyFile(keyFile, new NFCNormalizer().normalize(passphrase), pepper, keyFile.getVersion());
this.cryptor = provider.provide(this.getMasterKey(mkFile, passphrase), FastSecureRandomProvider.get().provide());
this.fileNameCryptor = new CryptorCache(cryptor.fileNameCryptor());
this.filenameProvider = filenameProvider;
this.directoryProvider = directoryProvider;
}
catch(IllegalArgumentException e) {
catch(IllegalArgumentException | IOException e) {
throw new VaultException("Failure reading key file", e);
}
catch(InvalidPassphraseException e) {
throw new CryptoAuthenticationException("Failure to decrypt master key file", e);
}
}

private Masterkey getMasterKey(final MasterkeyFile mkFile, final CharSequence passphrase) throws IOException {
final StringWriter writer = new StringWriter();
mkFile.write(writer);
return new MasterkeyFileAccess(pepper, FastSecureRandomProvider.get().provide()).load(
new ByteArrayInputStream(writer.getBuffer().toString().getBytes(StandardCharsets.UTF_8)), passphrase);
}

public synchronized boolean isUnlocked() {
return cryptor != null;
}
Expand Down Expand Up @@ -495,7 +473,7 @@ public long toCiphertextSize(final long cleartextFileOffset, final long cleartex
else {
headerSize = 0;
}
return headerSize + Cryptors.ciphertextSize(cleartextFileSize, cryptor);
return headerSize + cryptor.fileContentCryptor().ciphertextSize(cleartextFileSize);
}

@Override
Expand All @@ -511,7 +489,7 @@ public long toCleartextSize(final long cleartextFileOffset, final long ciphertex
headerSize = 0;
}
try {
return Cryptors.cleartextSize(ciphertextFileSize - headerSize, cryptor);
return cryptor.fileContentCryptor().cleartextSize(ciphertextFileSize - headerSize);
}
catch(AssertionError e) {
throw new CryptoInvalidFilesizeException(String.format("Encrypted file size must be at least %d bytes", headerSize));
Expand Down

0 comments on commit 20d1a7f

Please sign in to comment.