Skip to content

Commit

Permalink
introduce "unlock" subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
infeo committed Oct 18, 2024
1 parent 1e246c2 commit 080c17e
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 103 deletions.
109 changes: 9 additions & 100 deletions src/main/java/org/cryptomator/cli/CryptomatorCli.java
Original file line number Diff line number Diff line change
@@ -1,117 +1,26 @@
package org.cryptomator.cli;

import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import picocli.CommandLine.*;

import java.io.IOException;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.concurrent.Callable;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.RunLast;

@Command(name = "cryptomator-cli",
mixinStandardHelpOptions = true,
version = "1.0.0",
description = "Unlocks a cryptomator vault and mounts it into the system.")
public class CryptomatorCli implements Callable<Integer> {
description = "Unlocks a cryptomator vault and mounts it into the system.",
subcommands = Unlock.class)
public class CryptomatorCli {

private static final Logger LOG = LoggerFactory.getLogger(CryptomatorCli.class);
private static final byte[] PEPPER = new byte[0];
private static final String CONFIG_FILE_NAME = "vault.cryptomator";

@Spec
Model.CommandSpec spec;
@Mixin
LogginMixin logginMixin;

@Parameters(index = "0", paramLabel = "/path/to/vaultDirectory", description = "Path to the vault directory")
Path pathToVault;

@ArgGroup(multiplicity = "1")
PasswordSource passwordSource;

@ArgGroup(exclusive = false, multiplicity = "1")
MountSetup mountSetup;

@Option(names = {"--maxCleartextNameLength"}, description = "Maximum cleartext filename length limit of created files. Remark: If this limit is greater than the shortening threshold, it does not have any effect.")
void setMaxCleartextNameLength(int input) {
if (input <= 0) {
throw new CommandLine.ParameterException(spec.commandLine(),
String.format("Invalid value '%d' for option '--maxCleartextNameLength': " +
"value must be a positive Number between 1 and %d.", input, Integer.MAX_VALUE));
}
maxCleartextNameLength = input;
}

private int maxCleartextNameLength = 0;

private SecureRandom csprng = null;

@Override
public Integer call() throws Exception {
csprng = SecureRandom.getInstanceStrong();

var unverifiedConfig = readConfigFromStorage(pathToVault);
var fsPropsBuilder = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withKeyLoader(this::loadMasterkey) //
.withShorteningThreshold(unverifiedConfig.allegedShorteningThreshold()); //cryptofs checks, if config is signed with masterkey
if (maxCleartextNameLength > 0) {
fsPropsBuilder.withMaxCleartextNameLength(maxCleartextNameLength);
}

try (var fs = CryptoFileSystemProvider.newFileSystem(pathToVault, fsPropsBuilder.build());
var mount = mountSetup.mount(fs)) {
System.out.println(mount.getMountpoint().uri());
while (true) {
int c = System.in.read();
//TODO: Password piping is currently not supported due to read() returing -1
if (c == -1 || c == 0x03 || c == 0x04) {//Ctrl+C, Ctrl+D
LOG.info("Unmounting and locking vault...");
mount.unmount();
break;
}
}
} catch (UnmountFailedException e) {
LOG.error("Regular unmount failed. Just terminating process...", e);
}
return 0;
}

private Masterkey loadMasterkey(URI keyId) {
try (var passphraseContainer = passwordSource.readPassphrase()) {
Path filePath = pathToVault.resolve("masterkey.cryptomator");
return new MasterkeyFileAccess(PEPPER, csprng)
.load(filePath, CharBuffer.wrap(passphraseContainer.content()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* Attempts to read the vault config file and parse it without verifying its integrity.
*
* @throws IOException if reading the file fails
*/
static VaultConfig.UnverifiedVaultConfig readConfigFromStorage(Path vaultPath) throws IOException {
Path configPath = vaultPath.resolve(CONFIG_FILE_NAME);
LOG.debug("Reading vault config from file {}.", configPath);
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
return VaultConfig.decode(token);
}
LoggingMixin loggingMixin;

private int executionStrategy(ParseResult parseResult) {
if (logginMixin.isVerbose) {
if (loggingMixin.isVerbose) {
activateVerboseMode();
}
return new RunLast().execute(parseResult); // default execution strategy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import picocli.CommandLine.Model;
import picocli.CommandLine.Option;

public class LogginMixin {
public class LoggingMixin {

@Spec(Spec.Target.MIXEE)
private Model.CommandSpec mixee;

boolean isVerbose;


/**
* Sets a verbose logging leven on the LoggingMixin of the top-level command.
* @param isVerbose boolean flag to activate verbose mode
Expand All @@ -24,6 +23,6 @@ public void setVerbose(boolean isVerbose) {
// We want to store the verbosity value in a single, central place,
// so we find the top-level command,
// and store the verbosity level on our top-level command's LoggingMixin.
((CryptomatorCli) mixee.root().userObject()).logginMixin.isVerbose = true;
((CryptomatorCli) mixee.root().userObject()).loggingMixin.isVerbose = true;
}
}
109 changes: 109 additions & 0 deletions src/main/java/org/cryptomator/cli/Unlock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.cryptomator.cli;

import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.integrations.mount.UnmountFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import picocli.CommandLine.*;

import java.io.IOException;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.concurrent.Callable;

@Command(name = "unlock")
public class Unlock implements Callable<Integer> {

private static final Logger LOG = LoggerFactory.getLogger(Unlock.class);
private static final byte[] PEPPER = new byte[0];
private static final String CONFIG_FILE_NAME = "vault.cryptomator";

@Spec
Model.CommandSpec spec;
@Mixin
LoggingMixin loggingMixin;

@Parameters(index = "0", paramLabel = "/path/to/vaultDirectory", description = "Path to the vault directory")
Path pathToVault;

@ArgGroup(multiplicity = "1")
PasswordSource passwordSource;

@ArgGroup(exclusive = false, multiplicity = "1")
MountSetup mountSetup;

@Option(names = {"--maxCleartextNameLength"}, description = "Maximum cleartext filename length limit of created files. Remark: If this limit is greater than the shortening threshold, it does not have any effect.")
void setMaxCleartextNameLength(int input) {
if (input <= 0) {
throw new CommandLine.ParameterException(spec.commandLine(),
String.format("Invalid value '%d' for option '--maxCleartextNameLength': " +
"value must be a positive Number between 1 and %d.", input, Integer.MAX_VALUE));
}
maxCleartextNameLength = input;
}
private int maxCleartextNameLength = 0;

private SecureRandom csprng = null;

@Override
public Integer call() throws Exception {
csprng = SecureRandom.getInstanceStrong();

var unverifiedConfig = readConfigFromStorage(pathToVault);
var fsPropsBuilder = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withKeyLoader(this::loadMasterkey) //
.withShorteningThreshold(unverifiedConfig.allegedShorteningThreshold()); //cryptofs checks, if config is signed with masterkey
if (maxCleartextNameLength > 0) {
fsPropsBuilder.withMaxCleartextNameLength(maxCleartextNameLength);
}

try (var fs = CryptoFileSystemProvider.newFileSystem(pathToVault, fsPropsBuilder.build());
var mount = mountSetup.mount(fs)) {
System.out.println(mount.getMountpoint().uri());
while (true) {
int c = System.in.read();
//TODO: Password piping is currently not supported due to read() returing -1
if (c == -1 || c == 0x03 || c == 0x04) {//Ctrl+C, Ctrl+D
LOG.info("Unmounting and locking vault...");
mount.unmount();
break;
}
}
} catch (UnmountFailedException e) {
LOG.error("Regular unmount failed. Just terminating process...", e);
}
return 0;
}

private Masterkey loadMasterkey(URI keyId) {
try (var passphraseContainer = passwordSource.readPassphrase()) {
Path filePath = pathToVault.resolve("masterkey.cryptomator");
return new MasterkeyFileAccess(PEPPER, csprng)
.load(filePath, CharBuffer.wrap(passphraseContainer.content()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* Attempts to read the vault config file and parse it without verifying its integrity.
*
* @throws IOException if reading the file fails
*/
static VaultConfig.UnverifiedVaultConfig readConfigFromStorage(Path vaultPath) throws IOException {
Path configPath = vaultPath.resolve(CONFIG_FILE_NAME);
LOG.debug("Reading vault config from file {}.", configPath);
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
return VaultConfig.decode(token);
}

}

0 comments on commit 080c17e

Please sign in to comment.