Skip to content

Commit 16fe8cb

Browse files
committed
Add a Cli command to decrypt secrets
1 parent fae46f4 commit 16fe8cb

File tree

4 files changed

+120
-4
lines changed

4 files changed

+120
-4
lines changed

devtools/cli/src/main/java/io/quarkus/cli/Config.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55

66
import io.quarkus.cli.common.HelpOption;
77
import io.quarkus.cli.common.OutputOptionMixin;
8+
import io.quarkus.cli.config.Decrypt;
89
import io.quarkus.cli.config.Encrypt;
910
import io.quarkus.cli.config.RemoveConfig;
1011
import io.quarkus.cli.config.SetConfig;
1112
import picocli.CommandLine;
1213
import picocli.CommandLine.Command;
1314

1415
@Command(name = "config", header = "Manage Quarkus configuration", subcommands = { SetConfig.class, RemoveConfig.class,
15-
Encrypt.class })
16+
Encrypt.class, Decrypt.class })
1617
public class Config implements Callable<Integer> {
1718
@CommandLine.Mixin(name = "output")
1819
protected OutputOptionMixin output;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package io.quarkus.cli.config;
2+
3+
import static io.quarkus.devtools.messagewriter.MessageIcons.SUCCESS_ICON;
4+
import static java.nio.charset.StandardCharsets.UTF_8;
5+
6+
import java.nio.ByteBuffer;
7+
import java.security.MessageDigest;
8+
import java.util.Base64;
9+
import java.util.concurrent.Callable;
10+
11+
import javax.crypto.Cipher;
12+
import javax.crypto.spec.GCMParameterSpec;
13+
import javax.crypto.spec.SecretKeySpec;
14+
15+
import io.quarkus.cli.config.Encrypt.KeyFormat;
16+
import picocli.CommandLine.Command;
17+
import picocli.CommandLine.Option;
18+
import picocli.CommandLine.Parameters;
19+
20+
@Command(name = "decrypt", aliases = "dec", header = "Decrypt Secrets", description = "Decrypt a Secret value using the AES/GCM/NoPadding algorithm as a default.")
21+
public class Decrypt extends BaseConfigCommand implements Callable<Integer> {
22+
@Parameters(index = "0", paramLabel = "SECRET", description = "The secret value to decrypt")
23+
String secret;
24+
25+
@Parameters(index = "1", paramLabel = "DECRYPTION KEY", description = "The decryption key")
26+
String decryptionKey;
27+
28+
@Option(names = { "-f", "--format" }, description = "The decryption key format (base64 / plain)", defaultValue = "base64")
29+
KeyFormat decryptionKeyFormat;
30+
31+
@Option(hidden = true, names = { "-a", "--algorithm" }, description = "Algorithm", defaultValue = "AES")
32+
String algorithm;
33+
34+
@Option(hidden = true, names = { "-m", "--mode" }, description = "Mode", defaultValue = "GCM")
35+
String mode;
36+
37+
@Option(hidden = true, names = { "-p", "--padding" }, description = "Padding", defaultValue = "NoPadding")
38+
String padding;
39+
40+
@Option(hidden = true, names = { "-q", "--quiet" }, defaultValue = "false")
41+
boolean quiet;
42+
43+
@Override
44+
public Integer call() throws Exception {
45+
if (decryptionKey.startsWith("\\\"") && decryptionKey.endsWith("\"\\")) {
46+
decryptionKey = decryptionKey.substring(2, decryptionKey.length() - 2);
47+
}
48+
49+
byte[] decryptionKeyBytes;
50+
if (decryptionKeyFormat.equals(KeyFormat.base64)) {
51+
decryptionKeyBytes = Base64.getUrlDecoder().decode(decryptionKey);
52+
} else {
53+
decryptionKeyBytes = decryptionKey.getBytes(UTF_8);
54+
}
55+
56+
Cipher cipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding);
57+
ByteBuffer byteBuffer = ByteBuffer.wrap(Base64.getUrlDecoder().decode(secret.getBytes(UTF_8)));
58+
int ivLength = byteBuffer.get();
59+
byte[] iv = new byte[ivLength];
60+
byteBuffer.get(iv);
61+
byte[] encrypted = new byte[byteBuffer.remaining()];
62+
byteBuffer.get(encrypted);
63+
64+
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
65+
sha256.update(decryptionKeyBytes);
66+
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sha256.digest(), "AES"), new GCMParameterSpec(128, iv));
67+
String decrypted = new String(cipher.doFinal(encrypted), UTF_8);
68+
69+
if (!quiet) {
70+
String success = SUCCESS_ICON + " The secret @|bold " + secret + "|@ was decrypted to @|bold " + decrypted + "|@";
71+
output.info(success);
72+
}
73+
74+
return 0;
75+
}
76+
}

devtools/cli/src/main/java/io/quarkus/cli/config/Encrypt.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@
2121

2222
@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.")
2323
public class Encrypt extends BaseConfigCommand implements Callable<Integer> {
24-
@Parameters(index = "0", paramLabel = "SECRET", description = "The Secret value to encrypt")
24+
@Parameters(index = "0", paramLabel = "SECRET", description = "The secret value to encrypt")
2525
String secret;
2626

27-
@Option(names = { "-k", "--key" }, description = "The Encryption Key")
27+
@Option(names = { "-k", "--key" }, description = "The encryption Key")
2828
String encryptionKey;
2929

30-
@Option(names = { "-f", "--format" }, description = "The Encryption Key Format (base64 / plain)", defaultValue = "base64")
30+
@Option(names = { "-f", "--format" }, description = "The encryption key format (base64 / plain)", defaultValue = "base64")
3131
KeyFormat encryptionKeyFormat;
3232

3333
@Option(hidden = true, names = { "-a", "--algorithm" }, description = "Algorithm", defaultValue = "AES")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.quarkus.cli.config;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.nio.file.Path;
6+
import java.util.Scanner;
7+
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.condition.DisabledOnOs;
10+
import org.junit.jupiter.api.condition.OS;
11+
import org.junit.jupiter.api.io.TempDir;
12+
13+
import io.quarkus.cli.CliDriver;
14+
15+
@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.")
16+
public class DecryptTest {
17+
@TempDir
18+
Path tempDir;
19+
20+
@Test
21+
void decryptPlain() throws Exception {
22+
CliDriver.Result result = CliDriver.execute(tempDir, "config", "decrypt",
23+
"DPZqAC4GZNAXi6_43A4O2SBmaQssGkq6PS7rz8tzHDt1", "somearbitrarycrazystringthatdoesnotmatter", "-f=plain");
24+
Scanner scanner = new Scanner(result.getStdout());
25+
String[] split = scanner.nextLine().split(" ");
26+
String secret = split[split.length - 1];
27+
assertEquals("1234", secret);
28+
}
29+
30+
@Test
31+
void decryptBase64() throws Exception {
32+
CliDriver.Result result = CliDriver.execute(tempDir, "config", "decrypt",
33+
"DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg", "c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI");
34+
Scanner scanner = new Scanner(result.getStdout());
35+
String[] split = scanner.nextLine().split(" ");
36+
String secret = split[split.length - 1];
37+
assertEquals("decoded", secret);
38+
}
39+
}

0 commit comments

Comments
 (0)