Skip to content

Commit ad4761b

Browse files
authored
Feature: Add vault creation command (#101)
* Add vault creation command with default values
2 parents 3a1c457 + b1b42dd commit ad4761b

4 files changed

Lines changed: 123 additions & 2 deletions

File tree

src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
requires org.fusesource.jansi;
1111
requires ch.qos.logback.core;
1212
requires ch.qos.logback.classic;
13+
requires org.cryptomator.cryptolib;
1314

1415
provides Configurator with LogbackConfigurator;
1516
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package org.cryptomator.cli;
2+
3+
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
4+
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
5+
import org.cryptomator.cryptolib.api.CryptoException;
6+
import org.cryptomator.cryptolib.api.CryptorProvider;
7+
import org.cryptomator.cryptolib.api.Masterkey;
8+
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
import picocli.CommandLine.ArgGroup;
13+
import picocli.CommandLine.Command;
14+
import picocli.CommandLine.Mixin;
15+
import picocli.CommandLine.Model;
16+
import picocli.CommandLine.Parameters;
17+
import picocli.CommandLine.Spec;
18+
19+
import java.io.IOException;
20+
import java.net.URI;
21+
import java.nio.CharBuffer;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.security.SecureRandom;
25+
import java.util.concurrent.Callable;
26+
27+
@Command(
28+
name = "create",
29+
header = "Creates a vault",
30+
description = "Creates a new cryptomator vault at the specified path.",
31+
parameterListHeading = "%nParameters:%n",
32+
headerHeading = "Usage:%n%n",
33+
synopsisHeading = "%n",
34+
descriptionHeading = "%nDescription:%n%n",
35+
optionListHeading = "%nOptions:%n",
36+
mixinStandardHelpOptions = true)
37+
public class Create implements Callable<Integer> {
38+
39+
private static final Logger LOG = LoggerFactory.getLogger(Create.class);
40+
private static final byte[] PEPPER = new byte[0];
41+
private static final String MASTERKEY_FILE_NAME = "masterkey.cryptomator";
42+
private static final URI DEFAULT_KEY_ID = URI.create("masterkeyfile:" + MASTERKEY_FILE_NAME);
43+
44+
@Spec Model.CommandSpec spec;
45+
@Mixin LoggingMixin loggingMixin;
46+
47+
@Parameters(
48+
index = "0",
49+
paramLabel = "/path/to/vaultDirectory",
50+
description = "Path to the vault directory")
51+
Path pathToVault;
52+
53+
@ArgGroup(multiplicity = "1")
54+
PasswordSource passwordSource;
55+
56+
private SecureRandom csprng = null;
57+
58+
@Override
59+
public Integer call() throws Exception {
60+
csprng = SecureRandom.getInstanceStrong();
61+
62+
try (var passphraseContainer = passwordSource.readPassphrase()) {
63+
passwordSource.confirmPassphrase();
64+
65+
// Throw exception if there's something already there.
66+
Files.createDirectory(pathToVault);
67+
68+
try (var masterkey = Masterkey.generate(csprng)) {
69+
persistMasterkey(pathToVault, masterkey, passphraseContainer.content());
70+
initializeVault(pathToVault, masterkey);
71+
}
72+
}
73+
74+
LOG.info("Vault created successfully in {}", pathToVault);
75+
return 0;
76+
}
77+
78+
private void persistMasterkey(Path path, Masterkey masterkey, char[] passphrase)
79+
throws IOException {
80+
Path masterkeyFilePath = path.resolve(MASTERKEY_FILE_NAME);
81+
MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(PEPPER, csprng);
82+
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, CharBuffer.wrap(passphrase));
83+
}
84+
85+
private void initializeVault(Path path, Masterkey masterkey) throws IOException {
86+
CryptoFileSystemProperties fsProps =
87+
CryptoFileSystemProperties.cryptoFileSystemProperties()
88+
.withCipherCombo(CryptorProvider.Scheme.SIV_GCM)
89+
.withKeyLoader(ignored -> masterkey.copy())
90+
.build();
91+
try {
92+
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
93+
} catch (CryptoException e) {
94+
throw new IOException("Vault initialization failed", e);
95+
}
96+
}
97+
}

src/main/java/org/cryptomator/cli/CryptomatorCli.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
@Command(name = "cryptomator-cli",
1212
mixinStandardHelpOptions = true,
1313
version = "${org.cryptomator.cli.version}",
14-
description = "Unlocks a cryptomator vault and mounts it into the system.",
15-
subcommands = { Unlock.class, ListMounters.class, CommandLine.HelpCommand.class})
14+
description = "Manage Cryptomator vaults from the command line.",
15+
subcommands = {Create.class, Unlock.class, ListMounters.class, CommandLine.HelpCommand.class})
1616
public class CryptomatorCli {
1717

1818
private static final Logger LOG = LoggerFactory.getLogger(CryptomatorCli.class);

src/main/java/org/cryptomator/cli/PasswordSource.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ Passphrase readPassphrase() throws IOException {
3838
throw new IllegalStateException("Passphrase source not specified, but required.");
3939
}
4040

41+
void confirmPassphrase() throws IOException {
42+
// Don't confirm the passphrase if stdin is piped.
43+
if (passphraseStdin == null || System.console() == null) {
44+
return;
45+
}
46+
char[] confirmationInput = System.console().readPassword("Confirm passphrase: ");
47+
if (confirmationInput == null) {
48+
throw new PassphraseNotConfirmedException("Passphrase confirmation failed (EOF reached).");
49+
}
50+
try (Passphrase confirmationPassphrase = new Passphrase(confirmationInput)) {
51+
System.out.println("\n");
52+
if (!Arrays.equals(passphraseStdin, confirmationPassphrase.content)) {
53+
throw new PassphraseNotConfirmedException("Passphrase does not match. Please try again.");
54+
}
55+
}
56+
}
57+
4158
private Passphrase readPassphraseFromEnvironment() {
4259
LOG.debug("Reading passphrase from env variable '{}'", passphraseEnvironmentVariable);
4360
var tmp = System.getenv(passphraseEnvironmentVariable);
@@ -106,6 +123,12 @@ static class ReadingEnvironmentVariableFailedException extends PasswordSourceExc
106123
}
107124
}
108125

126+
static class PassphraseNotConfirmedException extends PasswordSourceException {
127+
PassphraseNotConfirmedException(String msg) {
128+
super(msg);
129+
}
130+
}
131+
109132
record Passphrase(char[] content) implements AutoCloseable {
110133

111134
@Override

0 commit comments

Comments
 (0)