/*******************************************************************************
* 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 static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.util.EnumSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.ui.l10n.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Contains the collective knowledge of all creatures who were alive during the development of vault format 3.
* This class uses no external classes from the crypto or shortening layer by purpose, so we don't need legacy code inside these.
*/
@Singleton
class UpgradeVersion3to4 extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3to4.class);
private static final Pattern LVL1_DIR_PATTERN = Pattern.compile("[A-Z2-7]{2}");
private static final Pattern LVL2_DIR_PATTERN = Pattern.compile("[A-Z2-7]{30}");
private static final Pattern BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN = Pattern.compile("^(([A-Z2-7]{8})*[A-Z2-7=]{8})_");
private static final int FILE_MIN_SIZE = 88; // vault version 3 files have a header of 88 bytes (assuming no chunks at all)
private static final String LONG_FILENAME_SUFFIX = ".lng";
private static final String OLD_FOLDER_SUFFIX = "_";
private static final String NEW_FOLDER_PREFIX = "0";
private final MessageDigest sha1 = MessageDigestSupplier.SHA1.get();
private final BaseNCodec base32 = new Base32();
@Inject
public UpgradeVersion3to4(Localization localization) {
super(Cryptors.version1(UpgradeStrategy.strongSecureRandom()), localization, 3, 4);
}
@Override
public String getTitle(Vault vault) {
return localization.getString("upgrade.version3to4.title");
}
@Override
public String getMessage(Vault vault) {
return localization.getString("upgrade.version3to4.msg");
}
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path dataDir = vault.getPath().resolve("d");
Path metadataDir = vault.getPath().resolve("m");
if (!Files.isDirectory(dataDir)) {
return; // empty vault. no migration needed.
}
try {
Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (dir.equals(dataDir)) {
// path/to/vault/d
return FileVisitResult.CONTINUE;
} else if (dir.getParent().equals(dataDir) && LVL1_DIR_PATTERN.matcher(dir.getFileName().toString()).matches()) {
// path/to/vault/d/AB
return FileVisitResult.CONTINUE;
} else if (dir.getParent().getParent().equals(dataDir) && LVL2_DIR_PATTERN.matcher(dir.getFileName().toString()).matches()) {
// path/to/vault/d/AB/CDEFGHIJKLMNOPQRSTUVWXYZ234567
return FileVisitResult.CONTINUE;
} else {
LOG.info("Skipping irrelevant directory {}", dir);
return FileVisitResult.SKIP_SUBTREE;
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String name = file.getFileName().toString();
if (name.endsWith(LONG_FILENAME_SUFFIX)) {
migrateLong(metadataDir, file);
} else {
migrate(file, attrs);
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
LOG.error("Migration failed.", e);
throw new UpgradeFailedException(localization.getString("upgrade.version3to4.err.io"));
}
LOG.info("Migration finished.");
}
private void migrate(Path file, BasicFileAttributes attrs) throws IOException {
String name = file.getFileName().toString();
long size = attrs.size();
Matcher m = BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN.matcher(name);
if (attrs.isRegularFile() && m.find(0) && size < FILE_MIN_SIZE) {
String base32 = m.group(1);
String suffix = name.substring(m.end());
String renamed = NEW_FOLDER_PREFIX + base32 + (suffix.isEmpty() ? "" : " " + suffix);
renameWithoutOverwriting(file, renamed);
}
}
private void renameWithoutOverwriting(Path path, String newName) throws IOException {
Path newPath = path.resolveSibling(newName);
for (int i = 2; Files.exists(newPath); i++) {
newPath = path.resolveSibling(newName + " " + i);
}
Files.move(path, newPath);
LOG.info("Renaming {} to {}", path, newPath.getFileName());
}
private void migrateLong(Path metadataDir, Path path) throws IOException {
String oldName = path.getFileName().toString();
Path oldMetadataFile = metadataDir.resolve(oldName.substring(0, 2)).resolve(oldName.substring(2, 4)).resolve(oldName);
if (Files.isRegularFile(oldMetadataFile)) {
String oldContent = new String(Files.readAllBytes(oldMetadataFile), UTF_8);
if (oldContent.endsWith(OLD_FOLDER_SUFFIX)) {
String newContent = NEW_FOLDER_PREFIX + StringUtils.removeEnd(oldContent, OLD_FOLDER_SUFFIX);
String newName = base32.encodeAsString(sha1.digest(newContent.getBytes(UTF_8))) + LONG_FILENAME_SUFFIX;
Path newPath = path.resolveSibling(newName);
Path newMetadataFile = metadataDir.resolve(newName.substring(0, 2)).resolve(newName.substring(2, 4)).resolve(newName);
Files.move(path, newPath);
Files.createDirectories(newMetadataFile.getParent());
Files.write(newMetadataFile, newContent.getBytes(UTF_8));
Files.delete(oldMetadataFile);
LOG.info("Renaming {} to {}\nDeleting {}\nCreating {}", path, newName, oldMetadataFile, newMetadataFile);
}
} else {
LOG.warn("Found uninflatable long file name. Expected: {}", oldMetadataFile);
}
}
}