Skip to content

Commit

Permalink
Merge pull request #34493 from radcortez/fix-29172
Browse files Browse the repository at this point in the history
Add CLI command for Config
  • Loading branch information
radcortez committed Jan 25, 2024
2 parents b0199e8 + 0332ff2 commit bd6465f
Show file tree
Hide file tree
Showing 9 changed files with 633 additions and 1 deletion.
5 changes: 5 additions & 0 deletions devtools/cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-crypto</artifactId>
<scope>test</scope>
</dependency>
<!-- This dependency is here to make sure the build order is correct-->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
29 changes: 29 additions & 0 deletions devtools/cli/src/main/java/io/quarkus/cli/Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.cli;

import java.util.List;
import java.util.concurrent.Callable;

import io.quarkus.cli.common.OutputOptionMixin;
import io.quarkus.cli.config.Encrypt;
import io.quarkus.cli.config.SetConfig;
import picocli.CommandLine;
import picocli.CommandLine.Command;

@Command(name = "config", header = "Manage Quarkus configuration", subcommands = { SetConfig.class, Encrypt.class })
public class Config implements Callable<Integer> {
@CommandLine.Mixin(name = "output")
protected OutputOptionMixin output;

@CommandLine.Spec
protected CommandLine.Model.CommandSpec spec;

@CommandLine.Unmatched // avoids throwing errors for unmatched arguments
List<String> unmatchedArgs;

@Override
public Integer call() throws Exception {
CommandLine.ParseResult result = spec.commandLine().getParseResult();
CommandLine appCommand = spec.subcommands().get("set");
return appCommand.execute(result.originalArgs().stream().filter(x -> !"config".equals(x)).toArray(String[]::new));
}
}
10 changes: 9 additions & 1 deletion devtools/cli/src/main/java/io/quarkus/cli/QuarkusCli.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@
import picocli.CommandLine.UnmatchedArgumentException;

@CommandLine.Command(name = "quarkus", subcommands = {
Create.class, Build.class, Dev.class, Run.class, Test.class, ProjectExtensions.class, Image.class, Deploy.class,
Create.class,
Build.class,
Dev.class,
Run.class,
Test.class,
Config.class,
ProjectExtensions.class,
Image.class,
Deploy.class,
Registry.class,
Info.class,
Update.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.cli.config;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

import io.quarkus.cli.common.OutputOptionMixin;
import picocli.CommandLine;

public class BaseConfigCommand {
@CommandLine.Mixin(name = "output")
protected OutputOptionMixin output;

@CommandLine.Spec
protected CommandLine.Model.CommandSpec spec;

Path projectRoot;

protected Path projectRoot() {
if (projectRoot == null) {
projectRoot = output.getTestDirectory();
if (projectRoot == null) {
projectRoot = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
}
}
return projectRoot;
}

protected String encodeToString(byte[] data) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
}
}
99 changes: 99 additions & 0 deletions devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package io.quarkus.cli.config;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.concurrent.Callable;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

@Command(name = "encrypt", aliases = "enc", header = "Encrypt Secrets using AES/GCM/NoPadding algorithm by default")
public class Encrypt extends BaseConfigCommand implements Callable<Integer> {
@Option(required = true, names = { "-s", "--secret" }, description = "Secret")
String secret;

@Option(names = { "-k", "--key" }, description = "Encryption Key")
String encryptionKey;

@Option(names = { "-f", "--format" }, description = "Encryption Key Format (base64 / plain)", defaultValue = "base64")
KeyFormat encryptionKeyFormat;

@Option(hidden = true, names = { "-a", "--algorithm" }, description = "Algorithm", defaultValue = "AES")
String algorithm;

@Option(hidden = true, names = { "-m", "--mode" }, description = "Mode", defaultValue = "GCM")
String mode;

@Option(hidden = true, names = { "-p", "--padding" }, description = "Algorithm", defaultValue = "NoPadding")
String padding;

@Option(hidden = true, names = { "-q", "--quiet" }, defaultValue = "false")
boolean quiet;

private String encryptedSecret;

@Override
public Integer call() throws Exception {
if (encryptionKey == null) {
encryptionKey = encodeToString(generateEncryptionKey().getEncoded());
} else {
if (encryptionKeyFormat.equals(KeyFormat.base64)) {
encryptionKey = encodeToString(encryptionKey.getBytes());
}
}

Cipher cipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding);
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(encryptionKey.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sha256.digest(), "AES"), new GCMParameterSpec(128, iv));

byte[] encrypted = cipher.doFinal(secret.getBytes(StandardCharsets.UTF_8));

ByteBuffer message = ByteBuffer.allocate(1 + iv.length + encrypted.length);
message.put((byte) iv.length);
message.put(iv);
message.put(encrypted);

this.encryptedSecret = Base64.getUrlEncoder().withoutPadding().encodeToString((message.array()));
if (!quiet) {
System.out.println("Encrypted Secret: " + encryptedSecret);
System.out.println("Encryption Key: " + encryptionKey);
}

return 0;
}

private SecretKey generateEncryptionKey() {
try {
return KeyGenerator.getInstance(algorithm).generateKey();
} catch (Exception e) {
System.err.println("Error while generating the encryption key: " + e);
System.exit(-1);
}
return null;
}

public String getEncryptedSecret() {
return encryptedSecret;
}

public String getEncryptionKey() {
return encryptionKey;
}

public enum KeyFormat {
base64,
plain
}
}
102 changes: 102 additions & 0 deletions devtools/cli/src/main/java/io/quarkus/cli/config/SetConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.quarkus.cli.config;

import java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

import io.smallrye.config.ConfigValue;
import picocli.CommandLine;

@CommandLine.Command(name = "set")
public class SetConfig extends BaseConfigCommand implements Callable<Integer> {
@CommandLine.Option(required = true, names = { "-n", "--name" }, description = "Configuration name")
String name;
@CommandLine.Option(names = { "-a", "--value" }, description = "Configuration value")
String value;
@CommandLine.Option(names = { "-k", "--encrypt" }, description = "Encrypt value")
boolean encrypt;

@Override
public Integer call() throws Exception {
Path properties = projectRoot().resolve("src/main/resources/application.properties");
if (!properties.toFile().exists()) {
System.out.println("Could not find an application.properties file");
return 0;
}

List<String> lines = Files.readAllLines(properties);

if (encrypt) {
Encrypt encrypt = new Encrypt();
List<String> args = new ArrayList<>();
args.add("-q");
if (value == null) {
value = findKey(lines, name).getValue();
}
args.add("--secret=" + value);
if (value == null || value.length() == 0) {
System.out.println("Cannot encrypt an empty value");
return -1;
}

ConfigValue encryptionKey = findKey(lines, "smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key");
if (encryptionKey.getValue() != null) {
args.add("--key=" + encryptionKey.getValue());
}

int execute = new CommandLine(encrypt).execute(args.toArray(new String[] {}));
if (execute < 0) {
System.exit(execute);
}
value = "${aes-gcm-nopadding::" + encrypt.getEncryptedSecret() + "}";
if (encryptionKey.getValue() == null) {
lines.add(encryptionKey.getName() + "=" + encrypt.getEncryptionKey());
}
}

int nameLineNumber = -1;
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
if (line.startsWith(name + "=")) {
nameLineNumber = i;
break;
}
}

if (nameLineNumber != -1) {
if (value != null) {
System.out.println("Setting " + name + " to " + value);
lines.set(nameLineNumber, name + "=" + value);
} else {
System.out.println("Removing " + name);
lines.remove(nameLineNumber);
}
} else {
System.out.println("Adding " + name + " with " + value);
lines.add(name + "=" + (value != null ? value : ""));
}

try (BufferedWriter writer = Files.newBufferedWriter(properties)) {
for (String i : lines) {
writer.write(i);
writer.newLine();
}
}

return 0;
}

public static ConfigValue findKey(List<String> lines, String name) {
ConfigValue configValue = ConfigValue.builder().withName(name).build();
for (int i = 0; i < lines.size(); i++) {
final String line = lines.get(i);
if (line.startsWith(configValue.getName() + "=")) {
return configValue.withValue(line.substring(name.length() + 1)).withLineNumber(i);
}
}
return configValue;
}
}
78 changes: 78 additions & 0 deletions devtools/cli/src/test/java/io/quarkus/cli/config/EncryptTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.quarkus.cli.config;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.nio.file.Path;
import java.util.Scanner;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import io.quarkus.cli.CliDriver;
import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigBuilder;

class EncryptTest {
@TempDir
Path tempDir;

@Test
void encrypt() throws Exception {
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "--secret=12345678");
Scanner scanner = new Scanner(result.getStdout());
String secret = scanner.nextLine().split(": ")[1];
String encryptionKey = scanner.nextLine().split(": ")[1];

SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
.addDiscoveredSecretKeysHandlers()
.withDefaultValue("my.secret", "${aes-gcm-nopadding::" + secret + "}")
.withDefaultValue("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key", encryptionKey)
.build();

assertEquals("12345678", config.getConfigValue("my.secret").getValue());
}

@Test
void keyPlain() throws Exception {
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "--secret=12345678", "-f=plain",
"--key=12345678");
Scanner scanner = new Scanner(result.getStdout());
String secret = scanner.nextLine().split(": ")[1];

SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
.addDiscoveredSecretKeysHandlers()
.withDefaultValue("my.secret", "${aes-gcm-nopadding::" + secret + "}")
.withDefaultValue("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key", "12345678")
.build();

assertEquals("12345678", config.getConfigValue("my.secret").getValue());

config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
.addDiscoveredSecretKeysHandlers()
.withDefaultValue("my.secret", "${aes-gcm-nopadding::" + secret + "}")
.withDefaultValue("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key", "MTIzNDU2Nzg")
.withDefaultValue("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode", "true")
.build();

assertEquals("12345678", config.getConfigValue("my.secret").getValue());
}

@Test
void keyBase64() throws Exception {
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encrypt", "--secret=12345678", "--key=12345678");
Scanner scanner = new Scanner(result.getStdout());
String secret = scanner.nextLine().split(": ")[1];

SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
.addDiscoveredSecretKeysHandlers()
.withDefaultValue("my.secret", "${aes-gcm-nopadding::" + secret + "}")
.withDefaultValue("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key", "MTIzNDU2Nzg")
.build();

assertEquals("12345678", config.getConfigValue("my.secret").getValue());
}
}

0 comments on commit bd6465f

Please sign in to comment.