/*******************************************************************************
* 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.ByteBuffer;
import java.nio.channels.FileChannel;
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.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileHeader;
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 UpgradeVersion4to5 extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion4to5.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_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}");
@Inject
public UpgradeVersion4to5(Localization localization) {
super(Cryptors.version1(UpgradeStrategy.strongSecureRandom()), localization, 4, 5);
}
@Override
public String getTitle(Vault vault) {
return localization.getString("upgrade.version4to5.title");
}
@Override
public String getMessage(Vault vault) {
return localization.getString("upgrade.version4to5.msg");
}
@Override
protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path dataDir = vault.getPath().resolve("d");
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 {
if (attrs.isRegularFile() && BASE32_PATTERN.matcher(file.getFileName().toString()).find() && attrs.size() > cryptor.fileHeaderCryptor().headerSize()) {
migrate(file, attrs, cryptor);
} else {
LOG.info("Skipping irrelevant file {}.", file);
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
LOG.error("Migration failed.", e);
throw new UpgradeFailedException(localization.getString("upgrade.version4to5.err.io"));
}
LOG.info("Migration finished.");
}
@SuppressWarnings("deprecation")
private void migrate(Path file, BasicFileAttributes attrs, Cryptor cryptor) throws IOException {
LOG.info("Starting migration of {}...", file);
try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
// read header:
ByteBuffer headerBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize());
ch.read(headerBuf);
headerBuf.flip();
LOG.info("\tHeader read");
FileHeader header = cryptor.fileHeaderCryptor().decryptHeader(headerBuf);
long cleartextSize = header.getFilesize();
if (cleartextSize < 0) {
LOG.info("\tSkipping already migrated file");
return;
} else if (cleartextSize > attrs.size()) {
LOG.warn("\tSkipping file with invalid file size {}/{}", cleartextSize, attrs.size());
return;
}
int headerSize = cryptor.fileHeaderCryptor().headerSize();
int ciphertextChunkSize = cryptor.fileContentCryptor().ciphertextChunkSize();
int cleartextChunkSize = cryptor.fileContentCryptor().cleartextChunkSize();
long newCiphertextSize = Cryptors.ciphertextSize(cleartextSize, cryptor);
long newEOF = headerSize + newCiphertextSize;
long newFullChunks = newCiphertextSize / ciphertextChunkSize; // int-truncation
long newAdditionalCiphertextBytes = newCiphertextSize % ciphertextChunkSize;
if (newAdditionalCiphertextBytes == 0) {
// (new) last block is already correct. just truncate:
LOG.info("\tMigrating cleartext size {}: Truncating to new ciphertext size: {}", cleartextSize, newEOF);
ch.truncate(newEOF);
LOG.info("\tFile truncated");
} else {
// last block may contain padding and needs to be re-encrypted:
long lastChunkIdx = newFullChunks;
LOG.info("\tMigrating cleartext size {}: Re-encrypting chunk {}. New ciphertext size: {}", cleartextSize, lastChunkIdx, newEOF);
long beginOfLastChunk = headerSize + lastChunkIdx * ciphertextChunkSize;
assert beginOfLastChunk < newEOF;
int lastCleartextChunkLength = (int) (cleartextSize % cleartextChunkSize);
assert lastCleartextChunkLength < cleartextChunkSize;
assert lastCleartextChunkLength > 0;
ch.position(beginOfLastChunk);
ByteBuffer lastCiphertextChunk = ByteBuffer.allocate(ciphertextChunkSize);
int read = ch.read(lastCiphertextChunk);
if (read != -1) {
lastCiphertextChunk.flip();
ByteBuffer lastCleartextChunk = cryptor.fileContentCryptor().decryptChunk(lastCiphertextChunk, lastChunkIdx, header, true);
lastCleartextChunk.position(0).limit(lastCleartextChunkLength);
assert lastCleartextChunk.remaining() == lastCleartextChunkLength;
ByteBuffer newLastChunkCiphertext = cryptor.fileContentCryptor().encryptChunk(lastCleartextChunk, lastChunkIdx, header);
ch.truncate(beginOfLastChunk);
ch.write(newLastChunkCiphertext);
} else {
LOG.error("\tReached EOF at position {}/{}", beginOfLastChunk, newEOF);
return; // must exit method before changing header!
}
LOG.info("\tReencrypted last block");
}
header.setFilesize(-1l);
ByteBuffer newHeaderBuf = cryptor.fileHeaderCryptor().encryptHeader(header);
ch.position(0);
ch.write(newHeaderBuf);
LOG.info("\tUpdated header");
}
LOG.info("Finished migration of {}.", file);
}
}