-
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
582 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); | ||
} | ||
} |
94 changes: 94 additions & 0 deletions
94
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,94 @@ | ||
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 = { "-b" }, description = "Encryption Key in Base64 format", defaultValue = "false") | ||
private boolean base64EncryptionKey; | ||
|
||
@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 (!base64EncryptionKey) { | ||
encryptionKey = encodeToString(encryptionKey.getBytes(StandardCharsets.UTF_8)); | ||
} | ||
} | ||
|
||
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; | ||
} | ||
} |
103 changes: 103 additions & 0 deletions
103
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,103 @@ | ||
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("-b"); | ||
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; | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
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,25 @@ | ||
package io.quarkus.cli.config; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
||
import java.nio.file.Paths; | ||
import java.util.Scanner; | ||
|
||
import org.junit.jupiter.api.Test; | ||
|
||
import io.quarkus.cli.CliDriver; | ||
import io.smallrye.config.crypto.AESGCMNoPaddingSecretKeysHandler; | ||
|
||
class EncryptorTest { | ||
@Test | ||
void encrypt() throws Exception { | ||
CliDriver.Result result = CliDriver.execute(Paths.get(System.getProperty("user.dir")), "config", "encryptor", | ||
"--secret=12345678"); | ||
Scanner scanner = new Scanner(result.getStdout()); | ||
String secret = scanner.nextLine().split(": ")[1]; | ||
String encryptionKey = scanner.nextLine().split(": ")[1]; | ||
|
||
AESGCMNoPaddingSecretKeysHandler handler = new AESGCMNoPaddingSecretKeysHandler(encryptionKey); | ||
assertEquals("12345678", handler.decode(secret)); | ||
} | ||
} |
Oops, something went wrong.