Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read key password from CLI #765

Merged
merged 3 commits into from
May 29, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.quorum.tessera.cli.keypassresolver;

import com.quorum.tessera.config.Config;
import com.quorum.tessera.config.KeyConfiguration;
import com.quorum.tessera.config.PrivateKeyType;
import com.quorum.tessera.config.keypairs.ConfigKeyPair;
import com.quorum.tessera.config.keypairs.FilesystemKeyPair;
import com.quorum.tessera.config.keypairs.InlineKeypair;
import com.quorum.tessera.config.util.PasswordReader;
import com.quorum.tessera.config.util.PasswordReaderFactory;
import com.quorum.tessera.io.SystemAdapter;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;

public class CliKeyPasswordResolver implements KeyPasswordResolver {

private static final int MAX_PASSWORD_ATTEMPTS = 2;

private final PasswordReader passwordReader;

public CliKeyPasswordResolver() {
this(PasswordReaderFactory.create());
}

public CliKeyPasswordResolver(final PasswordReader passwordReader) {
this.passwordReader = Objects.requireNonNull(passwordReader);
}

public void resolveKeyPasswords(final Config config) {

final KeyConfiguration input = config.getKeys();
if (input == null) {
//invalid config, but gets picked up by validation later
return;
}

final List<String> allPasswords = new ArrayList<>();
if (input.getPasswords() != null) {
allPasswords.addAll(input.getPasswords());
} else if (input.getPasswordFile() != null) {
try {
allPasswords.addAll(Files.readAllLines(input.getPasswordFile(), StandardCharsets.UTF_8));
} catch (final IOException ex) {
//dont do anything, if any keys are locked validation will complain that
//locked keys were provided without passwords
SystemAdapter.INSTANCE.err().println("Could not read the password file");
}
}

IntStream
.range(0, input.getKeyData().size())
.forEachOrdered(i -> {
if(i < allPasswords.size()) {
input.getKeyData().get(i).withPassword(allPasswords.get(i));
}
});

//decrypt the keys, either using provided passwords or read from CLI
IntStream
.rangeClosed(1, input.getKeyData().size())
.forEachOrdered(keyNumber -> getSingleKeyPassword(keyNumber, input.getKeyData().get(keyNumber-1)));
}

//TODO: make private
//@VisibleForTesting
public void getSingleKeyPassword(final int keyNumber, final ConfigKeyPair keyPair) {
final boolean isInline = keyPair instanceof InlineKeypair;
final boolean isFilesystem = keyPair instanceof FilesystemKeyPair;

if (!isInline && !isFilesystem) {
//some other key type that doesn't use passwords, skip
return;
}

final InlineKeypair inlineKey = isInline ? (InlineKeypair)keyPair : ((FilesystemKeyPair)keyPair).getInlineKeypair();

if(inlineKey == null) {
//filesystem key pair that couldn't load the keys, catch in validation later
return;
}

final boolean isLocked = inlineKey.getPrivateKeyConfig().getType() == PrivateKeyType.LOCKED;

if (isLocked) {
int currentAttemptNumber = MAX_PASSWORD_ATTEMPTS;
while (currentAttemptNumber > 0) {
if (StringUtils.isEmpty(keyPair.getPassword()) || keyPair.getPrivateKey() == null || keyPair.getPrivateKey().contains("NACL_FAILURE")) {
System.out.println("Password for key " + keyNumber + " missing on invalid.");
System.out.println("Enter a password for the key");
final String pass = passwordReader.readPasswordFromConsole();
keyPair.withPassword(pass);
}
currentAttemptNumber--;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.quorum.tessera.cli.keypassresolver;

import com.quorum.tessera.config.Config;

public interface KeyPasswordResolver {

/**
* Attempts to resolve all the locked keys using/fetching the required
* password.
*
* The passwords may be provided in the configuration, or they may be
* fetched from other sources as required.
*
* @param config the configuration that contains the keys to resolve, as
* well as some predefined passwords
*/
void resolveKeyPasswords(Config config);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.quorum.tessera.cli.keypassresolver.CliKeyPasswordResolver
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package com.quorum.tessera.cli.keypassresolver;

import com.quorum.tessera.config.*;
import com.quorum.tessera.config.keypairs.ConfigKeyPair;
import com.quorum.tessera.config.keypairs.DirectKeyPair;
import com.quorum.tessera.config.keypairs.FilesystemKeyPair;
import com.quorum.tessera.config.keypairs.InlineKeypair;
import com.quorum.tessera.config.util.PasswordReader;
import com.quorum.tessera.config.util.PasswordReaderFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.SystemOutRule;

import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class CliKeyPasswordResolverTest {

@Rule
public SystemOutRule systemOutRule = new SystemOutRule().enableLog();

private PasswordReader passwordReader;

private CliKeyPasswordResolver cliKeyPasswordResolver;

@Before
public void init() {
this.passwordReader = mock(PasswordReader.class);

this.cliKeyPasswordResolver = new CliKeyPasswordResolver(passwordReader);
}

@Test
public void defaultConstructorCreatesReaderInstanceFromFactory() throws ReflectiveOperationException {
final CliKeyPasswordResolver resolver = new CliKeyPasswordResolver();

final Field field = resolver.getClass().getDeclaredField("passwordReader");
field.setAccessible(true);
final Object obj = field.get(resolver);

assertThat(obj).isInstanceOf(PasswordReaderFactory.create().getClass());
}

@Test
public void emptyPasswordsReturnsSameKeys() {

//null paths since we won't actually be reading them
final ConfigKeyPair keypair = new FilesystemKeyPair(null, null);
final KeyConfiguration keyConfig = new KeyConfiguration(null, emptyList(), singletonList(keypair), null, null);
final Config config = new Config();
config.setKeys(keyConfig);

this.cliKeyPasswordResolver.resolveKeyPasswords(config);

assertThat(keyConfig.getKeyData()).hasSize(1);
final ConfigKeyPair returned = keyConfig.getKeyData().get(0);

//passwords are always non-null, set to empty string if not present or not needed
assertThat(returned.getPassword()).isNull();
assertThat(returned).isSameAs(keypair);
}

@Test
public void noPasswordsReturnsSameKeys() {

//null paths since we won't actually be reading them
final ConfigKeyPair keypair = new FilesystemKeyPair(null, null);
final KeyConfiguration keyConfig = new KeyConfiguration(null, null, singletonList(keypair), null, null);
final Config config = new Config();
config.setKeys(keyConfig);

this.cliKeyPasswordResolver.resolveKeyPasswords(config);

assertThat(keyConfig.getKeyData()).hasSize(1);
final ConfigKeyPair returned = keyConfig.getKeyData().get(0);

//passwords are always non-null, set to empty string if not present or not needed
assertThat(returned.getPassword()).isNull();
assertThat(returned).isSameAs(keypair);
}

@Test
public void passwordsAssignedToKeys() {

//null paths since we won't actually be reading them
final ConfigKeyPair keypair = new FilesystemKeyPair(null, null);
final KeyConfiguration keyConfig
= new KeyConfiguration(null, singletonList("passwordsAssignedToKeys"), singletonList(keypair), null, null);
final Config config = new Config();
config.setKeys(keyConfig);

this.cliKeyPasswordResolver.resolveKeyPasswords(config);

assertThat(keyConfig.getKeyData()).hasSize(1);
final ConfigKeyPair returned = keyConfig.getKeyData().get(0);
assertThat(returned.getPassword()).isEqualTo("passwordsAssignedToKeys");
}

@Test
public void unreadablePasswordFileGivesNoPasswords() throws IOException {

final Path passes = Files.createTempDirectory("testdirectory").resolve("nonexistantfile.txt");

final ConfigKeyPair keypair = new FilesystemKeyPair(null, null);
final KeyConfiguration keyConfig = new KeyConfiguration(passes, null, singletonList(keypair), null, null);
final Config config = new Config();
config.setKeys(keyConfig);

this.cliKeyPasswordResolver.resolveKeyPasswords(config);

assertThat(keyConfig.getKeyData()).hasSize(1);
final ConfigKeyPair returned = keyConfig.getKeyData().get(0);
assertThat(returned.getPassword()).isNull();
}

@Test
public void readablePasswordFileAssignsPasswords() throws IOException {

final Path passes = Files.createTempDirectory("testdirectory").resolve("passwords.txt");
Files.write(passes, "q".getBytes());

final ConfigKeyPair keypair = new FilesystemKeyPair(null, null);
final KeyConfiguration keyConfig = new KeyConfiguration(passes, null, singletonList(keypair), null, null);
final Config config = new Config();
config.setKeys(keyConfig);

this.cliKeyPasswordResolver.resolveKeyPasswords(config);

assertThat(keyConfig.getKeyData()).hasSize(1);
final ConfigKeyPair returned = keyConfig.getKeyData().get(0);
assertThat(returned.getPassword()).isEqualTo("q");
}

@Test
public void nullKeyConfigReturns() {
final Throwable throwable = catchThrowable(() -> this.cliKeyPasswordResolver.resolveKeyPasswords(new Config()));

assertThat(throwable).isNull();
}

@Test
public void gettingPasswordForNonInlineOrFileSystemKeyReturns() {

final ConfigKeyPair keyPair = new DirectKeyPair("public", "private");

this.cliKeyPasswordResolver.getSingleKeyPassword(0, keyPair);

assertThat(keyPair.getPassword()).isNullOrEmpty();
}

@Test
public void nullInlineKeyDoesntReadPassword() {

final ConfigKeyPair keyPair = new FilesystemKeyPair(null, null);

this.cliKeyPasswordResolver.getSingleKeyPassword(0, keyPair);

assertThat(keyPair.getPassword()).isNullOrEmpty();
}

@Test
public void unlockedKeyDoesntReadPassword() {
final KeyDataConfig privKeyDataConfig = new KeyDataConfig(
new PrivateKeyData("Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA=", null, null, null, null),
PrivateKeyType.UNLOCKED
);

final InlineKeypair keyPair = new InlineKeypair("public", privKeyDataConfig);

this.cliKeyPasswordResolver.getSingleKeyPassword(0, keyPair);

assertThat(keyPair.getPassword()).isNullOrEmpty();
}

@Test
public void lockedKeyWithEmptyPasswordRequestsPassword() {
when(passwordReader.readPasswordFromConsole()).thenReturn("a");

final KeyDataConfig privKeyDataConfig = new KeyDataConfig(
new PrivateKeyData(
"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA=",
"yb7M8aRJzgxoJM2NecAPcmSVWDW1tRjv",
"MIqkFlgR2BWEpx2U0rObGg==",
"Gtvp1t6XZEiFVyaE/LHiP1+yvOIBBoiOL+bKeqcKgpiNt4j1oDDoqCC47UJpmQRC",
new ArgonOptions("i", 10, 1048576, 4)
),
PrivateKeyType.LOCKED
);

final InlineKeypair keyPair = new InlineKeypair("public", privKeyDataConfig);
keyPair.withPassword("");

this.cliKeyPasswordResolver.getSingleKeyPassword(0, keyPair);

assertThat(systemOutRule.getLog())
.containsOnlyOnce("Password for key 0 missing on invalid.\nEnter a password for the key");
}

@Test
public void lockedKeyWithInvalidPasswordRequestsPassword() {
when(passwordReader.readPasswordFromConsole()).thenReturn("a");

final KeyDataConfig privKeyDataConfig = new KeyDataConfig(
new PrivateKeyData(
"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA=",
"yb7M8aRJzgxoJM2NecAPcmSVWDW1tRjv",
"MIqkFlgR2BWEpx2U0rObGg==",
"Gtvp1t6XZEiFVyaE/LHiP1+yvOIBBoiOL+bKeqcKgpiNt4j1oDDoqCC47UJpmQRC",
new ArgonOptions("i", 10, 1048576, 4)
),
PrivateKeyType.LOCKED
);

final InlineKeypair keyPair = new InlineKeypair("public", privKeyDataConfig);
keyPair.withPassword("invalidPassword");

this.cliKeyPasswordResolver.getSingleKeyPassword(0, keyPair);

assertThat(systemOutRule.getLog())
.containsOnlyOnce("Password for key 0 missing on invalid.\nEnter a password for the key");
}

@Test
public void invalidRequestedPasswordRerequests() {
when(passwordReader.readPasswordFromConsole()).thenReturn("invalid", "a");

final KeyDataConfig privKeyDataConfig = new KeyDataConfig(
new PrivateKeyData(
"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA=",
"yb7M8aRJzgxoJM2NecAPcmSVWDW1tRjv",
"MIqkFlgR2BWEpx2U0rObGg==",
"Gtvp1t6XZEiFVyaE/LHiP1+yvOIBBoiOL+bKeqcKgpiNt4j1oDDoqCC47UJpmQRC",
new ArgonOptions("i", 10, 1048576, 4)
),
PrivateKeyType.LOCKED
);

final InlineKeypair keyPair = new InlineKeypair("public", privKeyDataConfig);
keyPair.withPassword("invalidPassword");

this.cliKeyPasswordResolver.getSingleKeyPassword(0, keyPair);

//work around for checking string appears twice in message
final String expectedMessage = "Password for key 0 missing on invalid.\nEnter a password for the key";
assertThat(systemOutRule.getLog().split(expectedMessage)).hasSize(3);
}

}
Loading