-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
9 changed files
with
633 additions
and
1 deletion.
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
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,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.Encryptor; | ||
import io.quarkus.cli.config.SetConfig; | ||
import picocli.CommandLine; | ||
import picocli.CommandLine.Command; | ||
|
||
@Command(name = "config", header = "Manage Quarkus configuration", subcommands = { SetConfig.class, Encryptor.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)); | ||
} | ||
} |
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
32 changes: 32 additions & 0 deletions
32
devtools/cli/src/main/java/io/quarkus/cli/config/BaseConfigCommand.java
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,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
99
devtools/cli/src/main/java/io/quarkus/cli/config/Encryptor.java
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,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 = "encryptor", aliases = "enc", header = "Encrypt Secrets using AES/GCM/NoPadding algorithm by default") | ||
public class Encryptor 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
102
devtools/cli/src/main/java/io/quarkus/cli/config/SetConfig.java
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,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) { | ||
Encryptor encryptor = new Encryptor(); | ||
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(encryptor).execute(args.toArray(new String[] {})); | ||
if (execute < 0) { | ||
System.exit(execute); | ||
} | ||
value = "${aes-gcm-nopadding::" + encryptor.getEncryptedSecret() + "}"; | ||
if (encryptionKey.getValue() == null) { | ||
lines.add(encryptionKey.getName() + "=" + encryptor.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
78
devtools/cli/src/test/java/io/quarkus/cli/config/EncryptorTest.java
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,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 EncryptorTest { | ||
@TempDir | ||
Path tempDir; | ||
|
||
@Test | ||
void encrypt() throws Exception { | ||
CliDriver.Result result = CliDriver.execute(tempDir, "config", "encryptor", "--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", "encryptor", "--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", "encryptor", "--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()); | ||
} | ||
} |
Oops, something went wrong.