/******************************************************************************* * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). * All rights reserved. This program and the accompanying materials * are made available under the terms of the accompanying LICENSE file. *******************************************************************************/ package org.cryptomator.ui.model; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; 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.ui.l10n.Localization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class UpgradeStrategy { private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class); private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup"; protected final CryptorProvider cryptorProvider; protected final Localization localization; protected final int vaultVersionBeforeUpgrade; protected final int vaultVersionAfterUpgrade; UpgradeStrategy(CryptorProvider cryptorProvider, Localization localization, int vaultVersionBeforeUpgrade, int vaultVersionAfterUpgrade) { this.cryptorProvider = cryptorProvider; this.localization = localization; this.vaultVersionBeforeUpgrade = vaultVersionBeforeUpgrade; this.vaultVersionAfterUpgrade = vaultVersionAfterUpgrade; } static SecureRandom strongSecureRandom() { try { return SecureRandom.getInstanceStrong(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e); } } /** * @return Localized title string to display to the user when an upgrade is needed. */ public abstract String getTitle(Vault vault); /** * @return Localized message string to display to the user when an upgrade is needed. */ public abstract String getMessage(Vault vault); /** * Upgrades a vault. Might take a moment, should be run in a background thread. */ public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException { LOG.info("Upgrading {} from {} to {}.", vault.getPath(), vaultVersionBeforeUpgrade, vaultVersionAfterUpgrade); Cryptor cryptor = null; try { final Path masterkeyFile = vault.getPath().resolve(MASTERKEY_FILENAME); final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile); cryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(masterkeyFileContents), passphrase, vaultVersionBeforeUpgrade); // create backup, as soon as we know the password was correct: final Path masterkeyBackupFile = vault.getPath().resolve(MASTERKEY_BACKUP_FILENAME); Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING); LOG.info("Backuped masterkey."); // do stuff: upgrade(vault, cryptor); // write updated masterkey file: final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase, vaultVersionAfterUpgrade).serialize(); final Path masterkeyFileAfterUpgrade = vault.getPath().resolve(MASTERKEY_FILENAME); // path may have changed Files.write(masterkeyFileAfterUpgrade, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING); LOG.info("Updated masterkey."); } catch (InvalidPassphraseException e) { throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword")); } catch (UnsupportedVaultFormatException e) { if (e.getDetectedVersion() == Integer.MAX_VALUE) { LOG.warn("Version MAC authentication error in vault {}", vault.getPath()); throw new UpgradeFailedException(localization.getString("unlock.errorMessage.unauthenticVersionMac")); } else { LOG.warn("Upgrade failed.", e); throw new UpgradeFailedException("Upgrade failed. Details in log message."); } } catch (IOException e) { LOG.warn("Upgrade failed.", e); throw new UpgradeFailedException("Upgrade failed. Details in log message."); } finally { if (cryptor != null) { cryptor.destroy(); } } } protected abstract void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException; /** * Determines in O(1), if an upgrade can be applied to a vault. * * @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses. */ public boolean isApplicable(Vault vault) { final Path masterkeyFile = vault.getPath().resolve(MASTERKEY_FILENAME); try { if (Files.isRegularFile(masterkeyFile)) { byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile); return KeyFile.parse(masterkeyFileContents).getVersion() == vaultVersionBeforeUpgrade; } else { LOG.warn("Not a file: {}", masterkeyFile); return false; } } catch (IOException e) { LOG.warn("Could not determine, whether upgrade is applicable.", e); return false; } } /** * Thrown when data migration failed. */ public static class UpgradeFailedException extends Exception { UpgradeFailedException() { } UpgradeFailedException(String message) { super(message); } } }