-
-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
|
||
} |