Skip to content

Commit

Permalink
implement mvp
Browse files Browse the repository at this point in the history
  • Loading branch information
infeo committed Sep 13, 2024
1 parent aecf18c commit 92f3cb3
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 0 deletions.
90 changes: 90 additions & 0 deletions src/main/java/org/cryptomator/cli/CryptomatorCli.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.cryptomator.cli;

import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
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.Command;
import picocli.CommandLine.Parameters;

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

@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> {

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


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

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

@CommandLine.ArgGroup(exclusive = false, multiplicity = "1")
MountOptions mountOptions;

private SecureRandom csrpg = null;

@Override
public Integer call() throws Exception {
csrpg = SecureRandom.getInstanceStrong();
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withKeyLoader(this::loadMasterkey) //
//TODO: shortening Threshold
//TODO: maxCleartextname
.build();
try (var fs = CryptoFileSystemProvider.newFileSystem(pathToVault, fsProps);
var mount = mountOptions.mount(fs)) {

while (true) {
int c = System.in.read();
if (c == -1) { //END OF STREAM
//TODO: terminate with error?
mount.unmount();
return 1;
} else if (c == 0x03) {//Ctrl+C
mount.unmount();
break;
}
}
} catch (UnmountFailedException e) {
LOG.error("Regular unmount failed. Just terminating...", e);
}
return 0;
}

private Masterkey loadMasterkey(URI keyId) {
try {
char[] passphrase = passwordSource.readPassphrase();
Path filePath = pathToVault.resolve("masterkey.cryptomator");

var masterkey = new MasterkeyFileAccess(PEPPER, csrpg).load(filePath, CharBuffer.wrap(passphrase));
Arrays.fill(passphrase, '\u0000');
return masterkey;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String... args) {
int exitCode = new CommandLine(new CryptomatorCli())
.setPosixClusteredShortOptionsAllowed(false)
.execute(args);
System.exit(exitCode);
}
}
81 changes: 81 additions & 0 deletions src/main/java/org/cryptomator/cli/MountOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.cryptomator.cli;

import org.cryptomator.integrations.common.IntegrationsLoader;
import org.cryptomator.integrations.mount.*;
import picocli.CommandLine;

import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

public class MountOptions {

@CommandLine.Spec
CommandLine.Model.CommandSpec spec;

@CommandLine.Option(names = {"--mounter"}, paramLabel = "fully.qualified.ClassName", description = "Name of the mounter to use", required = true)
void setMountService(String value) {
var services = IntegrationsLoader.loadAll(MountService.class).toList();
var service = services.stream().filter(s -> s.getClass().getName().equals(value)).findFirst();
if (service.isEmpty()) {
var availableServices = services.stream().map(s -> getClass().getName()).collect(Collectors.joining(","));
var errorMessage = String.format("Invalid value '%s' for option '--mounter': Available mounters are [%s].", value, availableServices);
throw new CommandLine.ParameterException(spec.commandLine(), errorMessage);
}
this.mountService = service.get();
}

private MountService mountService;

@CommandLine.Option(names = {"--mountPoint"}, paramLabel = "/path/to/mount/point", description = "Path to the mount point. Requirements for mount point depend on the chosen mount service")
Optional<Path> mountPoint;

@CommandLine.Option(names = {"--volumeName"}, description = "Name of the virtual volume.")
Optional<String> volumeName;

@CommandLine.Option(names = {"--volumeId"}, description = "Id of the virtual volume.")
String volumeId = UUID.randomUUID().toString();

@CommandLine.Option(names = {"--mountOption", "-mop"}, description = "Additional mount option. For a list of mountoptions, see the WinFsp, macFUSE, FUSE-T and libfuse documentation.")
List<String> mountOptions = new ArrayList<>();

@CommandLine.Option(names = {"--loopbackHostName"}, description = "Name of the loopback address.")
Optional<String> loopbackHostName;
@CommandLine.Option(names = {"--loopbackPort"}, description = "Port used at the loopback address.")
Optional<Integer> loopbackPort;

MountBuilder prepareMountBuilder(FileSystem fs) {
var builder = mountService.forFileSystem(fs.getPath("/"));
for (var capability : mountService.capabilities()) {
switch (capability) {
case FILE_SYSTEM_NAME -> builder.setFileSystemName("cryptoFs");
case LOOPBACK_PORT -> loopbackPort.ifPresent(builder::setLoopbackPort);
case LOOPBACK_HOST_NAME -> loopbackHostName.ifPresent(builder::setLoopbackHostName);
//TODO: case READ_ONLY -> builder.setReadOnly(vaultSettings.usesReadOnlyMode.get());
case MOUNT_FLAGS -> {
if (mountOptions.isEmpty()) {
builder.setMountFlags(mountService.getDefaultMountFlags());
} else {
builder.setMountFlags(String.join(" ", mountOptions));
}
}
case VOLUME_ID -> builder.setVolumeId(volumeId);
case VOLUME_NAME -> volumeName.ifPresent(builder::setVolumeName);
}
}
return builder;
}

Mount mount(FileSystem fs) throws MountFailedException {
if (!mountService.hasCapability(MountCapability.MOUNT_TO_SYSTEM_CHOSEN_PATH) && mountPoint.isEmpty()) {
throw new CommandLine.ParameterException(spec.commandLine(), "The selected mounter %s requires a mount point. Use --mountPoint /path/to/mount/point to specify it.".formatted(mountService.displayName()));
}
var builder = prepareMountBuilder(fs);
mountPoint.ifPresent(builder::setMountpoint);
return builder.mount();
}
}
92 changes: 92 additions & 0 deletions src/main/java/org/cryptomator/cli/PasswordSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.cryptomator.cli;

import picocli.CommandLine;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class PasswordSource {

@CommandLine.Option(names = {"--password"}, paramLabel = "Passphrase", description = "Passphrase, read from STDIN")
boolean passphraseStdin;

@CommandLine.Option(names = "--password:env", description = "Name of the environment variable containing the passphrase")
String passphraseEnvironmentVariable = null;

@CommandLine.Option(names = "--password:file", description = "Path of the file containing the passphrase")
Path passphraseFile = null;

@CommandLine.Option(names = "--password:ipc", hidden = true, description = "Used by Cryptomator GUI")
boolean passphraseIpc = false;


char[] readPassphrase() throws IOException {
if (passphraseStdin) {
return readPassphraseFromStdin();
} else if (passphraseEnvironmentVariable != null) {
return readPassphraseFromEnvironment();
} else if (passphraseFile != null) {
return readPassphraseFromFile();
} else {
//TODO: use ipc
return new char[]{};
}
}

private char[] readPassphraseFromStdin() {
System.out.println("Enter a value for --password:");
var console = System.console();
if (console == null) {
throw new IllegalStateException("No console found to read password from.");
}
return console.readPassword();
}

private char[] readPassphraseFromEnvironment() {
var tmp = System.getenv(passphraseEnvironmentVariable);
if (tmp == null) {
throw new ReadingEnvironmentVariableFailedException("Environment variable " + passphraseEnvironmentVariable + " is not defined.");
}
char[] result = new char[tmp.length()];
tmp.getChars(0, tmp.length(), result, 0);
return result;
}

private char[] readPassphraseFromFile() throws ReadingFileFailedException {
try {
var bytes = Files.readAllBytes(passphraseFile);
var byteBuffer = ByteBuffer.wrap(bytes);
var charBuffer = StandardCharsets.UTF_8.decode(byteBuffer);
return charBuffer.array();
} catch (IOException e) {
throw new ReadingFileFailedException(e);
}
}

static class PasswordSourceException extends RuntimeException {
PasswordSourceException(String msg) {
super(msg);
}

PasswordSourceException(Throwable cause) {
super(cause);
}
}

static class ReadingFileFailedException extends PasswordSourceException {
ReadingFileFailedException(Throwable e) {
super(e);

}
}

static class ReadingEnvironmentVariableFailedException extends PasswordSourceException {
ReadingEnvironmentVariableFailedException(String msg) {
super(msg);
}
}

}

0 comments on commit 92f3cb3

Please sign in to comment.