diff options
author | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-11-08 17:06:15 +0100 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-11-08 17:12:54 +0100 |
commit | 29993d7fa6af8edbc51dcd903a1e27d3f62b4e82 (patch) | |
tree | 19e211cf4be60d056dd4834b532e5027df279260 /vespaclient-java | |
parent | f268379cb63e70cefaea18999767244a7d8bfc7f (diff) |
Add a simple base conversion tool
Currently supports converting from and to any combination of
base {16, 58, 62, 64}. Input is read from STDIN and is intentionally
limited in length due to the algorithmic complexity of base
conversions that are not a power of two. Converted value is
written to STDOUT.
Diffstat (limited to 'vespaclient-java')
5 files changed, 143 insertions, 2 deletions
diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java index 11bd8815d77..0498154aa91 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/Main.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.security.tool; +import com.yahoo.vespa.security.tool.crypto.ConvertBaseTool; import com.yahoo.vespa.security.tool.crypto.DecryptTool; import com.yahoo.vespa.security.tool.crypto.EncryptTool; import com.yahoo.vespa.security.tool.crypto.KeygenTool; @@ -45,7 +46,8 @@ public class Main { } private static final List<Tool> TOOLS = List.of( - new KeygenTool(), new EncryptTool(), new DecryptTool(), new TokenInfoTool()); + new KeygenTool(), new EncryptTool(), new DecryptTool(), new TokenInfoTool(), + new ConvertBaseTool()); private static Optional<Tool> toolFromCliArgs(String[] args) { if (args.length == 0) { diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ConvertBaseTool.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ConvertBaseTool.java new file mode 100644 index 00000000000..120fc8a6f98 --- /dev/null +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/ConvertBaseTool.java @@ -0,0 +1,98 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.security.tool.crypto; + +import com.yahoo.security.Base58; +import com.yahoo.security.Base62; +import com.yahoo.vespa.security.tool.CliUtils; +import com.yahoo.vespa.security.tool.Tool; +import com.yahoo.vespa.security.tool.ToolDescription; +import com.yahoo.vespa.security.tool.ToolInvocation; +import org.apache.commons.cli.Option; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Base64; +import java.util.List; + +import static com.yahoo.security.ArrayUtils.fromUtf8Bytes; +import static com.yahoo.security.ArrayUtils.hex; +import static com.yahoo.security.ArrayUtils.unhex; + +/** + * Simple tool to convert between different Base N encodings, for a fixed set of N. + * + * @author vekterli + */ +public class ConvertBaseTool implements Tool { + + private static final int MAX_IN_BYTES = 1024; + + static final String FROM_OPTION = "from"; + static final String TO_OPTION = "to"; + + private static final List<Option> OPTIONS = List.of( + Option.builder("f") + .longOpt(FROM_OPTION) + .hasArg(true) + .required(false) + .desc("From base. Supported values: 16, 58, 62, 64") + .build(), + Option.builder("t") + .longOpt(TO_OPTION) + .hasArg(true) + .required(false) + .desc("To base. Supported values: 16, 58, 62, 64") + .build()); + + @Override + public String name() { + return "convert-base"; + } + + @Override + public ToolDescription description() { + return new ToolDescription( + "--from <base N> --to <base M>", + ("Reads up to %d bytes of STDIN interpreted as a base N string (ignoring " + + "whitespace) and writes to STDOUT as a base M string. Note that base 64 is " + + "expected to be in (and is output as) the URL-safe alphabet (padding optional " + + "for input, no padding for output).").formatted(MAX_IN_BYTES), + "Note: this is a BETA tool version; its interface may be changed at any time.", + OPTIONS); + } + + @Override + public int invoke(ToolInvocation invocation) { + try { + var arguments = invocation.arguments(); + var fromBase = Integer.parseInt(CliUtils.optionOrThrow(arguments, FROM_OPTION)); + var toBase = Integer.parseInt(CliUtils.optionOrThrow(arguments, TO_OPTION)); + // We cap the input length since non-base(16|64) transforms are O(n^2) and we don't want + // to risk melting someone's CPU by them piping something large into the process by accident. + byte[] inBytes = invocation.stdIn().readAllBytes(); + if (inBytes.length > MAX_IN_BYTES) { + throw new IllegalArgumentException("Input size is too large (%d), max is %d" + .formatted(inBytes.length, MAX_IN_BYTES)); + } + var inString = fromUtf8Bytes(inBytes).strip(); // We ignore whitespace to avoid trailing \n issues + byte[] decoded = switch (fromBase) { + case 16 -> unhex(inString); + case 58 -> Base58.codec().decode(inString); + case 62 -> Base62.codec().decode(inString); + case 64 -> Base64.getUrlDecoder().decode(inString); + default -> throw new IllegalArgumentException("Unsupported from-base: %d".formatted(fromBase)); + }; + String encoded = switch (toBase) { + case 16 -> hex(decoded); + case 58 -> Base58.codec().encode(decoded); + case 62 -> Base62.codec().encode(decoded); + case 64 -> Base64.getUrlEncoder().withoutPadding().encodeToString(decoded); + default -> throw new IllegalArgumentException("Unsupported to-base: %d".formatted(toBase)); + }; + invocation.stdOut().println(encoded); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return 0; + } +} diff --git a/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java b/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java index 8ba24d2cccc..f529ed828ea 100644 --- a/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java +++ b/vespaclient-java/src/test/java/com/yahoo/vespa/security/tool/CryptoToolsTest.java @@ -40,6 +40,12 @@ public class CryptoToolsTest { assertEquals(readTestResource(expectedFile), procOut.stdOut()); } + private void verifyStdoutEquals(List<String> args, String stdIn, String expectedMessage) throws IOException { + var procOut = runMain(args, toUtf8Bytes(stdIn), Map.of()); + assertEquals(0, procOut.exitCode()); + assertEquals(expectedMessage, procOut.stdOut()); + } + private void verifyStderrEquals(List<String> args, String expectedMessage) throws IOException { var procOut = runMain(args, EMPTY_BYTES, Map.of()); assertEquals(1, procOut.exitCode()); // Assume checking stderr is because of a failure. @@ -77,6 +83,11 @@ public class CryptoToolsTest { } @Test + void convert_base_help_printed_if_help_option_given_to_subtool() throws IOException { + verifyStdoutMatchesFile(List.of("convert-base", "--help"), "expected-convert-base-help-output.txt"); + } + + @Test void missing_required_parameter_prints_error_message() throws IOException { // We don't test all possible input arguments to all tools, since it'd be too closely // bound to the order in which the implementation checks for argument presence. @@ -240,6 +251,26 @@ public class CryptoToolsTest { } @Test + void convert_base_reads_stdin_and_prints_conversion_on_stdout() throws IOException { + // Check all possible output encodings + verifyStdoutEquals(List.of("convert-base", "--from", "16", "--to", "16"), "0000287fb4cd", "0000287fb4cd\n"); + verifyStdoutEquals(List.of("convert-base", "--from", "16", "--to", "58"), "0000287fb4cd", "11233QC4\n"); + verifyStdoutEquals(List.of("convert-base", "--from", "16", "--to", "62"), "0000287fb4cd", "00jyw3x\n"); + verifyStdoutEquals(List.of("convert-base", "--from", "16", "--to", "64"), "0000287fb4cd", "AAAof7TN\n"); + + // Check a single output encoding for each input encoding, making the simplifying assumption that + // decoding and encoding is independent. Base 16 already covered above. + verifyStdoutEquals(List.of("convert-base", "--from", "58", "--to", "16"), "11233QC4", "0000287fb4cd\n"); + verifyStdoutEquals(List.of("convert-base", "--from", "62", "--to", "16"), "00jyw3x", "0000287fb4cd\n"); + verifyStdoutEquals(List.of("convert-base", "--from", "64", "--to", "16"), "AAAof7TN", "0000287fb4cd\n"); + } + + @Test + void convert_base_tool_ignores_whitespace_on_stdin() throws IOException { + verifyStdoutEquals(List.of("convert-base", "--from", "16", "--to", "58"), " 0000287fb4cd\n", "11233QC4\n"); + } + + @Test void can_end_to_end_keygen_encrypt_and_decrypt_via_files() throws IOException { String greatSecret = "Dogs can't look up"; diff --git a/vespaclient-java/src/test/resources/expected-convert-base-help-output.txt b/vespaclient-java/src/test/resources/expected-convert-base-help-output.txt new file mode 100644 index 00000000000..8a65c2240e0 --- /dev/null +++ b/vespaclient-java/src/test/resources/expected-convert-base-help-output.txt @@ -0,0 +1,10 @@ +usage: vespa-security convert-base --from <base N> --to <base M> +Reads up to 1024 bytes of STDIN interpreted as a base N string (ignoring +whitespace) and writes to STDOUT as a base M string. Note that base 64 is +expected to be in (and is output as) the URL-safe alphabet (padding +optional for input, no padding for output). + -f,--from <arg> From base. Supported values: 16, 58, 62, 64 + -h,--help Show help + -t,--to <arg> To base. Supported values: 16, 58, 62, 64 +Note: this is a BETA tool version; its interface may be changed at any +time. diff --git a/vespaclient-java/src/test/resources/expected-help-output.txt b/vespaclient-java/src/test/resources/expected-help-output.txt index c973735ad46..8cea1366973 100644 --- a/vespaclient-java/src/test/resources/expected-help-output.txt +++ b/vespaclient-java/src/test/resources/expected-help-output.txt @@ -1,4 +1,4 @@ usage: vespa-security <tool> [TOOL OPTIONS] -Where <tool> is one of: keygen, encrypt, decrypt, token-info +Where <tool> is one of: keygen, encrypt, decrypt, token-info, convert-base -h,--help Show help Invoke vespa-security <tool> --help for tool-specific help |