diff --git a/devtools/cli/src/main/java/io/quarkus/cli/Config.java b/devtools/cli/src/main/java/io/quarkus/cli/Config.java index be4505002a1cf..017ebce2e3965 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/Config.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/Config.java @@ -5,6 +5,7 @@ import io.quarkus.cli.common.HelpOption; import io.quarkus.cli.common.OutputOptionMixin; +import io.quarkus.cli.config.Decrypt; import io.quarkus.cli.config.Encrypt; import io.quarkus.cli.config.RemoveConfig; import io.quarkus.cli.config.SetConfig; @@ -12,7 +13,7 @@ import picocli.CommandLine.Command; @Command(name = "config", header = "Manage Quarkus configuration", subcommands = { SetConfig.class, RemoveConfig.class, - Encrypt.class }) + Encrypt.class, Decrypt.class }) public class Config implements Callable { @CommandLine.Mixin(name = "output") protected OutputOptionMixin output; diff --git a/devtools/cli/src/main/java/io/quarkus/cli/config/Decrypt.java b/devtools/cli/src/main/java/io/quarkus/cli/config/Decrypt.java new file mode 100644 index 0000000000000..cc929b92e22f0 --- /dev/null +++ b/devtools/cli/src/main/java/io/quarkus/cli/config/Decrypt.java @@ -0,0 +1,76 @@ +package io.quarkus.cli.config; + +import static io.quarkus.devtools.messagewriter.MessageIcons.SUCCESS_ICON; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Base64; +import java.util.concurrent.Callable; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import io.quarkus.cli.config.Encrypt.KeyFormat; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Command(name = "decrypt", aliases = "dec", header = "Decrypt Secrets", description = "Decrypt a Secret value using the AES/GCM/NoPadding algorithm as a default.") +public class Decrypt extends BaseConfigCommand implements Callable { + @Parameters(index = "0", paramLabel = "SECRET", description = "The secret value to decrypt") + String secret; + + @Parameters(index = "1", paramLabel = "DECRYPTION KEY", description = "The decryption key") + String decryptionKey; + + @Option(names = { "-f", "--format" }, description = "The decryption key format (base64 / plain)", defaultValue = "base64") + KeyFormat decryptionKeyFormat; + + @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 = "Padding", defaultValue = "NoPadding") + String padding; + + @Option(hidden = true, names = { "-q", "--quiet" }, defaultValue = "false") + boolean quiet; + + @Override + public Integer call() throws Exception { + if (decryptionKey.startsWith("\\\"") && decryptionKey.endsWith("\"\\")) { + decryptionKey = decryptionKey.substring(2, decryptionKey.length() - 2); + } + + byte[] decryptionKeyBytes; + if (decryptionKeyFormat.equals(KeyFormat.base64)) { + decryptionKeyBytes = Base64.getUrlDecoder().decode(decryptionKey); + } else { + decryptionKeyBytes = decryptionKey.getBytes(UTF_8); + } + + Cipher cipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding); + ByteBuffer byteBuffer = ByteBuffer.wrap(Base64.getUrlDecoder().decode(secret.getBytes(UTF_8))); + int ivLength = byteBuffer.get(); + byte[] iv = new byte[ivLength]; + byteBuffer.get(iv); + byte[] encrypted = new byte[byteBuffer.remaining()]; + byteBuffer.get(encrypted); + + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + sha256.update(decryptionKeyBytes); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sha256.digest(), "AES"), new GCMParameterSpec(128, iv)); + String decrypted = new String(cipher.doFinal(encrypted), UTF_8); + + if (!quiet) { + String success = SUCCESS_ICON + " The secret @|bold " + secret + "|@ was decrypted to @|bold " + decrypted + "|@"; + output.info(success); + } + + return 0; + } +} diff --git a/devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java b/devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java index 2bdc8cd839730..42c7d76d49aa5 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java @@ -21,13 +21,13 @@ @Command(name = "encrypt", aliases = "enc", header = "Encrypt Secrets", description = "Encrypt a Secret value using the AES/GCM/NoPadding algorithm as a default. The encryption key is generated unless a specific key is set with the --key option.") public class Encrypt extends BaseConfigCommand implements Callable { - @Parameters(index = "0", paramLabel = "SECRET", description = "The Secret value to encrypt") + @Parameters(index = "0", paramLabel = "SECRET", description = "The secret value to encrypt") String secret; - @Option(names = { "-k", "--key" }, description = "The Encryption Key") + @Option(names = { "-k", "--key" }, description = "The encryption Key") String encryptionKey; - @Option(names = { "-f", "--format" }, description = "The Encryption Key Format (base64 / plain)", defaultValue = "base64") + @Option(names = { "-f", "--format" }, description = "The encryption key format (base64 / plain)", defaultValue = "base64") KeyFormat encryptionKeyFormat; @Option(hidden = true, names = { "-a", "--algorithm" }, description = "Algorithm", defaultValue = "AES") diff --git a/devtools/cli/src/test/java/io/quarkus/cli/config/DecryptTest.java b/devtools/cli/src/test/java/io/quarkus/cli/config/DecryptTest.java new file mode 100644 index 0000000000000..4f23d6b30a2b3 --- /dev/null +++ b/devtools/cli/src/test/java/io/quarkus/cli/config/DecryptTest.java @@ -0,0 +1,39 @@ +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.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; + +import io.quarkus.cli.CliDriver; + +@DisabledOnOs(value = OS.WINDOWS, disabledReason = "Parsing the stdout is not working on Github Windows, maybe because of the console formatting. I did try it in a Windows box and it works fine.") +public class DecryptTest { + @TempDir + Path tempDir; + + @Test + void decryptPlain() throws Exception { + CliDriver.Result result = CliDriver.execute(tempDir, "config", "decrypt", + "DPZqAC4GZNAXi6_43A4O2SBmaQssGkq6PS7rz8tzHDt1", "somearbitrarycrazystringthatdoesnotmatter", "-f=plain"); + Scanner scanner = new Scanner(result.getStdout()); + String[] split = scanner.nextLine().split(" "); + String secret = split[split.length - 1]; + assertEquals("1234", secret); + } + + @Test + void decryptBase64() throws Exception { + CliDriver.Result result = CliDriver.execute(tempDir, "config", "decrypt", + "DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg", "c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI"); + Scanner scanner = new Scanner(result.getStdout()); + String[] split = scanner.nextLine().split(" "); + String secret = split[split.length - 1]; + assertEquals("decoded", secret); + } +}