From 4019af58e9c668a1ba1b9ad2842024942e15b399 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Thu, 20 Aug 2026 21:19:16 +0900 Subject: [PATCH] Serialization and Ed25519 (#2) finally... Reviewed-on: https://git.qpq.swiss/QPQ-AG/gm-java/pulls/2 Co-authored-by: Craig Everett Co-committed-by: Craig Everett --- .gitignore | 3 + README.md | 26 + {gajumaru-core/bin => bin}/compile | 5 +- {gajumaru-core/bin => bin}/run | 0 gajumaru-core/src/Testinator.java | 172 ---- gajumaru-core/test/README.md | 24 - gajumaru-core/test/src/gmt.erl | 328 ------- .../qpq/gajumaru/core/crypto/Blake2b.java | 156 ++++ .../qpq/gajumaru/core/crypto/Ed25519.java | 849 ++++++++++++++++++ .../qpq/gajumaru/core/crypto/Keccak256.java | 172 ++++ .../java/swiss/qpq/gajumaru/core/data/Id.java | 107 +++ .../gajumaru/core/encoding/ApiEncoder.java | 93 ++ .../qpq/gajumaru/core/encoding/Base58.java | 1 + .../swiss/qpq/gajumaru/core/encoding/RLP.java | 8 - .../gajumaru/core/formatting/GajuFormat.java | 0 .../qpq/gajumaru/core/tools/CryptoUtils.java | 62 ++ {gajumaru-core/test => test}/.gitignore | 2 + {gajumaru-core/test => test}/Emakefile | 0 {gajumaru-core/test => test}/LICENSE | 0 test/README.md | 53 ++ test/Testinator.java | 416 +++++++++ .../test => test}/ebin/gm_libtester.app | 0 test/src/gmt.erl | 588 ++++++++++++ {gajumaru-core/test => test}/zomp.meta | 3 +- 24 files changed, 2532 insertions(+), 536 deletions(-) rename {gajumaru-core/bin => bin}/compile (86%) rename {gajumaru-core/bin => bin}/run (100%) delete mode 100644 gajumaru-core/src/Testinator.java delete mode 100644 gajumaru-core/test/README.md delete mode 100644 gajumaru-core/test/src/gmt.erl create mode 100644 src/main/java/swiss/qpq/gajumaru/core/crypto/Blake2b.java create mode 100644 src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java create mode 100644 src/main/java/swiss/qpq/gajumaru/core/crypto/Keccak256.java create mode 100644 src/main/java/swiss/qpq/gajumaru/core/data/Id.java create mode 100644 src/main/java/swiss/qpq/gajumaru/core/encoding/ApiEncoder.java rename {gajumaru-core/src => src}/main/java/swiss/qpq/gajumaru/core/encoding/Base58.java (99%) rename {gajumaru-core/src => src}/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java (95%) rename {gajumaru-core/src => src}/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java (100%) create mode 100644 src/main/java/swiss/qpq/gajumaru/core/tools/CryptoUtils.java rename {gajumaru-core/test => test}/.gitignore (93%) rename {gajumaru-core/test => test}/Emakefile (100%) rename {gajumaru-core/test => test}/LICENSE (100%) create mode 100644 test/README.md create mode 100644 test/Testinator.java rename {gajumaru-core/test => test}/ebin/gm_libtester.app (100%) create mode 100644 test/src/gmt.erl rename {gajumaru-core/test => test}/zomp.meta (92%) diff --git a/.gitignore b/.gitignore index 0b20c35..340330e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,12 @@ Thumbs.db .netrwhist .nvimlog .idea/ +.artifacts/ *.iml .gradle/ local.properties Captures/ .externalNativeBuild/ temp +erl_crash.dump +*.class diff --git a/README.md b/README.md index 707af5f..eb370f1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,32 @@ Java libraries for the Gajumaru + +## Purpose + +gm-java provides the basic functionality required to interact with the [Gajumaru](https://gajumaru./io) +blockchain system. This includes serialization, function wrappers for Gajumaru node endpoint calls, +handling of GRIDS URLs, formatting of monetary values, any cryptographic functions not covered in +standard Java libraries, and some network functionality necessary to interact smoothly with +Gajumaru chains (Groot and AC networks). + +This codebase may *not* include references to third-party libraries, may *not* permit un-zeroed +byte values to be left on the heap until the JVM eventually decides to garbage collect them, +and may *not* build up a big, gnarly, inheritance-heavy OOPsy object hierarchy. + +Classes should generally be structured as `final` classes of functions that operate over data +that can be expressed as Java primitives, most commonly `byte[]` and some form of `int` or +occasionally `BigInteger`. + +`BigInteger` is not permitted in cryptographic operations, as it leaves potentially sensitive +artifacts scattered all throughout the heap and can occasionally make the garbage collector +go bananas when performing heavy math operations. `BigInteger` is excellent, however, for things +like representing currency values in Pucks. + +All forms of floating-point arithmetic are forbidden in all currency calculations. The Gajumaru +does not have floats, and they do not exist at all in the Sophia language. + + ## Terms Copyright (c) 2026 QPQ AG . All rights reserved. diff --git a/gajumaru-core/bin/compile b/bin/compile similarity index 86% rename from gajumaru-core/bin/compile rename to bin/compile index 833bf63..ccd245c 100755 --- a/gajumaru-core/bin/compile +++ b/bin/compile @@ -14,11 +14,10 @@ rm -f "$project_dir/Testinator.class" # Build every .java file find "$project_dir/src/main/java" -name "*.java" | xargs javac \ - -source 21 \ - -target 21 \ + --release 21 \ -d "$project_dir/build/classes" # Build the Testinator thingy -javac -cp "build/classes" -d build src/Testinator.java +javac -cp "build/classes" -d build test/Testinator.java echo "Bytecode in $project_dir/build/classes/" diff --git a/gajumaru-core/bin/run b/bin/run similarity index 100% rename from gajumaru-core/bin/run rename to bin/run diff --git a/gajumaru-core/src/Testinator.java b/gajumaru-core/src/Testinator.java deleted file mode 100644 index e01796a..0000000 --- a/gajumaru-core/src/Testinator.java +++ /dev/null @@ -1,172 +0,0 @@ -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Base64; -import java.util.ArrayList; -import java.util.List; -import java.math.BigInteger; - -import swiss.qpq.gajumaru.core.encoding.Base58; -import swiss.qpq.gajumaru.core.encoding.RLP; -import swiss.qpq.gajumaru.core.formatting.GajuFormat; - -public class Testinator { - public static void main(String[] args) { - if (args.length != 2) { - System.out.println("Error: Provide the test suite name and the working dir path."); - System.exit(1); - } - - try { - switch (args[0]) { - case "base64" -> { - System.out.print(base64(args[1])); - } - case "base58" -> { - System.out.print(base58(args[1])); - } - case "base58_check" -> { - System.out.print(base58_check(args[1])); - } - case "rlp" -> { - System.out.print(rlp(args[1])); - } - case "rlp_stream" -> { - System.out.print(rlp_stream(args[1])); - } - case "rlp_fail" -> { - System.out.print(rlp_fail(args[1])); - } - case "gaju_format" -> { - System.out.print(gaju_format(args[1])); - } - } - } catch (Exception e) { - System.err.println("Error: " + e.getMessage()); - System.exit(1); - } - } - - private static String base64(String workingPath) throws IOException { - Path testPath = Path.of(workingPath, "base64.test"); - Path encPath = Path.of(workingPath, "base64.java.txt"); - Path decPath = Path.of(workingPath, "base64.java.back"); - byte[] rawB = Files.readAllBytes(testPath); - byte[] encB = Base64.getEncoder().encode(rawB); - Files.write(encPath, encB); - byte[] readE = Files.readAllBytes(encPath); - byte[] decB = Base64.getDecoder().decode(readE); - Files.write(decPath, decB); - return encPath.toString() + " " + decPath.toString(); - } - - private static String base58(String workingPath) throws IOException { - Path testPath = Path.of(workingPath, "base58.test"); - Path encPath = Path.of(workingPath, "base58.java.txt"); - Path decPath = Path.of(workingPath, "base58.java.back"); - byte[] rawB = Files.readAllBytes(testPath); - String encS = Base58.encode(rawB); - Files.write(encPath, encS.getBytes()); - byte[] readE = Files.readAllBytes(encPath); - String readS = new String(readE); - byte[] decB = Base58.decode(readS); - Files.write(decPath, decB); - return encPath.toString() + " " + decPath.toString(); - } - - private static String base58_check(String workingPath) throws IOException { - Path testPath = Path.of(workingPath, "base58_check.test"); - Path encPath = Path.of(workingPath, "base58_check.java.txt"); - Path decPath = Path.of(workingPath, "base58_check.java.back"); - byte[] rawB = Files.readAllBytes(testPath); - String encS = Base58.checkEncode(rawB); - Files.write(encPath, encS.getBytes()); - byte[] readE = Files.readAllBytes(encPath); - String readS = new String(readE); - byte[] decB = Base58.checkDecode(readS); - Files.write(decPath, decB); - return encPath.toString() + " " + decPath.toString(); - } - - private static String rlp(String workingPath) throws IOException { - Path testPath = Path.of(workingPath, "rlp.test"); - Path resPath = Path.of(workingPath, "rlp.java.back"); - byte[] encB = Files.readAllBytes(testPath); - - RLP.RLP_Data decRLP = RLP.decode(encB); - - RLP.RLP_List root = decRLP.asList(); - RLP.RLP_List sub = root.getItems().get(1).asList(); - byte[] anchor = sub.getItems().get(1).asItem().getBytes(); - - byte[] reEnc = RLP.encode(decRLP); - Files.write(resPath, reEnc); - - return new String(anchor) + "|" + resPath.toString(); - } - - private static String rlp_stream(String workingPath) throws IOException { - Path testPath = Path.of(workingPath, "rlp_stream.test"); - byte[] buffer = Files.readAllBytes(testPath); - - List hashes = new ArrayList<>(); - int offset = 0; - while (offset < buffer.length) { - RLP.DecodeResult res = RLP.decodeFrom(buffer, offset); - hashes.add(Integer.toString(res.data().hashCode())); // Just to verify we got objects - offset += res.consumed(); - } - return Integer.toString(hashes.size()); - } - - private static String rlp_fail(String workingPath) throws IOException { - Path testPath = Path.of(workingPath, "rlp_fail.test"); - byte[] buffer = Files.readAllBytes(testPath); - try { - RLP.decode(buffer); - return "SUCCESS"; - } catch (RLP.RLPException e) { - return "RLP_EXCEPTION: " + e.getMessage(); - } catch (Exception e) { - return "OTHER_EXCEPTION: " + e.getClass().getSimpleName(); - } - } - - private static String gaju_format(String workingPath) throws IOException { - Path testPath = Path.of(workingPath, "gaju_format.test"); - Path resPath = Path.of(workingPath, "gaju_format.java.txt"); - List lines = Files.readAllLines(testPath); - List results = new ArrayList<>(); - - for (String line : lines) { - if (line.trim().isEmpty()) continue; - String[] p = line.split("\\|"); - String op = p[0]; - switch (op) { - case "amount" -> { - GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]); - GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]); - char sep = p[3].charAt(0); - int span = Integer.parseInt(p[4]); - BigInteger val = new BigInteger(p[5]); - results.add(GajuFormat.amount(new GajuFormat.FormatSpec(style, unit, sep, span), val)); - } - case "approx" -> { - GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]); - GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]); - char sep = p[3].charAt(0); - int span = Integer.parseInt(p[4]); - BigInteger val = new BigInteger(p[5]); - int prec = Integer.parseInt(p[6]); - results.add(GajuFormat.approxAmount(new GajuFormat.FormatSpec(style, unit, sep, span), val, prec)); - } - case "read" -> { - byte[] b = GajuFormat.read(p[1]); - results.add(new BigInteger(b).toString()); - } - } - } - Files.write(resPath, results); - return resPath.toString(); - } -} diff --git a/gajumaru-core/test/README.md b/gajumaru-core/test/README.md deleted file mode 100644 index 6bfd9e1..0000000 --- a/gajumaru-core/test/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# GM Java <-> Erlang Tests - -The core Java libraries are all written as transform functions over data. -There is little point, therefore, in obsessing over "unit tests" and "regression testing" of Java code in Java when we have cannonical code in Erlang. - -The purpose of this utility is to instead test the Java libraries agains the Erlang libraries directly. - - -## How to run - -In the current directory simply run `zx runlocal` and the report map will be printed to the screen and random test data will be in the `temp/` directory. - -To run a specific test suite run `zx runlocal [module names]`. - -To list module names, run `zx runlocal list`. - - -## Where things are - -The test cases are all generated in gmt.erl, generally based on randomized but valid data that are fed to reference Erlang implementations. -The input to and output from each test is recorded to disk in the `temp/` dir. -The inputs are then read in by equivalent Java functions and the output is then compared. - -For a test case to pass the outputs must be identical. diff --git a/gajumaru-core/test/src/gmt.erl b/gajumaru-core/test/src/gmt.erl deleted file mode 100644 index f0f4155..0000000 --- a/gajumaru-core/test/src/gmt.erl +++ /dev/null @@ -1,328 +0,0 @@ -%%% @doc -%%% Gajumaru Java Lib Tester: gmt -%%% -%%% This module provides an inter-language test suite for the Gajumaru core -%%% Java libraries. It tests the Java implementation against the canonical -%%% Erlang implementation by generating random test vectors and comparing -%%% the outputs. -%%% @end - --module(gmt). --vsn("0.1.0"). --author("Craig Everett "). --copyright("Craig Everett "). --license("LGPL-3.0-or-later"). - --export([start/1]). - - -%%% Logic - -mods() -> - #{"base64" => fun base64/0, - "base58" => fun base58/0, - "base58_check" => fun base58_check/0, - "rlp" => fun rlp/0, - "rlp_stream" => fun rlp_stream/0, - "rlp_fail" => fun rlp_fail/0, - "gaju_format" => fun gaju_format/0}. - - -start([]) -> - Tests = mods(), - ok = run(Tests), - zx:silent_stop(); -start(["list"]) -> - ok = io:format("Available tests:~n"), - ok = lists:foreach(fun display/1, maps:keys(mods())), - zx:silent_stop(); -start(Mods) -> - Available = mods(), - Tests = maps:with(Mods, Available), - ok = - case maps:size(Tests) =:= length(Mods) of - true -> - run(Tests); - false -> - NotMods = lists:subtract(Mods, maps:keys(Available)), - ok = io:format("The following arguments are not testable module names:~n"), - lists:foreach(fun display/1, NotMods) - end, - zx:silent_stop(). - -display(Name) -> - io:format(" ~ts~n", [Name]). - -run(Tests) -> - {ok, Cwd} = file:get_cwd(), - ok = - case filename:basename(Cwd) of - "test" -> file:set_cwd(".."); - _ -> ok - end, - ok = clean(), - ok = build(), - Results = maps:map(fun run/2, Tests), - io:format("Results:~n ~tp~n", [Results]). - -run(Name, Test) -> - ok = io:format("~nRunning: ~ts...~n", [Name]), - Test(). - -clean() -> - Temp = "test/temp", - lists:foreach(fun(D) -> ok = clean(D) end, [Temp]). - -clean(Dir) -> - case file:del_dir_r(Dir) of - ok -> ok; - {error, enoent} -> ok; - Error -> Error - end. - -build() -> - Out = os:cmd("bin/compile"), - io:format("Compile: ~ts", [Out]). - -temp_dir() -> - {ok, Cwd} = file:get_cwd(), - filename:join(Cwd, "test/temp"). - -trim(S) -> - Unprintable = fun(C) -> C =< 32 end, - lists:reverse(lists:dropwhile(Unprintable, lists:reverse(lists:dropwhile(Unprintable, S)))). - - - -%%% Test Modules - -base64() -> - Temp = temp_dir(), - TestFile = filename:join(Temp, "base64.test"), - ConvFile = filename:join(Temp, "base64.erlang.txt"), - ok = filelib:ensure_dir(TestFile), - ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))), - {ok, B} = file:read_file(TestFile), - Base64 = base64:encode(B), - ok = file:write_file(ConvFile, Base64), - Run = "bin/run base64 " ++ Temp, - Out = trim(os:cmd(Run)), - [JEnc, JDec] = string:split(Out, " "), - {ok, EEncB} = file:read_file(ConvFile), - {ok, JEncB} = file:read_file(JEnc), - EHash = crypto:hash(sha512, EEncB), - JHash = crypto:hash(sha512, JEncB), - {ok, EDecB} = file:read_file(TestFile), - {ok, JDecB} = file:read_file(JDec), - EBinHash = crypto:hash(sha512, EDecB), - JBinHash = crypto:hash(sha512, JDecB), - EHash =:= JHash andalso EBinHash =:= JBinHash. - - -base58() -> - Temp = temp_dir(), - TestFile = filename:join(Temp, "base58.test"), - ConvFile = filename:join(Temp, "base58.erlang.txt"), - ok = filelib:ensure_dir(TestFile), - ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))), - {ok, B} = file:read_file(TestFile), - Base58 = base58:binary_to_base58(B), - ok = file:write_file(ConvFile, Base58), - Run = "bin/run base58 " ++ Temp, - Out = trim(os:cmd(Run)), - [JEnc, JDec] = string:split(Out, " "), - {ok, EEncB} = file:read_file(ConvFile), - {ok, JEncB} = file:read_file(JEnc), - EHash = crypto:hash(sha512, EEncB), - JHash = crypto:hash(sha512, JEncB), - {ok, EDecB} = file:read_file(TestFile), - {ok, JDecB} = file:read_file(JDec), - EBinHash = crypto:hash(sha512, EDecB), - JBinHash = crypto:hash(sha512, JDecB), - EHash =:= JHash andalso EBinHash =:= JBinHash. - - -base58_check() -> - Temp = temp_dir(), - TestFile = filename:join(Temp, "base58_check.test"), - ConvFile = filename:join(Temp, "base58_check.erlang.txt"), - ok = filelib:ensure_dir(TestFile), - ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))), - {ok, B} = file:read_file(TestFile), - Checksum = binary:part(crypto:hash(sha256, crypto:hash(sha256, B)), 0, 4), - Base58C = base58:binary_to_base58(<>), - ok = file:write_file(ConvFile, Base58C), - Run = "bin/run base58_check " ++ Temp, - Out = trim(os:cmd(Run)), - [JEnc, JDec] = string:split(Out, " "), - {ok, EEncB} = file:read_file(ConvFile), - {ok, JEncB} = file:read_file(JEnc), - EHash = crypto:hash(sha512, EEncB), - JHash = crypto:hash(sha512, JEncB), - {ok, EDecB} = file:read_file(TestFile), - {ok, JDecB} = file:read_file(JDec), - EBinHash = crypto:hash(sha512, EDecB), - JBinHash = crypto:hash(sha512, JDecB), - EHash =:= JHash andalso EBinHash =:= JBinHash. - - -rlp() -> - Temp = temp_dir(), - Anchor = <<"I thought what I'd do was, I'd pretend I was one of those deaf-mutes.">>, - Data = - [rand:bytes(rand:uniform(20)), - [rand:bytes(rand:uniform(20)), - Anchor, - rand:bytes(rand:uniform(20)), - rand:bytes(rand:uniform(5000))], - rand:bytes(rand:uniform(2000))], - RLP = gmser_rlp:encode(Data), - RLP_File = filename:join(Temp, "rlp.test"), - ok = filelib:ensure_dir(RLP_File), - ok = file:write_file(RLP_File, RLP), - Run = "bin/run rlp " ++ Temp, - Out = trim(os:cmd(Run)), - case string:split(Out, "|") of - [Found, JPath] -> - JPathTrimmed = trim(JPath), - {ok, EEncB} = file:read_file(RLP_File), - case file:read_file(JPathTrimmed) of - {ok, JEncB} -> - EHash = crypto:hash(sha512, EEncB), - JHash = crypto:hash(sha512, JEncB), - EHash =:= JHash andalso Found =:= unicode:characters_to_list(Anchor); - {error, R} -> - io:format("Failed to read RLP Java result: ~tp (Path: ~tp)~n", [R, JPathTrimmed]), - false - end; - _ -> - io:format("RLP output mismatch: ~tp~n", [Out]), - false - end. - - -rlp_stream() -> - Temp = temp_dir(), - TestFile = filename:join(Temp, "rlp_stream.test"), - ok = filelib:ensure_dir(TestFile), - Data = [rand:bytes(rand:uniform(100)) || _ <- lists:seq(1, 10)], - RLP = << <<(gmser_rlp:encode(D))/binary>> || D <- Data >>, - ok = file:write_file(TestFile, RLP), - Run = "bin/run rlp_stream " ++ Temp, - Out = trim(os:cmd(Run)), - Out =:= "10". - - -rlp_fail() -> - Temp = temp_dir(), - TestFile = filename:join(Temp, "rlp_fail.test"), - ok = filelib:ensure_dir(TestFile), - Data = [<<"item1">>, <<"item2">>], - FullRLP = gmser_rlp:encode(Data), - TruncRLP = binary:part(FullRLP, 0, byte_size(FullRLP) - 2), - ok = file:write_file(TestFile, TruncRLP), - Run = "bin/run rlp_fail " ++ Temp, - Out = trim(os:cmd(Run)), - string:prefix(Out, "RLP_EXCEPTION:") =/= nomatch. - - -gaju_format() -> - Temp = temp_dir(), - TestFile = filename:join(Temp, "gaju_format.test"), - ok = filelib:ensure_dir(TestFile), - Cases = [gen_case() || _ <- lists:seq(1, 100)], - Lines = [serialize_case(C) || C <- Cases], - ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])), - Run = "bin/run gaju_format " ++ Temp, - RawOut = os:cmd(Run), - JavaResPath = trim(RawOut), - case file:read_file(JavaResPath) of - {ok, ResContent} -> - JavaResults = string:split(trim(unicode:characters_to_list(ResContent)), "\n", all), - length(Cases) =:= length(JavaResults) andalso compare_results(Cases, JavaResults); - {error, Reason} -> - Format = "Failed to read Java results from: ~tp (Reason: ~tp)~nRaw Output: ~ts~n", - io:format(Format, [JavaResPath, Reason, RawOut]), - false - end. - - - -%%% Generatorators - -gen_case() -> - Ops = [amount, approx, read], - Op = lists:nth(rand:uniform(length(Ops)), Ops), - gen_case(Op). - -gen_case(read) -> - Pucks = random_pucks(12), - Style = random_style(), - {read, hz_format:amount(gaju, Style, Pucks), Pucks}; -gen_case(amount) -> - Unit = random_unit(), - Pucks = - case Unit of - gaju -> random_pucks(12); - puck -> random_pucks(8) - end, - Style = random_style(), - Sep = lists:nth(rand:uniform(2), [$,, $_]), - Span = rand:uniform(4), - Amount = hz_format:amount(Unit, hz_style(Style, Sep, Span), Pucks), - {amount, Style, Unit, Sep, Span, Pucks, Amount}; -gen_case(approx) -> - Pucks = random_pucks(12), - Sep = lists:nth(rand:uniform(2), [$,, $_]), - Span = rand:uniform(2) + 2, - Prec = rand:uniform(18), - Approx = hz_format:approx_amount({Sep, Span}, Prec, Pucks), - {approx, us, gaju, Sep, Span, Pucks, Prec, Approx}. - -random_pucks(MaxBytes) -> - Bytes = rand:bytes(rand:uniform(MaxBytes)), - crypto:bytes_to_integer(Bytes). - -random_style() -> - lists:nth(rand:uniform(4), [us, jp, metric, legacy]). - -random_unit() -> - lists:nth(rand:uniform(2), [gaju, puck]). - -hz_style(us, Sep, Span) -> {Sep, Span}; -hz_style(Style, _, _) -> Style. - -serialize_case({amount, Style, Unit, Sep, Span, Pucks, _}) -> - FStyle = string:uppercase(atom_to_list(Style)), - FUnit = string:uppercase(atom_to_list(Unit)), - Stuff = [FStyle, FUnit, Sep, Span, Pucks], - io_lib:format("amount|~ts|~ts|~c|~b|~b", Stuff); -serialize_case({approx, Style, Unit, Sep, Span, Pucks, Prec, _}) -> - FStyle = string:uppercase(atom_to_list(Style)), - FUnit = string:uppercase(atom_to_list(Unit)), - Stuff = [FStyle, FUnit, Sep, Span, Pucks, Prec], - io_lib:format("approx|~ts|~ts|~c|~b|~b|~b", Stuff); -serialize_case({read, Input, _}) -> - InputStr = unicode:characters_to_list(Input), - io_lib:format("read|~ts", [InputStr]). - -compare_results([], []) -> - true; -compare_results([Case | Cases], [Result | Results]) -> - case check_case(Case, Result) of - true -> - compare_results(Cases, Results); - false -> - io:format("Parity failure!~nCase: ~tp~nJava Result: ~tp~n", [Case, Result]), - false - end. - -check_case({amount, _, _, _, _, _, Expected}, Result) -> - flatten(Expected) =:= flatten(Result); -check_case({approx, _, _, _, _, _, _, Expected}, Result) -> - flatten(Expected) =:= flatten(Result); -check_case({read, _, Expected}, Result) -> - integer_to_list(Expected) =:= flatten(Result). - -flatten(B) when is_binary(B) -> unicode:characters_to_list(B); -flatten(L) when is_list(L) -> unicode:characters_to_list(L). diff --git a/src/main/java/swiss/qpq/gajumaru/core/crypto/Blake2b.java b/src/main/java/swiss/qpq/gajumaru/core/crypto/Blake2b.java new file mode 100644 index 0000000..330b73f --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/crypto/Blake2b.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * This program is dual-licensed: + * 1) Under the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version (AGPL-3.0-or-later). + * + * 2) Under a commercial/proprietary license available directly from QPQ AG. + * If you wish to use this software outside the strict constraints of the + * AGPLv3 (e.g. within a closed-source or proprietary product), you must + * purchase a commercial license from QPQ AG. + * + * Authors: + * - Craig Everett + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.crypto; + +import java.util.Arrays; +import swiss.qpq.gajumaru.core.tools.CryptoUtils; + + +// Pure Java implementation of BLAKE2b. +// Based on eblake2.erl. + +public final class Blake2b { + + private static final long[] IV = { + 0x6a09e667f3bcc908L, 0xbb67ae8584caa73bL, 0x3c6ef372fe94f82bL, 0xa54ff53a5f1d36f1L, + 0x510e527fade682d1L, 0x9b05688c2b3e6c1fL, 0x1f83d9abfb41bd6bL, 0x5be0cd19137e2179L + }; + + private static final byte[][] SIGMA = { + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 }, + { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 }, + { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 }, + { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 }, + { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 }, + { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 }, + { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 }, + { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 }, + { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, // Round 10: sigma[0] + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 } // Round 11: sigma[1] + }; + + private Blake2b() {} + + public static byte[] hash(byte[] data) { + return hash(data, 32); + } + + public static byte[] hash(byte[] data, int hashLen) { + long[] h = Arrays.copyOf(IV, 8); + h[0] ^= 0x01010000L ^ hashLen; + + long t0 = 0; + long t1 = 0; + + int offset = 0; + while (offset + 128 < data.length) { + t0 += 128; + if (t0 < 128) t1++; // Overflow + compress(h, data, offset, t0, t1, false); + offset += 128; + } + + t0 += (data.length - offset); + if (t0 < (data.length - offset)) t1++; + + byte[] lastBlock = new byte[128]; + System.arraycopy(data, offset, lastBlock, 0, data.length - offset); + compress(h, lastBlock, 0, t0, t1, true); + + byte[] fullHash = new byte[64]; + for (int i = 0; i < 8; i++) { + writeLongLittleEndian(h[i], fullHash, i * 8); + } + + byte[] result = Arrays.copyOf(fullHash, hashLen); + + // Memory hygiene + Arrays.fill(h, 0L); + Arrays.fill(fullHash, (byte) 0); + Arrays.fill(lastBlock, (byte) 0); + + return result; + } + + private static void compress(long[] h, byte[] block, int offset, long t0, long t1, boolean isLast) { + long[] m = new long[16]; + for (int i = 0; i < 16; i++) { + m[i] = readLongLittleEndian(block, offset + i * 8); + } + + long[] v = new long[16]; + System.arraycopy( h, 0, v, 0, 8); + System.arraycopy(IV, 0, v, 8, 8); + + v[12] ^= t0; + v[13] ^= t1; + if (isLast) { + v[14] ^= 0xffffffffffffffffL; + } + + for (int round = 0; round < 12; round++) { + byte[] s = SIGMA[round]; + g(v, 0, 4, 8, 12, m[s[0]], m[s[1]]); + g(v, 1, 5, 9, 13, m[s[2]], m[s[3]]); + g(v, 2, 6, 10, 14, m[s[4]], m[s[5]]); + g(v, 3, 7, 11, 15, m[s[6]], m[s[7]]); + g(v, 0, 5, 10, 15, m[s[8]], m[s[9]]); + g(v, 1, 6, 11, 12, m[s[10]], m[s[11]]); + g(v, 2, 7, 8, 13, m[s[12]], m[s[13]]); + g(v, 3, 4, 9, 14, m[s[14]], m[s[15]]); + } + + for (int i = 0; i < 8; i++) { + h[i] ^= v[i] ^ v[i + 8]; + } + + // Wiping local arrays + Arrays.fill(m, 0L); + Arrays.fill(v, 0L); + } + + private static void g(long[] v, int a, int b, int c, int d, long x, long y) { + v[a] = v[a] + v[b] + x; + v[d] = Long.rotateRight(v[d] ^ v[a], 32); + v[c] = v[c] + v[d]; + v[b] = Long.rotateRight(v[b] ^ v[c], 24); + v[a] = v[a] + v[b] + y; + v[d] = Long.rotateRight(v[d] ^ v[a], 16); + v[c] = v[c] + v[d]; + v[b] = Long.rotateRight(v[b] ^ v[c], 63); + } + + private static long readLongLittleEndian(byte[] b, int offset) { + long res = 0; + for (int i = 0; i < 8; i++) { + res |= ((long) (b[offset + i] & 0xFF)) << (i * 8); + } + return res; + } + + private static void writeLongLittleEndian(long v, byte[] b, int offset) { + for (int i = 0; i < 8; i++) { + b[offset + i] = (byte) ((v >>> (i * 8)) & 0xFF); + } + } +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java b/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java new file mode 100644 index 0000000..2f7f0fe --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java @@ -0,0 +1,849 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * This program is dual-licensed: + * 1) Under the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version (AGPL-3.0-or-later). + * + * 2) Under a commercial/proprietary license available directly from QPQ AG. + * If you wish to use this software outside the strict constraints of the + * AGPLv3 (e.g. within a closed-source or proprietary product), you must + * purchase a commercial license from QPQ AG. + * + * Authors: + * - Craig Everett + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.crypto; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import swiss.qpq.gajumaru.core.tools.CryptoUtils; + + +// Pure Java implementation of Ed25519 curve arithmetic. +// Uses high-performance Radix-2^25.5 field arithmetic. +// Verified against the canonical Erlang ec_utils. + +public final class Ed25519 { + + // Precomputed limbs for Ed25519 constants (Radix 2^25.5) + private static final long[] BX = {52811034L, 25909283L, 16144682L, 17082669L, 27570973L, 30858332L, 40966398L, 8378388L, 20764389L, 8758491L}; + private static final long[] BY = {40265304L, 26843545L, 13421772L, 20132659L, 26843545L, 6710886L, 53687091L, 13421772L, 40265318L, 26843545L}; + private static final long[] D = {56195235L, 13857412L, 51736253L, 6949390L, 114729L, 24766616L, 60832955L, 30306712L, 48412415L, 21499315L}; + private static final long[] D2 = {45281625L, 27714825L, 36363642L, 13898781L, 229458L, 15978800L, 54557047L, 27058993L, 29715967L, 9444199L}; + private static final long[] I = {34513072L, 25610706L, 9377949L, 3500415L, 12389472L, 33281959L, 41962654L, 31548777L, 326685L, 11406482L}; + + public static final class Ge { + public final long[] X = new long[10], Y = new long[10], Z = new long[10], T = new long[10]; + + public void wipe() { + CryptoUtils.wipe(X); + CryptoUtils.wipe(Y); + CryptoUtils.wipe(Z); + CryptoUtils.wipe(T); + } + } + + public static final class Scratch { + public final long[] + a = new long[10], b = new long[10], c = new long[10], d = new long[10], + e = new long[10], f = new long[10], g = new long[10], h = new long[10], + tmp = new long[10], t19 = new long[19]; + public final long[][] stack = new long[10][10]; + public final Ge geTmp = new Ge(); + + public void wipe() { + CryptoUtils.wipe(a); + CryptoUtils.wipe(b); + CryptoUtils.wipe(c); + CryptoUtils.wipe(d); + CryptoUtils.wipe(e); + CryptoUtils.wipe(f); + CryptoUtils.wipe(g); + CryptoUtils.wipe(h); + CryptoUtils.wipe(tmp); + CryptoUtils.wipe(t19); + for (long[] s : stack) CryptoUtils.wipe(s); + geTmp.wipe(); + } + } + + private Ed25519() { + } + + public static byte[] publicKey(byte[] seed) { + byte[] hash = sha512(seed); + byte[] s = Arrays.copyOfRange(hash, 0, 32); + s[0] &= 248; + s[31] &= 127; + s[31] |= 64; + Scratch sc = new Scratch(); + Ge A_point = scalarMulBase(s, sc); + byte[] A = compress(A_point, sc); + A_point.wipe(); + sc.wipe(); + CryptoUtils.wipe(hash); + CryptoUtils.wipe(s); + return A; + } + + public static byte[] sign(byte[] seed, byte[] message) { + byte[] hash = sha512(seed); + byte[] s = Arrays.copyOfRange(hash, 0, 32); + s[0] &= 248; + s[31] &= 127; + s[31] |= 64; + byte[] prefix = Arrays.copyOfRange(hash, 32, 64); + byte[] rHash = sha512(prefix, message); + reduceScalar(rHash); + byte[] r = Arrays.copyOfRange(rHash, 0, 32); + Scratch sc = new Scratch(); + Ge R_point = scalarMulBase(r, sc); + byte[] R = compress(R_point, sc); + byte[] A = publicKey(seed); + byte[] kHash = sha512(R, A, message); + reduceScalar(kHash); + byte[] k = Arrays.copyOfRange(kHash, 0, 32); + byte[] S = new byte[32]; + scalarMulAdd(S, k, s, r); + byte[] sig = new byte[64]; + System.arraycopy(R, 0, sig, 0, 32); + System.arraycopy(S, 0, sig, 32, 32); + R_point.wipe(); + sc.wipe(); + CryptoUtils.wipe(hash); + CryptoUtils.wipe(s); + CryptoUtils.wipe(prefix); + CryptoUtils.wipe(rHash); + CryptoUtils.wipe(r); + CryptoUtils.wipe(kHash); + CryptoUtils.wipe(k); + CryptoUtils.wipe(A); + return sig; + } + + public static boolean verify(byte[] publicKey, byte[] message, byte[] signature) { + if (signature.length != 64) return false; + byte[] R_bytes = Arrays.copyOfRange(signature, 0, 32); + byte[] S_bytes = Arrays.copyOfRange(signature, 32, 64); + if ((S_bytes[31] & 0xe0) != 0) return false; + Scratch sc = new Scratch(); + Ge A = decompress(publicKey, sc); + if (A == null) return false; + Ge R = decompress(R_bytes, sc); + if (R == null) return false; + + byte[] kHash = sha512(R_bytes, publicKey, message); + reduceScalar(kHash); + byte[] k = Arrays.copyOfRange(kHash, 0, 32); + + Ge sB = scalarMulBase(S_bytes, sc); + Ge kA = scalarMul(A, k, sc); + Ge RHS = new Ge(); + ge_add(RHS, R, kA, sc); + + byte[] LHS_bytes = compress(sB, sc); + byte[] RHS_bytes = compress(RHS, sc); + + boolean ok = Arrays.equals(LHS_bytes, RHS_bytes); + + A.wipe(); + R.wipe(); + sB.wipe(); + kA.wipe(); + RHS.wipe(); + sc.wipe(); + CryptoUtils.wipe(kHash); + CryptoUtils.wipe(k); + return ok; + } + + public static Ge scalarMulBase(byte[] scalar, Scratch s) { + Ge res = new Ge(); + fe_0(res.X); + fe_1(res.Y); + fe_1(res.Z); + fe_0(res.T); + Ge q = new Ge(); + fe_copy(q.X, BX); + fe_copy(q.Y, BY); + fe_1(q.Z); + fe_mul(q.T, BX, BY, s.t19); + for (int i = 0; i < 256; i++) { + int bit = ((scalar[i / 8] & 0xFF) >>> (i % 8)) & 1; + ge_add(s.geTmp, res, q, s); + ge_cmov(res, s.geTmp, bit); + ge_double(q, q, s); + } + q.wipe(); + return res; + } + + public static void ge_double_scalarmul_vartime(Ge r, byte[] s, Ge a, byte[] k, Scratch sc) { + Ge sB = scalarMulBase(s, sc); + Ge kA = scalarMul(a, k, sc); + fe_neg(kA.X, kA.X); + fe_neg(kA.T, kA.T); + ge_add(r, sB, kA, sc); + sB.wipe(); + kA.wipe(); + } + + public static Ge scalarMul(Ge p, byte[] scalar, Scratch s) { + Ge res = new Ge(); + fe_0(res.X); + fe_1(res.Y); + fe_1(res.Z); + fe_0(res.T); + Ge q = new Ge(); + fe_copy(q.X, p.X); + fe_copy(q.Y, p.Y); + fe_copy(q.Z, p.Z); + fe_copy(q.T, p.T); + for (int i = 0; i < 256; i++) { + int bit = ((scalar[i / 8] & 0xFF) >>> (i % 8)) & 1; + ge_add(s.geTmp, res, q, s); + ge_cmov(res, s.geTmp, bit); + ge_double(q, q, s); + } + q.wipe(); + return res; + } + + public static void ge_add(Ge r, Ge p1, Ge p2, Scratch s) { + long[] yMinusX1 = s.stack[0], yPlusX1 = s.stack[1], yMinusX2 = s.stack[2], yPlusX2 = s.stack[3]; + long[] A = s.stack[4], B = s.stack[5], C = s.stack[6], D = s.stack[7]; + long[] E = s.stack[8], F = s.stack[9], G = s.a, H = s.b; + fe_sub(yMinusX1, p1.Y, p1.X); + fe_add(yPlusX1, p1.Y, p1.X); + fe_sub(yMinusX2, p2.Y, p2.X); + fe_add(yPlusX2, p2.Y, p2.X); + fe_mul(A, yMinusX1, yMinusX2, s.t19); + fe_mul(B, yPlusX1, yPlusX2, s.t19); + fe_mul(C, p1.T, p2.T, s.t19); + fe_mul(C, C, D2, s.t19); + fe_mul(D, p1.Z, p2.Z, s.t19); + fe_add(D, D, D); + fe_sub(E, B, A); + fe_sub(F, D, C); + fe_add(G, D, C); + fe_add(H, B, A); + fe_mul(r.X, E, F, s.t19); + fe_mul(r.Y, G, H, s.t19); + fe_mul(r.Z, F, G, s.t19); + fe_mul(r.T, E, H, s.t19); + } + + public static void ge_double(Ge r, Ge p, Scratch s) { + long[] A = s.stack[0], B = s.stack[1], C = s.stack[2], D = s.stack[3]; + long[] E = s.stack[4], F = s.stack[5], G = s.stack[6], H = s.stack[7]; + fe_sq(A, p.X, s.t19); + fe_sq(B, p.Y, s.t19); + fe_sq(C, p.Z, s.t19); + fe_add(C, C, C); + fe_neg(D, A); + fe_add(E, p.X, p.Y); + fe_sq(E, E, s.t19); + fe_sub(E, E, A); + fe_sub(E, E, B); + fe_add(G, D, B); + fe_sub(F, G, C); + fe_sub(H, D, B); + fe_mul(r.X, E, F, s.t19); + fe_mul(r.Y, G, H, s.t19); + fe_mul(r.Z, F, G, s.t19); + fe_mul(r.T, E, H, s.t19); + } + + private static void ge_cmov(Ge r, Ge p, int b) { + fe_cmov(r.X, p.X, b); + fe_cmov(r.Y, p.Y, b); + fe_cmov(r.Z, p.Z, b); + fe_cmov(r.T, p.T, b); + } + + private static void fe_cmov(long[] r, long[] p, int b) { + long mask = -(long) b; + for (int i = 0; i < 10; i++) { + long x = mask & (r[i] ^ p[i]); + r[i] ^= x; + } + } + + public static Ge decompress(byte[] b, Scratch s) { + if (b.length != 32) return null; + Ge p = new Ge(); + fe_frombytes(p.Y, b); + fe_1(p.Z); + + // u = y^2 - 1 + fe_sq(s.a, p.Y, s.t19); + fe_sub(s.a, s.a, p.Z); + + // v = dy^2 + 1 + fe_sq(s.b, p.Y, s.t19); + fe_mul(s.b, s.b, D, s.t19); + fe_add(s.b, s.b, p.Z); + + // check root of u/v + fe_invert(s.c, s.b, s); + fe_mul(s.c, s.a, s.c, s.t19); // x2 = u/v + + fe_pow22523(s.a, s.c, s); // a = x2^((p-5)/8) + fe_mul(s.a, s.c, s.a, s.t19); // x = x2 * a = x2^((p+3)/8) + + fe_sq(s.b, s.a, s.t19); + fe_sub(s.b, s.b, s.c); // x^2 - x2 + if (fe_isnonzero(s.b)) { + fe_mul(s.a, s.a, I, s.t19); + fe_sq(s.b, s.a, s.t19); + fe_sub(s.b, s.b, s.c); + if (fe_isnonzero(s.b)) return null; + } + + if (fe_isnegative(s.a) != ((b[31] >> 7) & 1)) fe_neg(p.X, s.a); + else fe_copy(p.X, s.a); + + fe_mul(p.T, p.X, p.Y, s.t19); + return p; + } + + public static byte[] compress(Ge p, Scratch s) { + fe_invert(s.a, p.Z, s); + fe_mul(s.b, p.X, s.a, s.t19); + fe_mul(s.c, p.Y, s.a, s.t19); + byte[] out = fe_contract(s.c); + if (fe_isnegative(s.b) == 1) out[31] |= (byte) 0x80; + return out; + } + + private static void fe_0(long[] h) { + for (int i = 0; i < 10; i++) h[i] = 0; + } + + private static void fe_1(long[] h) { + h[0] = 1; + for (int i = 1; i < 10; i++) h[i] = 0; + } + + private static void fe_copy(long[] h, long[] f) { + System.arraycopy(f, 0, h, 0, 10); + } + + private static void fe_add(long[] h, long[] f, long[] g) { + for (int i = 0; i < 10; i++) h[i] = f[i] + g[i]; + } + + private static void fe_sub(long[] h, long[] f, long[] g) { + for (int i = 0; i < 10; i++) h[i] = f[i] - g[i]; + } + + private static void fe_neg(long[] h, long[] f) { + for (int i = 0; i < 10; i++) h[i] = -f[i]; + } + + public static void fe_mul(long[] out, long[] f, long[] g, long[] t) { + t[0] = f[0] * g[0]; + t[1] = f[0] * g[1] + f[1] * g[0]; + t[2] = f[0] * g[2] + f[2] * g[0] + 2 * f[1] * g[1]; + t[3] = f[0] * g[3] + f[3] * g[0] + f[1] * g[2] + f[2] * g[1]; + t[4] = f[0] * g[4] + f[4] * g[0] + f[2] * g[2] + 2 * f[1] * g[3] + 2 * f[3] * g[1]; + t[5] = f[0] * g[5] + f[5] * g[0] + f[1] * g[4] + f[4] * g[1] + f[2] * g[3] + f[3] * g[2]; + t[6] = f[0] * g[6] + f[6] * g[0] + f[2] * g[4] + f[4] * g[2] + 2 * f[1] * g[5] + 2 * f[5] * g[1] + 2 * f[3] * g[3]; + t[7] = f[0] * g[7] + f[7] * g[0] + f[1] * g[6] + f[6] * g[1] + f[2] * g[5] + f[5] * g[2] + f[3] * g[4] + f[4] * g[3]; + t[8] = f[0] * g[8] + f[8] * g[0] + f[2] * g[6] + f[6] * g[2] + f[4] * g[4] + 2 * f[1] * g[7] + 2 * f[7] * g[1] + 2 * f[3] * g[5] + 2 * f[5] * g[3]; + t[9] = f[0] * g[9] + f[9] * g[0] + f[1] * g[8] + f[8] * g[1] + f[2] * g[7] + f[7] * g[2] + f[3] * g[6] + f[6] * g[3] + f[4] * g[5] + f[5] * g[4]; + t[10] = 2 * f[1] * g[9] + 2 * f[9] * g[1] + f[2] * g[8] + f[8] * g[2] + 2 * f[3] * g[7] + 2 * f[7] * g[3] + f[4] * g[6] + f[6] * g[4] + 2 * f[5] * g[5]; + t[11] = f[2] * g[9] + f[9] * g[2] + f[3] * g[8] + f[8] * g[3] + f[4] * g[7] + f[7] * g[4] + f[5] * g[6] + f[6] * g[5]; + t[12] = 2 * f[3] * g[9] + 2 * f[9] * g[3] + f[4] * g[8] + f[8] * g[4] + 2 * f[5] * g[7] + 2 * f[7] * g[5] + f[6] * g[6]; + t[13] = f[4] * g[9] + f[9] * g[4] + f[5] * g[8] + f[8] * g[5] + f[6] * g[7] + f[7] * g[6]; + t[14] = 2 * f[5] * g[9] + 2 * f[9] * g[5] + f[6] * g[8] + f[8] * g[6] + 2 * f[7] * g[7]; + t[15] = f[6] * g[9] + f[9] * g[6] + f[7] * g[8] + f[8] * g[7]; + t[16] = 2 * f[7] * g[9] + 2 * f[9] * g[7] + f[8] * g[8]; + t[17] = f[8] * g[9] + f[9] * g[8]; + t[18] = 2 * f[9] * g[9]; + fe_reduce(out, t); + } + + public static void fe_sq(long[] out, long[] f, long[] t) { + t[0] = f[0] * f[0]; + t[1] = 2 * f[0] * f[1]; + t[2] = 2 * f[0] * f[2] + 2 * f[1] * f[1]; + t[3] = 2 * f[0] * f[3] + 2 * f[1] * f[2]; + t[4] = 2 * f[0] * f[4] + 4 * f[1] * f[3] + f[2] * f[2]; + t[5] = 2 * f[0] * f[5] + 2 * f[1] * f[4] + 2 * f[2] * f[3]; + t[6] = 2 * f[0] * f[6] + 4 * f[1] * f[5] + 2 * f[2] * f[4] + 2 * f[3] * f[3]; + t[7] = 2 * f[0] * f[7] + 2 * f[1] * f[6] + 2 * f[2] * f[5] + 2 * f[3] * f[4]; + t[8] = 2 * f[0] * f[8] + 4 * f[1] * f[7] + 2 * f[2] * f[6] + 4 * f[3] * f[5] + f[4] * f[4]; + t[9] = 2 * f[0] * f[9] + 2 * f[1] * f[8] + 2 * f[2] * f[7] + 2 * f[3] * f[6] + 2 * f[4] * f[5]; + t[10] = 4 * f[1] * f[9] + 2 * f[2] * f[8] + 4 * f[3] * f[7] + 2 * f[4] * f[6] + 2 * f[5] * f[5]; + t[11] = 2 * f[2] * f[9] + 2 * f[3] * f[8] + 2 * f[4] * f[7] + 2 * f[5] * f[6]; + t[12] = 4 * f[3] * f[9] + 2 * f[4] * f[8] + 4 * f[5] * f[7] + f[6] * f[6]; + t[13] = 2 * f[4] * f[9] + 2 * f[5] * f[8] + 2 * f[6] * f[7]; + t[14] = 4 * f[5] * f[9] + 2 * f[6] * f[8] + 2 * f[7] * f[7]; + t[15] = 2 * f[6] * f[9] + 2 * f[7] * f[8]; + t[16] = 4 * f[7] * f[9] + f[8] * f[8]; + t[17] = 2 * f[8] * f[9]; + t[18] = 2 * f[9] * f[9]; + fe_reduce(out, t); + } + + public static void fe_reduce(long[] h, long[] t) { + t[0] += t[10] * 19; + t[1] += t[11] * 19; + t[2] += t[12] * 19; + t[3] += t[13] * 19; + t[4] += t[14] * 19; + t[5] += t[15] * 19; + t[6] += t[16] * 19; + t[7] += t[17] * 19; + t[8] += t[18] * 19; + for (int p = 0; p < 2; p++) { + for (int i = 0; i < 9; i++) { + long c = t[i] >> (i % 2 == 0 ? 26 : 25); + t[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL); + t[i + 1] += c; + } + long c = t[9] >> 25; + t[9] &= 0x1FFFFFFL; + t[0] += c * 19; + } + for (int i = 0; i < 10; i++) h[i] = t[i]; + } + + public static void fe_frombytes(long[] h, byte[] s) { + for (int i = 0; i < 10; i++) h[i] = 0; + int bitIdx = 0; + for (int i = 0; i < 10; i++) { + int len = (i % 2 == 0 ? 26 : 25); + for (int b = 0; b < len; b++) { + if (bitIdx < 255) { + if ((((s[bitIdx / 8] & 0xFF) >> (bitIdx % 8)) & 1) == 1) h[i] |= (1L << b); + } + bitIdx++; + } + } + } + + public static byte[] fe_contract(long[] h) { + long[] val = Arrays.copyOf(h, 10); + for (int p = 0; p < 2; p++) { + for (int i = 0; i < 9; i++) { + long c = val[i] >> (i % 2 == 0 ? 26 : 25); + val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL); + val[i + 1] += c; + } + long c = val[9] >> 25; + val[9] &= 0x1FFFFFFL; + val[0] += c * 19; + } + long mask = 1; + for (int i = 9; i >= 0; i--) { + long target = (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL); + if (i == 0) target -= 19; + if (val[i] < target) { + mask = 0; + break; + } + if (val[i] > target) break; + } + val[0] += mask * 19; + for (int i = 0; i < 9; i++) { + long c = val[i] >> (i % 2 == 0 ? 26 : 25); + val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL); + val[i + 1] += c; + } + val[9] -= mask * (1L << 25); + for (int i = 0; i < 9; i++) { + long c = val[i] >> (i % 2 == 0 ? 26 : 25); + val[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL); + val[i + 1] += c; + } + val[0] += (val[9] >> 25) * 19; + val[9] &= 0x1FFFFFFL; + byte[] out = new byte[32]; + int bitIdx = 0; + for (int i = 0; i < 10; i++) { + int len = (i % 2 == 0 ? 26 : 25); + for (int bit = 0; bit < len; bit++) { + if (bitIdx < 255) { + if (((val[i] >> bit) & 1) == 1) out[bitIdx / 8] |= (byte) (1 << (bitIdx % 8)); + bitIdx++; + } + } + } + return out; + } + + private static void fe_invert(long[] out, long[] z, Scratch s) { + long[] t0 = s.stack[0], t1 = s.stack[1], z2 = s.stack[2], z9 = s.stack[3], z11 = s.stack[4]; + long[] z2_5_0 = s.stack[5], z2_10_0 = s.stack[6], z2_20_0 = s.stack[7], z2_50_0 = s.stack[8], z2_100_0 = s.stack[9]; + fe_sq(z2, z, s.t19); + fe_sq(t1, z2, s.t19); + fe_sq(t0, t1, s.t19); + fe_mul(z9, t0, z, s.t19); + fe_mul(z11, z9, z2, s.t19); + fe_sq(t0, z11, s.t19); + fe_mul(z2_5_0, t0, z9, s.t19); + fe_sq(t0, z2_5_0, s.t19); + for (int i = 1; i < 5; i++) fe_sq(t0, t0, s.t19); + fe_mul(z2_10_0, t0, z2_5_0, s.t19); + fe_sq(t0, z2_10_0, s.t19); + for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19); + fe_mul(z2_20_0, t0, z2_10_0, s.t19); + fe_sq(t0, z2_20_0, s.t19); + for (int i = 1; i < 20; i++) fe_sq(t0, t0, s.t19); + fe_mul(t0, t0, z2_20_0, s.t19); + fe_sq(t0, t0, s.t19); + for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19); + fe_mul(z2_50_0, t0, z2_10_0, s.t19); + fe_sq(t0, z2_50_0, s.t19); + for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19); + fe_mul(z2_100_0, t0, z2_50_0, s.t19); + fe_sq(t1, z2_100_0, s.t19); + for (int i = 1; i < 100; i++) fe_sq(t1, t1, s.t19); + fe_mul(t1, t1, z2_100_0, s.t19); + fe_sq(t0, t1, s.t19); + for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19); + fe_mul(t0, t0, z2_50_0, s.t19); + fe_sq(t1, t0, s.t19); + for (int i = 1; i < 5; i++) fe_sq(t1, t1, s.t19); + fe_mul(out, t1, z11, s.t19); + } + + private static void fe_pow22523(long[] out, long[] in, Scratch s) { + long[] t0 = s.stack[0], t1 = s.stack[1], z2 = s.stack[2], z9 = s.stack[3], z11 = s.stack[4]; + long[] z2_5_0 = s.stack[5], z2_10_0 = s.stack[6], z2_20_0 = s.stack[7], z2_50_0 = s.stack[8], z2_100_0 = s.stack[9]; + fe_sq(z2, in, s.t19); + fe_sq(t1, z2, s.t19); + fe_sq(t0, t1, s.t19); + fe_mul(z9, t0, in, s.t19); + fe_mul(z11, z9, z2, s.t19); + fe_sq(t0, z11, s.t19); + fe_mul(z2_5_0, t0, z9, s.t19); + fe_sq(t0, z2_5_0, s.t19); + for (int i = 1; i < 5; i++) fe_sq(t0, t0, s.t19); + fe_mul(z2_10_0, t0, z2_5_0, s.t19); + fe_sq(t0, z2_10_0, s.t19); + for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19); + fe_mul(z2_20_0, t0, z2_10_0, s.t19); + fe_sq(t0, z2_20_0, s.t19); + for (int i = 1; i < 20; i++) fe_sq(t0, t0, s.t19); + fe_mul(t0, t0, z2_20_0, s.t19); + fe_sq(t0, t0, s.t19); + for (int i = 1; i < 10; i++) fe_sq(t0, t0, s.t19); + fe_mul(z2_50_0, t0, z2_10_0, s.t19); + fe_sq(t0, z2_50_0, s.t19); + for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19); + fe_mul(z2_100_0, t0, z2_50_0, s.t19); + fe_sq(t1, z2_100_0, s.t19); + for (int i = 1; i < 100; i++) fe_sq(t1, t1, s.t19); + fe_mul(t1, t1, z2_100_0, s.t19); + fe_sq(t0, t1, s.t19); + for (int i = 1; i < 50; i++) fe_sq(t0, t0, s.t19); + fe_mul(t0, t0, z2_50_0, s.t19); + fe_sq(t0, t0, s.t19); + fe_sq(t0, t0, s.t19); + fe_mul(out, t0, in, s.t19); + } + + private static boolean fe_isnonzero(long[] h) { + byte[] s = fe_contract(h); + for (byte b : s) if (b != 0) return true; + return false; + } + + private static int fe_isnegative(long[] h) { + byte[] s = fe_contract(h); + return s[0] & 1; + } + + public static void reduceScalar(byte[] s) { + long s0 = 2097151 & load3(s, 0); + long s1 = 2097151 & (load4(s, 2) >> 5); + long s2 = 2097151 & (load3(s, 5) >> 2); + long s3 = 2097151 & (load4(s, 7) >> 7); + long s4 = 2097151 & (load4(s, 10) >> 4); + long s5 = 2097151 & (load3(s, 13) >> 1); + long s6 = 2097151 & (load4(s, 15) >> 6); + long s7 = 2097151 & (load3(s, 18) >> 3); + long s8 = 2097151 & load3(s, 21); + long s9 = 2097151 & (load4(s, 23) >> 5); + long s10 = 2097151 & (load3(s, 26) >> 2); + long s11 = 2097151 & (load4(s, 28) >> 7); + long s12 = 2097151 & (load4(s, 31) >> 4); + long s13 = 2097151 & (load3(s, 34) >> 1); + long s14 = 2097151 & (load4(s, 36) >> 6); + long s15 = 2097151 & (load3(s, 39) >> 3); + long s16 = 2097151 & load3(s, 42); + long s17 = 2097151 & (load4(s, 44) >> 5); + long s18 = 2097151 & (load3(s, 47) >> 2); + long s19 = 2097151 & (load4(s, 49) >> 7); + long s20 = 2097151 & (load4(s, 52) >> 4); + long s21 = 2097151 & (load3(s, 55) >> 1); + long s22 = 2097151 & (load4(s, 57) >> 6); + long s23 = (load4(s, 60) >> 3); + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + long c6 = (s6 + (1 << 20)) >> 21; + s7 += c6; + s6 -= c6 << 21; + long c8 = (s8 + (1 << 20)) >> 21; + s9 += c8; + s8 -= c8 << 21; + long c10 = (s10 + (1 << 20)) >> 21; + s11 += c10; + s10 -= c10 << 21; + long c12 = (s12 + (1 << 20)) >> 21; + s13 += c12; + s12 -= c12 << 21; + long c14 = (s14 + (1 << 20)) >> 21; + s15 += c14; + s14 -= c14 << 21; + long c16 = (s16 + (1 << 20)) >> 21; + s17 += c16; + s16 -= c16 << 21; + long c7 = (s7 + (1 << 20)) >> 21; + s8 += c7; + s7 -= c7 << 21; + long c9 = (s9 + (1 << 20)) >> 21; + s10 += c9; + s9 -= c9 << 21; + long c11 = (s11 + (1 << 20)) >> 21; + s12 += c11; + s11 -= c11 << 21; + long c13 = (s13 + (1 << 20)) >> 21; + s14 += c13; + s13 -= c13 << 21; + long c15 = (s15 + (1 << 20)) >> 21; + s16 += c15; + s15 -= c15 << 21; + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + long cc0 = (s0 + (1 << 20)) >> 21; + s1 += cc0; + s0 -= cc0 << 21; + long cc2 = (s2 + (1 << 20)) >> 21; + s3 += cc2; + s2 -= cc2 << 21; + long cc4 = (s4 + (1 << 20)) >> 21; + s5 += cc4; + s4 -= cc4 << 21; + long cc6 = (s6 + (1 << 20)) >> 21; + s7 += cc6; + s6 -= cc6 << 21; + long cc8 = (s8 + (1 << 20)) >> 21; + s9 += cc8; + s8 -= cc8 << 21; + long cc10 = (s10 + (1 << 20)) >> 21; + s11 += cc10; + s10 -= cc10 << 21; + long cc1 = (s1 + (1 << 20)) >> 21; + s2 += cc1; + s1 -= cc1 << 21; + long cc3 = (s3 + (1 << 20)) >> 21; + s4 += cc3; + s3 -= cc3 << 21; + long cc5 = (s5 + (1 << 20)) >> 21; + s6 += cc5; + s5 -= cc5 << 21; + long cc7 = (s7 + (1 << 20)) >> 21; + s8 += cc7; + s7 -= cc7 << 21; + long cc9 = (s9 + (1 << 20)) >> 21; + s10 += cc9; + s9 -= cc9 << 21; + long cc11 = (s11 + (1 << 20)) >> 21; + long s12_2 = cc11; + s11 -= cc11 << 21; + s0 += s12_2 * 666643; + s1 += s12_2 * 470296; + s2 += s12_2 * 654183; + s3 -= s12_2 * 997805; + s4 += s12_2 * 136657; + s5 -= s12_2 * 683901; + long ccc0 = s0 >> 21; + s1 += ccc0; + s0 -= ccc0 << 21; + long ccc1 = s1 >> 21; + s2 += ccc1; + s1 -= ccc1 << 21; + long ccc2 = s2 >> 21; + s3 += ccc2; + s2 -= ccc2 << 21; + long ccc3 = s3 >> 21; + s4 += ccc3; + s3 -= ccc3 << 21; + long ccc4 = s4 >> 21; + s5 += ccc4; + s4 -= ccc4 << 21; + long ccc5 = s5 >> 21; + s6 += ccc5; + s5 -= ccc5 << 21; + long ccc6 = s6 >> 21; + s7 += ccc6; + s6 -= ccc6 << 21; + long ccc7 = s7 >> 21; + s8 += ccc7; + s7 -= ccc7 << 21; + long ccc8 = s8 >> 21; + s9 += ccc8; + s8 -= ccc8 << 21; + long ccc9 = s9 >> 21; + s10 += ccc9; + s9 -= ccc9 << 21; + long ccc10 = s10 >> 21; + s11 += ccc10; + s10 -= ccc10 << 21; + long ccc11 = s11 >> 21; + long s12_3 = ccc11; + s11 -= ccc11 << 21; + s0 += s12_3 * 666643; + s1 += s12_3 * 470296; + s2 += s12_3 * 654183; + s3 -= s12_3 * 997805; + s4 += s12_3 * 136657; + s5 -= s12_3 * 683901; + long sccc0 = s0 >> 21; + s1 += sccc0; + s0 -= sccc0 << 21; + long sccc1 = s1 >> 21; + s2 += sccc1; + s1 -= sccc1 << 21; + long sccc2 = s2 >> 21; + s3 += sccc2; + s2 -= sccc2 << 21; + long sccc3 = s3 >> 21; + s4 += sccc3; + s3 -= sccc3 << 21; + long sccc4 = s4 >> 21; + s5 += sccc4; + s4 -= sccc4 << 21; + long sccc5 = s5 >> 21; + s6 += sccc5; + s5 -= sccc5 << 21; + Arrays.fill(s, (byte) 0); + long[] resLimbs = {s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11}; + int bitIdx = 0; + for (int limbIdx = 0; limbIdx < 12; limbIdx++) { + for (int i = 0; i < 21; i++) { + if (bitIdx < 256) { + if (((resLimbs[limbIdx] >> i) & 1) == 1) + s[bitIdx / 8] |= (byte) (1 << (bitIdx % 8)); + bitIdx++; + } + } + } + } + + private static long load3(byte[] in, int off) { + return (in[off] & 0xffL) | ((in[off + 1] & 0xffL) << 8) | ((in[off + 2] & 0xffL) << 16); + } + + private static long load4(byte[] in, int off) { + return (in[off] & 0xffL) | ((in[off + 1] & 0xffL) << 8) | ((in[off + 2] & 0xffL) << 16) | ((in[off + 3] & 0xffL) << 24); + } + + private static void scalarMulAdd(byte[] S, byte[] k, byte[] s, byte[] r) { + long[] res = new long[64]; + for (int i = 0; i < 32; i++) { + for (int j = 0; j < 32; j++) res[i + j] += (k[i] & 0xFFL) * (s[j] & 0xFFL); + } + for (int i = 0; i < 32; i++) res[i] += (r[i] & 0xFFL); + byte[] buffer = new byte[64]; + long carry = 0; + for (int i = 0; i < 63; i++) { + long val = res[i] + carry; + buffer[i] = (byte) val; + carry = val >>> 8; + } + buffer[63] = (byte) (res[63] + carry); + reduceScalar(buffer); + System.arraycopy(buffer, 0, S, 0, 32); + } + + private static byte[] sha512(byte[]... parts) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + for (byte[] p : parts) md.update(p); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/crypto/Keccak256.java b/src/main/java/swiss/qpq/gajumaru/core/crypto/Keccak256.java new file mode 100644 index 0000000..dd5eb33 --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/crypto/Keccak256.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * This program is dual-licensed: + * 1) Under the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version (AGPL-3.0-or-later). + * + * 2) Under a commercial/proprietary license available directly from QPQ AG. + * If you wish to use this software outside the strict constraints of the + * AGPLv3 (e.g. within a closed-source or proprietary product), you must + * purchase a commercial license from QPQ AG. + * + * Authors: + * - Craig Everett + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.crypto; + +import java.util.Arrays; +import swiss.qpq.gajumaru.core.tools.CryptoUtils; + + +// Pure Java implementation of Keccak-256 (Keccak[c=512] sponge). +// Based on sha3.erl. + +public final class Keccak256 { + + private static final int LANE_SIZE = 64; + private static final int STATE_SIZE = 25; + private static final int CAPACITY = 512; + private static final int BITRATE = 1600 - CAPACITY; // 1088 bits = 136 bytes + private static final int BITRATE_BYTES = BITRATE / 8; + + private static final long[] ROUND_CONSTANTS = { + 0x0000000000000001L, 0x0000000000008082L, 0x800000000000808AL, 0x8000000080008000L, + 0x000000000000808BL, 0x0000000080000001L, 0x8000000080008081L, 0x8000000000008009L, + 0x000000000000008AL, 0x0000000000000088L, 0x0000000080008009L, 0x000000008000000AL, + 0x000000008000808BL, 0x800000000000008BL, 0x8000000000008089L, 0x8000000000008003L, + 0x8000000000008002L, 0x8000000000000080L, 0x000000000000800AL, 0x800000008000000AL, + 0x8000000080008081L, 0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L + }; + + private static final int[] ROTATION_OFFSETS = { + 0, 1, 62, 28, 27, + 36, 44, 6, 55, 20, + 3, 10, 43, 25, 39, + 41, 45, 15, 21, 8, + 18, 2, 61, 56, 14 + }; + + private Keccak256() {} + + public static byte[] hash(byte[] data) { + long[] state = new long[STATE_SIZE]; + + // Padding: Keccak[c=512] uses 0x01 ... 0x80 (or just 0x01 for non-NIST Keccak) + // Erlang code uses: keccak(Capacity, Message, <<>>, OutputBitLength) + // Pad logic in Erlang: <> + // With Delimiter = <<>>, it becomes <<1:1, 0:PadZeros, 1:1>> (standard Keccak padding) + + byte[] padded = pad(data, BITRATE_BYTES); + + for (int i = 0; i < padded.length; i += BITRATE_BYTES) { + absorb(state, padded, i); + } + + byte[] result = squeeze(state, 32); + + // Memory hygiene + Arrays.fill(state, 0L); + CryptoUtils.wipe(padded); + + return result; + } + + private static byte[] pad(byte[] data, int rateBytes) { + int mLen = data.length; + int padLen = rateBytes - (mLen % rateBytes); + + byte[] padded = new byte[mLen + padLen]; + System.arraycopy(data, 0, padded, 0, mLen); + + if (padLen == 1) { + padded[mLen] = (byte) 0x81; + } else { + padded[mLen] = (byte) 0x01; + padded[padded.length - 1] |= (byte) 0x80; + } + return padded; + } + + private static void absorb(long[] state, byte[] data, int offset) { + for (int i = 0; i < BITRATE_BYTES / 8; i++) { + state[i] ^= readLongLittleEndian(data, offset + i * 8); + } + keccakF(state); + } + + private static byte[] squeeze(long[] state, int len) { + byte[] result = new byte[len]; + int count = 0; + while (count < len) { + for (int i = 0; i < BITRATE_BYTES / 8 && count < len; i++) { + writeLongLittleEndian(state[i], result, count); + count += 8; + } + if (count < len) { + keccakF(state); + } + } + return result; + } + + private static void keccakF(long[] a) { + for (int round = 0; round < 24; round++) { + // Theta + long[] c = new long[5]; + for (int x = 0; x < 5; x++) { + c[x] = a[x] ^ a[x + 5] ^ a[x + 10] ^ a[x + 15] ^ a[x + 20]; + } + for (int x = 0; x < 5; x++) { + long d = c[(x + 4) % 5] ^ Long.rotateLeft(c[(x + 1) % 5], 1); + for (int y = 0; y < 5; y++) { + a[x + y * 5] ^= d; + } + } + + // Rho and Pi + long[] nextA = new long[STATE_SIZE]; + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + int index = x + y * 5; + nextA[y + ((2 * x + 3 * y) % 5) * 5] = Long.rotateLeft(a[index], ROTATION_OFFSETS[index]); + } + } + + // Chi + for (int y = 0; y < 5; y++) { + long[] row = new long[5]; + for (int x = 0; x < 5; x++) { + row[x] = nextA[x + y * 5]; + } + for (int x = 0; x < 5; x++) { + nextA[x + y * 5] = row[x] ^ ((~row[(x + 1) % 5]) & row[(x + 2) % 5]); + } + } + + // Iota + nextA[0] ^= ROUND_CONSTANTS[round]; + + System.arraycopy(nextA, 0, a, 0, STATE_SIZE); + } + } + + private static long readLongLittleEndian(byte[] b, int offset) { + long res = 0; + for (int i = 0; i < 8; i++) { + res |= ((long) (b[offset + i] & 0xFF)) << (i * 8); + } + return res; + } + + private static void writeLongLittleEndian(long v, byte[] b, int offset) { + for (int i = 0; i < 8; i++) { + b[offset + i] = (byte) ((v >>> (i * 8)) & 0xFF); + } + } +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/data/Id.java b/src/main/java/swiss/qpq/gajumaru/core/data/Id.java new file mode 100644 index 0000000..23fef95 --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/data/Id.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * This program is dual-licensed: + * 1) Under the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version (AGPL-3.0-or-later). + * + * 2) Under a commercial/proprietary license available directly from QPQ AG. + * If you wish to use this software outside the strict constraints of the + * AGPLv3 (e.g. within a closed-source or proprietary product), you must + * purchase a commercial license from QPQ AG. + * + * Authors: + * - Craig Everett + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.data; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + + +// Port of gmser_id.erl. +// Represents a 33-byte identifier (1-byte tag + 32-byte value). + +public final class Id { + + public enum Tag { + ACCOUNT(1), + NAME(2), + COMMITMENT(3), + CONTRACT(5), + CHANNEL(6), + ASSOCIATE_CHAIN(7), + NATIVE_TOKEN(8), + ENTRY(9); + + public final int value; + + Tag(int value) { + this.value = value; + } + + private static final Map VALUE_MAP = new HashMap<>(); + static { + for (Tag t : Tag.values()) { + VALUE_MAP.put(t.value, t); + } + } + + public static Tag fromValue(int v) { + Tag t = VALUE_MAP.get(v); + if (t == null) throw new IllegalArgumentException("Unknown ID tag value: " + v); + return t; + } + } + + private final Tag tag; + private final byte[] value; + + public Id(Tag tag, byte[] value) { + if (value.length != 32) { + throw new IllegalArgumentException("ID value must be exactly 32 bytes"); + } + this.tag = tag; + this.value = Arrays.copyOf(value, 32); + } + + public Tag getTag() { return tag; } + public byte[] getValue() { return Arrays.copyOf(value, 32); } + + public byte[] serialize() { + byte[] result = new byte[33]; + result[0] = (byte) tag.value; + System.arraycopy(value, 0, result, 1, 32); + return result; + } + + public static Id deserialize(byte[] data) { + if (data.length != 33) { + throw new IllegalArgumentException("Serialized ID must be exactly 33 bytes"); + } + Tag tag = Tag.fromValue(data[0] & 0xFF); + byte[] value = Arrays.copyOfRange(data, 1, 33); + return new Id(tag, value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Id)) return false; + Id id = (Id) o; + return tag == id.tag && Arrays.equals(value, id.value); + } + + @Override + public int hashCode() { + int result = tag.hashCode(); + result = 31 * result + Arrays.hashCode(value); + return result; + } +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/encoding/ApiEncoder.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/ApiEncoder.java new file mode 100644 index 0000000..35e4628 --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/ApiEncoder.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * This program is dual-licensed: + * 1) Under the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version (AGPL-3.0-or-later). + * + * 2) Under a commercial/proprietary license available directly from QPQ AG. + * If you wish to use this software outside the strict constraints of the + * AGPLv3 (e.g. within a closed-source or proprietary product), you must + * purchase a commercial license from QPQ AG. + * + * Authors: + * - Craig Everett + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.encoding; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + + +// Port of gmser_api_encoder.erl. +// Handles Gajumaru API encoding (prefixed Base58Check). + +public final class ApiEncoder { + + public enum Type { + ACCOUNT_PUBKEY("ak", 32), + ACCOUNT_SECKEY("sk", 32), + TX_HASH("th", 32), + CONTRACT_PUBKEY("ct", 32), + CHANNEL("ch", 32), + SIGNATURE("sg", 64), + KEY_BLOCK_HASH("kh", 32), + MICRO_BLOCK_HASH("mh", 32), + COMMITMENT("cm", 32), + PEER_PUBKEY("pp", 32), + NAME("nm", -1); // Variable size + + public final String prefix; + public final int size; + + Type(String prefix, int size) { + this.prefix = prefix; + this.size = size; + } + } + + private static final Map PREFIX_MAP = new HashMap<>(); + static { + for (Type t : Type.values()) { + PREFIX_MAP.put(t.prefix, t); + } + } + + private ApiEncoder() {} + + public static String encode(Type type, byte[] payload) { + if (type.size != -1 && payload.length != type.size) { + throw new IllegalArgumentException("Invalid payload size for " + type + ": " + payload.length); + } + return type.prefix + "_" + Base58.checkEncode(payload); + } + + public static DecodeResult decode(String input) { + String[] parts = input.split("_"); + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid encoded string format"); + } + + Type type = PREFIX_MAP.get(parts[0]); + if (type == null) { + throw new IllegalArgumentException("Unknown prefix: " + parts[0]); + } + + byte[] payload = Base58.checkDecode(parts[1]); + if (type.size != -1 && payload.length != type.size) { + throw new IllegalArgumentException("Invalid decoded payload size for " + type + ": " + payload.length); + } + + return new DecodeResult(type, payload); + } + + public record DecodeResult(Type type, byte[] payload) {} +} diff --git a/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/Base58.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/Base58.java similarity index 99% rename from gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/Base58.java rename to src/main/java/swiss/qpq/gajumaru/core/encoding/Base58.java index cacd556..ed08705 100644 --- a/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/Base58.java +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/Base58.java @@ -24,6 +24,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; + // Stateless Base58 and Base58Check implementation. public final class Base58 { diff --git a/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java similarity index 95% rename from gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java rename to src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java index b195347..bb9ef95 100644 --- a/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/RLP.java @@ -26,10 +26,7 @@ import java.util.List; public final class RLP { - // Integrated data models // RLP only has two types: byte arrays and lists of lists of byte arrays - // TODO: Comment this better with references. - public static class RLPException extends RuntimeException { public RLPException(String message) { super(message); @@ -110,12 +107,10 @@ public final class RLP { int prefix = buffer[offset] & 0xFF; - // 1. Single byte [0x00, 0x7F] (itself) if (prefix <= 0x7F) { return new DecodeResult(new RLP_Item(new byte[] { (byte) prefix }), 1); } - // 2. Short string [0x80, 0xB7] (length 0-55) if (prefix <= 0xB7) { int payloadLen = prefix - 0x80; checkBounds(buffer, offset + 1, payloadLen); @@ -123,7 +118,6 @@ public final class RLP { return new DecodeResult(new RLP_Item(payload), 1 + payloadLen); } - // 3. Long string [0xB8, 0xBF] (length > 55) if (prefix <= 0xBF) { int lenLen = prefix - 0xB7; checkBounds(buffer, offset + 1, lenLen); @@ -133,7 +127,6 @@ public final class RLP { return new DecodeResult(new RLP_Item(payload), 1 + lenLen + payloadLen); } - // 4. Short list [0xC0, 0xF7] (total length 0-55) if (prefix <= 0xF7) { int payloadLen = prefix - 0xC0; checkBounds(buffer, offset + 1, payloadLen); @@ -141,7 +134,6 @@ public final class RLP { return new DecodeResult(list, 1 + payloadLen); } - // 5. Long list [0xF8, 0xFF] (total length > 55) int lenLen = prefix - 0xF7; checkBounds(buffer, offset + 1, lenLen); int payloadLen = bigEndianToInt(buffer, offset + 1, offset + 1 + lenLen); diff --git a/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java b/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java similarity index 100% rename from gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java rename to src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/CryptoUtils.java b/src/main/java/swiss/qpq/gajumaru/core/tools/CryptoUtils.java new file mode 100644 index 0000000..fcdeebd --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/CryptoUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * This program is dual-licensed: + * 1) Under the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version (AGPL-3.0-or-later). + * + * 2) Under a commercial/proprietary license available directly from QPQ AG. + * If you wish to use this software outside the strict constraints of the + * AGPLv3 (e.g. within a closed-source or proprietary product), you must + * purchase a commercial license from QPQ AG. + * + * Authors: + * - Craig Everett + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.tools; + +import java.util.Arrays; + + +// Utility class for cryptographic operations and memory hygiene. + +public final class CryptoUtils { + + private CryptoUtils() {} + + // Wipes a byte array by filling it with zeros. + public static void wipe(byte[] data) { + if (data != null) { + Arrays.fill(data, (byte) 0); + } + } + + public static void wipe(long[] data) { + if (data != null) { + Arrays.fill(data, 0L); + } + } + + public static String binToHex(byte[] data) { + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } + + public static byte[] hexToBin(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } +} diff --git a/gajumaru-core/test/.gitignore b/test/.gitignore similarity index 93% rename from gajumaru-core/test/.gitignore rename to test/.gitignore index 20177b4..54e882c 100644 --- a/gajumaru-core/test/.gitignore +++ b/test/.gitignore @@ -13,3 +13,5 @@ doc/erlang.png rel/example_project .concrete/DEV_MODE .rebar +.idea +*.iml \ No newline at end of file diff --git a/gajumaru-core/test/Emakefile b/test/Emakefile similarity index 100% rename from gajumaru-core/test/Emakefile rename to test/Emakefile diff --git a/gajumaru-core/test/LICENSE b/test/LICENSE similarity index 100% rename from gajumaru-core/test/LICENSE rename to test/LICENSE diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..700334a --- /dev/null +++ b/test/README.md @@ -0,0 +1,53 @@ +# GM Java <-> Erlang Tests + +The core Java libraries are all written as transform functions over data. +There is little point, therefore, in obsessing over "unit tests" and "regression testing" of +Java code in Java when we have cannonical code in Erlang. + + +The purpose of this utility is to instead test the Java libraries agains the Erlang libraries +directly. + + +## How to run + +In the current directory simply run `zx runlocal` and the report map will be printed to the screen +and random test data will be in the `temp/` directory. + +To run a specific test suite run `zx runlocal [module names]`. + +To list module names, run `zx runlocal list`. + + +## Where things are + +The test cases are all generated in gmt.erl, generally based on randomized but valid data that +are fed to reference Erlang implementations. Java code is built using plain old javac, and is +handled by the `gajumaru-core/bin/compile` and `gajumaru-core/bin/run` scripts. + +The input to and output from each test is recorded to disk in the `temp/` dir. + +The inputs are then read in by equivalent Java functions and the output is then compared. + +For a test case to pass the outputs must be identical. + + +## The Erlang execution context + +`zx` manages loading the execution context, so dependencies like gmserialization, ec_utils, +base58, etc. (all of which are targets for which is increasingly turning into a Java port) +are brought in dynamically and managed by `zx`. The easiest way to test a specific function +in Java against the Erlang implementation is to add a call to it in gmt.erl and a matching +call in `../src/Testinator.java`. + +The dependencies that are brought in can be listed with `zx list deps`. + +The package names follow the pattern `[realm]-[package_name]-[version]` + +The location of the sources varies on different systems. +- Linux: `$HOME/zomp/lib/[realm]/[package_name]/[version]/src` +- MacOS: `$HOME/.zx/zomp/lib/[realm]/[package_name]/[version]/src` +- Windows: `%%LOCALAPPDATA%%/zomp/lib/[realm]/[package_name]/[version]/src` + +So for example `otpr-ec_utils-1.0.0` has its sources at `~/zomp/lib/otpr/ec_utils/1.0.0/src/` +on a reasonable system. \ No newline at end of file diff --git a/test/Testinator.java b/test/Testinator.java new file mode 100644 index 0000000..ef25b6c --- /dev/null +++ b/test/Testinator.java @@ -0,0 +1,416 @@ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.ArrayList; +import java.util.List; +import java.util.Arrays; +import java.math.BigInteger; + +import swiss.qpq.gajumaru.core.encoding.Base58; +import swiss.qpq.gajumaru.core.encoding.RLP; +import swiss.qpq.gajumaru.core.encoding.ApiEncoder; +import swiss.qpq.gajumaru.core.formatting.GajuFormat; +import swiss.qpq.gajumaru.core.crypto.Keccak256; +import swiss.qpq.gajumaru.core.crypto.Blake2b; +import swiss.qpq.gajumaru.core.crypto.Ed25519; +import swiss.qpq.gajumaru.core.data.Id; +import swiss.qpq.gajumaru.core.tools.CryptoUtils; + +public class Testinator { + public static void main(String[] args) { + if (args.length < 2) { + System.out.println("Error: Provide the test suite name and the required arguments."); + System.exit(1); + } + + try { + switch (args[0]) { + case "base64" -> { System.out.print(base64(args[1])); } + case "base58" -> { System.out.print(base58(args[1])); } + case "base58_check" -> { System.out.print(base58_check(args[1])); } + case "rlp" -> { System.out.print(rlp(args[1])); } + case "rlp_stream" -> { System.out.print(rlp_stream(args[1])); } + case "rlp_fail" -> { System.out.print(rlp_fail(args[1])); } + case "gaju_format" -> { System.out.print(gaju_format(args[1])); } + case "keccak256" -> { System.out.print(keccak256(args[1])); } + case "blake2b" -> { System.out.print(blake2b(args[1])); } + case "ed25519" -> { System.out.print(ed25519(args[1])); } + case "ed25519_verify" -> { System.out.print(ed25519_verify(args[1])); } + case "api_encode" -> { System.out.print(api_encode(args[1])); } + case "id_serialization" -> { System.out.print(id_serialization(args[1])); } + case "fe_parity" -> { System.out.print(fe_parity(args[1])); } + case "reduce_parity" -> { System.out.print(reduce_parity(args[1])); } + case "smb_parity" -> { System.out.print(smb_parity(args[1])); } + case "femul_parity" -> { System.out.print(femul_parity(args[1])); } + case "frombytes_parity" -> { System.out.print(frombytes_parity(args[1])); } + case "ge_parity" -> { System.out.print(ge_parity(args[1], args[2])); } + case "keccak" -> { + byte[] in = CryptoUtils.hexToBin(args[1]); + System.out.println(CryptoUtils.binToHex(Keccak256.hash(in))); + } + case "blake2b_direct" -> { + byte[] in = CryptoUtils.hexToBin(args[1]); + System.out.println(CryptoUtils.binToHex(Blake2b.hash(in))); + } + case "ak_encode" -> { + byte[] in = CryptoUtils.hexToBin(args[1]); + System.out.println(ApiEncoder.encode(ApiEncoder.Type.ACCOUNT_PUBKEY, in)); + } + case "ed25519_pub" -> { + byte[] seed = CryptoUtils.hexToBin(args[1]); + System.out.println(CryptoUtils.binToHex(Ed25519.publicKey(seed))); + } + case "id_serialize" -> { + int tag = Integer.parseInt(args[1]); + byte[] val = CryptoUtils.hexToBin(args[2]); + Id id = new Id(Id.Tag.fromValue(tag), val); + System.out.println(CryptoUtils.binToHex(id.serialize())); + } + default -> { + System.out.println("Error: Unknown test suite or command: " + args[0]); + System.exit(1); + } + } + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } + + private static String ge_parity(String aHex, String bHex) throws IOException { + byte[] a = CryptoUtils.hexToBin(aHex); + byte[] b = CryptoUtils.hexToBin(bHex); + + Ed25519.Scratch sc = new Ed25519.Scratch(); + Ed25519.Ge p1 = Ed25519.scalarMulBase(a, sc); + Ed25519.Ge p2 = Ed25519.scalarMulBase(b, sc); + Ed25519.Ge p3 = new Ed25519.Ge(); + Ed25519.ge_add(p3, p1, p2, sc); + byte[] res = Ed25519.compress(p3, sc); + p1.wipe(); p2.wipe(); p3.wipe(); sc.wipe(); + + return CryptoUtils.binToHex(res) + "|||"; + } + + private static String frombytes_parity(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "frombytes.test"); + Path resPath = Path.of(workingPath, "frombytes.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + for (String hex : lines) { + if (hex.trim().isEmpty()) continue; + byte[] in = CryptoUtils.hexToBin(hex); + long[] h = new long[10]; + Ed25519.fe_frombytes(h, in); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + if (i > 0) sb.append(","); + sb.append(h[i]); + } + results.add(sb.toString()); + } + Files.write(resPath, results); + return resPath.toString(); + } + + private static String femul_parity(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "femul.test"); + Path resPath = Path.of(workingPath, "femul.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + for (String line : lines) { + if (line.trim().isEmpty()) continue; + String[] parts = line.split("\\|"); + byte[] a = CryptoUtils.hexToBin(parts[0]); + byte[] b = CryptoUtils.hexToBin(parts[1]); + + long[] ha = new long[10], hb = new long[10]; + Ed25519.fe_frombytes(ha, a); + Ed25519.fe_frombytes(hb, b); + long[] hr = new long[10]; + Ed25519.fe_mul(hr, ha, hb, new long[19]); + byte[] out = Ed25519.fe_contract(hr); + + results.add(CryptoUtils.binToHex(out)); + } + Files.write(resPath, results); + return resPath.toString(); + } + + private static String smb_parity(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "smb.test"); + Path resPath = Path.of(workingPath, "smb.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + for (String hex : lines) { + if (hex.trim().isEmpty()) continue; + byte[] in = CryptoUtils.hexToBin(hex); + + Ed25519.Scratch sc = new Ed25519.Scratch(); + Ed25519.Ge p = Ed25519.scalarMulBase(in, sc); + byte[] out = Ed25519.compress(p, sc); + p.wipe(); sc.wipe(); + + results.add(CryptoUtils.binToHex(out)); + } + Files.write(resPath, results); + return resPath.toString(); + } + + private static String reduce_parity(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "reduce.test"); + Path resPath = Path.of(workingPath, "reduce.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + for (String hex : lines) { + if (hex.trim().isEmpty()) continue; + byte[] in = CryptoUtils.hexToBin(hex); + + byte[] s = Arrays.copyOf(in, 64); + Ed25519.reduceScalar(s); + byte[] out = Arrays.copyOf(s, 32); + + results.add(CryptoUtils.binToHex(out)); + } + Files.write(resPath, results); + return resPath.toString(); + } + + private static String fe_parity(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "fe.test"); + Path resPath = Path.of(workingPath, "fe.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + for (String hex : lines) { + if (hex.trim().isEmpty()) continue; + byte[] in = CryptoUtils.hexToBin(hex); + + long[] h = new long[10]; + Ed25519.fe_frombytes(h, in); + byte[] out = Ed25519.fe_contract(h); + + results.add(CryptoUtils.binToHex(out)); + } + Files.write(resPath, results); + return resPath.toString(); + } + + private static String base64(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "base64.test"); + Path encPath = Path.of(workingPath, "base64.java.txt"); + Path decPath = Path.of(workingPath, "base64.java.back"); + byte[] rawB = Files.readAllBytes(testPath); + byte[] encB = Base64.getEncoder().encode(rawB); + Files.write(encPath, encB); + byte[] readE = Files.readAllBytes(encPath); + byte[] decB = Base64.getDecoder().decode(readE); + Files.write(decPath, decB); + return encPath.toString() + " " + decPath.toString(); + } + + private static String base58(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "base58.test"); + Path encPath = Path.of(workingPath, "base58.java.txt"); + Path decPath = Path.of(workingPath, "base58.java.back"); + byte[] rawB = Files.readAllBytes(testPath); + String encS = Base58.encode(rawB); + Files.write(encPath, encS.getBytes()); + byte[] readE = Files.readAllBytes(encPath); + String readS = new String(readE); + byte[] decB = Base58.decode(readS); + Files.write(decPath, decB); + return encPath.toString() + " " + decPath.toString(); + } + + private static String base58_check(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "base58_check.test"); + Path encPath = Path.of(workingPath, "base58_check.java.txt"); + Path decPath = Path.of(workingPath, "base58_check.java.back"); + byte[] rawB = Files.readAllBytes(testPath); + String encS = Base58.checkEncode(rawB); + Files.write(encPath, encS.getBytes()); + byte[] readE = Files.readAllBytes(encPath); + String readS = new String(readE); + byte[] decB = Base58.checkDecode(readS); + Files.write(decPath, decB); + return encPath.toString() + " " + decPath.toString(); + } + + private static String rlp(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "rlp.test"); + Path resPath = Path.of(workingPath, "rlp.java.back"); + byte[] encB = Files.readAllBytes(testPath); + + RLP.RLP_Data decRLP = RLP.decode(encB); + + RLP.RLP_List root = decRLP.asList(); + RLP.RLP_List sub = root.getItems().get(1).asList(); + byte[] anchor = sub.getItems().get(1).asItem().getBytes(); + + byte[] reEnc = RLP.encode(decRLP); + Files.write(resPath, reEnc); + + return new String(anchor) + "|" + resPath.toString(); + } + + private static String rlp_stream(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "rlp_stream.test"); + byte[] buffer = Files.readAllBytes(testPath); + + List hashes = new ArrayList<>(); + int offset = 0; + while (offset < buffer.length) { + RLP.DecodeResult res = RLP.decodeFrom(buffer, offset); + hashes.add(Integer.toString(res.data().hashCode())); // Just to verify we got objects + offset += res.consumed(); + } + return Integer.toString(hashes.size()); + } + + private static String rlp_fail(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "rlp_fail.test"); + byte[] buffer = Files.readAllBytes(testPath); + try { + RLP.decode(buffer); + return "SUCCESS"; + } catch (RLP.RLPException e) { + return "RLP_EXCEPTION: " + e.getMessage(); + } catch (Exception e) { + return "OTHER_EXCEPTION: " + e.getClass().getSimpleName(); + } + } + + private static String gaju_format(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "gaju_format.test"); + Path resPath = Path.of(workingPath, "gaju_format.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + + for (String line : lines) { + if (line.trim().isEmpty()) continue; + String[] p = line.split("\\|"); + String op = p[0]; + switch (op) { + case "amount" -> { + GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]); + GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]); + char sep = p[3].charAt(0); + int span = Integer.parseInt(p[4]); + BigInteger val = new BigInteger(p[5]); + results.add(GajuFormat.amount(new GajuFormat.FormatSpec(style, unit, sep, span), val)); + } + case "approx" -> { + GajuFormat.Type style = GajuFormat.Type.valueOf(p[1]); + GajuFormat.Unit unit = GajuFormat.Unit.valueOf(p[2]); + char sep = p[3].charAt(0); + int span = Integer.parseInt(p[4]); + BigInteger val = new BigInteger(p[5]); + int prec = Integer.parseInt(p[6]); + results.add(GajuFormat.approxAmount(new GajuFormat.FormatSpec(style, unit, sep, span), val, prec)); + } + case "read" -> { + byte[] b = GajuFormat.read(p[1]); + results.add(new BigInteger(b).toString()); + } + } + } + Files.write(resPath, results); + return resPath.toString(); + } + + private static String keccak256(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "keccak256.test"); + Path resPath = Path.of(workingPath, "keccak256.java.txt"); + byte[] input = Files.readAllBytes(testPath); + byte[] hash = Keccak256.hash(input); + Files.write(resPath, CryptoUtils.binToHex(hash).getBytes()); + return resPath.toString(); + } + + private static String blake2b(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "blake2b.test"); + Path resPath = Path.of(workingPath, "blake2b.java.txt"); + byte[] input = Files.readAllBytes(testPath); + byte[] hash = Blake2b.hash(input); + Files.write(resPath, CryptoUtils.binToHex(hash).getBytes()); + return resPath.toString(); + } + + private static String ed25519(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "ed25519.test"); + Path resPath = Path.of(workingPath, "ed25519.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + for (String line : lines) { + if (line.trim().isEmpty()) continue; + String[] parts = line.split("\\|"); + byte[] seed = CryptoUtils.hexToBin(parts[0]); + byte[] msg = CryptoUtils.hexToBin(parts[1]); + + byte[] pub = Ed25519.publicKey(seed); + byte[] sig = Ed25519.sign(seed, msg); + boolean verify = Ed25519.verify(pub, msg, sig); + + results.add(CryptoUtils.binToHex(pub) + "|" + CryptoUtils.binToHex(sig) + "|" + verify); + + CryptoUtils.wipe(seed); + CryptoUtils.wipe(msg); + CryptoUtils.wipe(pub); + CryptoUtils.wipe(sig); + } + Files.write(resPath, results); + return resPath.toString(); + } + + private static String ed25519_verify(String workingPath) throws IOException { + Path seedPath = Path.of(workingPath, "ed25519_seed.test"); + Path msgPath = Path.of(workingPath, "ed25519_msg.test"); + + byte[] seed = CryptoUtils.hexToBin(Files.readString(seedPath).trim()); + byte[] message = Files.readAllBytes(msgPath); + + byte[] pub = Ed25519.publicKey(seed); + byte[] sig = Ed25519.sign(seed, message); + boolean verify = Ed25519.verify(pub, message, sig); + + return CryptoUtils.binToHex(pub) + "|" + CryptoUtils.binToHex(sig) + "|" + verify; + } + + private static String api_encode(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "api_encode.test"); + Path resPath = Path.of(workingPath, "api_encode.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + for (String line : lines) { + if (line.trim().isEmpty()) continue; + String[] p = line.split("\\|"); + ApiEncoder.Type type = ApiEncoder.Type.valueOf(p[0]); + byte[] payload = CryptoUtils.hexToBin(p[1]); + results.add(ApiEncoder.encode(type, payload)); + if (type == ApiEncoder.Type.ACCOUNT_SECKEY) { + CryptoUtils.wipe(payload); + } + } + Files.write(resPath, results); + return resPath.toString(); + } + + private static String id_serialization(String workingPath) throws IOException { + Path testPath = Path.of(workingPath, "id_serialization.test"); + Path resPath = Path.of(workingPath, "id_serialization.java.txt"); + List lines = Files.readAllLines(testPath); + List results = new ArrayList<>(); + for (String line : lines) { + if (line.trim().isEmpty()) continue; + String[] p = line.split("\\|"); + Id.Tag tag = Id.Tag.fromValue(Integer.parseInt(p[0])); + byte[] val = CryptoUtils.hexToBin(p[1]); + Id id = new Id(tag, val); + results.add(CryptoUtils.binToHex(id.serialize())); + } + Files.write(resPath, results); + return resPath.toString(); + } +} diff --git a/gajumaru-core/test/ebin/gm_libtester.app b/test/ebin/gm_libtester.app similarity index 100% rename from gajumaru-core/test/ebin/gm_libtester.app rename to test/ebin/gm_libtester.app diff --git a/test/src/gmt.erl b/test/src/gmt.erl new file mode 100644 index 0000000..cd5d9e6 --- /dev/null +++ b/test/src/gmt.erl @@ -0,0 +1,588 @@ +%%% @doc +%%% Gajumaru Java Lib Tester: gmt +%%% +%%% This module provides an inter-language test suite for the Gajumaru core +%%% Java libraries. It tests the Java implementation against the canonical +%%% Erlang implementation by generating random test vectors and comparing +%%% the outputs. +%%% @end + +-module(gmt). +-vsn("0.1.0"). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("LGPL-3.0-or-later"). + +-export([start/1]). + + +%%% Logic + +mods() -> + #{"base64" => fun base64/0, + "base58" => fun base58/0, + "base58_check" => fun base58_check/0, + "rlp" => fun rlp/0, + "rlp_stream" => fun rlp_stream/0, + "rlp_fail" => fun rlp_fail/0, + "gaju_format" => fun gaju_format/0, + "keccak256" => fun keccak256/0, + "blake2b" => fun blake2b/0, + "ed25519" => fun ed25519/0, + "api_encode" => fun api_encode/0, + "id_serialization" => fun id_serialization/0, + "fe_parity" => fun fe_parity/0, + "reduce_parity" => fun reduce_parity/0, + "smb_parity" => fun smb_parity/0, + "femul_parity" => fun femul_parity/0, + "frombytes_parity" => fun frombytes_parity/0, + "ge_parity" => fun ge_parity/0}. + + +start([]) -> + Tests = mods(), + ok = run(Tests), + zx:silent_stop(); +start(["list"]) -> + ok = io:format("Available tests:~n"), + ok = lists:foreach(fun display/1, maps:keys(mods())), + zx:silent_stop(); +start(Mods) -> + Available = mods(), + Tests = maps:with(Mods, Available), + ok = + case maps:size(Tests) =:= length(Mods) of + true -> + run(Tests); + false -> + NotMods = lists:subtract(Mods, maps:keys(Available)), + ok = io:format("The following arguments are not testable module names:~n"), + lists:foreach(fun display/1, NotMods) + end, + zx:silent_stop(). + +display(Name) -> + io:format(" ~ts~n", [Name]). + +run(Tests) -> + {ok, Cwd} = file:get_cwd(), + ok = + case filename:basename(Cwd) of + "test" -> file:set_cwd(".."); + _ -> ok + end, + + ok = clean(), + ok = build(), + Results = maps:map(fun run/2, Tests), + io:format("~nFinal Results:~n ~tp~n", [Results]). + +run(Name, Test) -> + ok = io:format("~nRunning: ~ts...~n", [Name]), + Test(). + +clean() -> + Temp = "test/temp", + lists:foreach(fun(D) -> ok = clean(D) end, [Temp]). + +clean(Dir) -> + case file:del_dir_r(Dir) of + ok -> ok; + {error, enoent} -> ok; + Error -> Error + end. + +build() -> + Out = os:cmd("bin/compile"), + io:format("Compile: ~ts", [Out]). + +temp_dir() -> + {ok, Cwd} = file:get_cwd(), + filename:join(Cwd, "test/temp"). + +trim(S) -> + Unprintable = fun(C) -> C =< 32 end, + lists:reverse(lists:dropwhile(Unprintable, lists:reverse(lists:dropwhile(Unprintable, S)))). + + + +%%% Test Modules + +base64() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "base64.test"), + ConvFile = filename:join(Temp, "base64.erlang.txt"), + ok = filelib:ensure_dir(TestFile), + ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))), + {ok, B} = file:read_file(TestFile), + Base64 = base64:encode(B), + ok = file:write_file(ConvFile, Base64), + Run = "bin/run base64 " ++ Temp, + Out = trim(os:cmd(Run)), + [JEnc, JDec] = string:split(Out, " "), + {ok, EEncB} = file:read_file(ConvFile), + {ok, JEncB} = file:read_file(JEnc), + EHash = crypto:hash(sha512, EEncB), + JHash = crypto:hash(sha512, JEncB), + {ok, EDecB} = file:read_file(TestFile), + {ok, JDecB} = file:read_file(JDec), + EBinHash = crypto:hash(sha512, EDecB), + JBinHash = crypto:hash(sha512, JDecB), + EHash =:= JHash andalso EBinHash =:= JBinHash. + + +base58() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "base58.test"), + ConvFile = filename:join(Temp, "base58.erlang.txt"), + ok = filelib:ensure_dir(TestFile), + ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))), + {ok, B} = file:read_file(TestFile), + Base58 = base58:binary_to_base58(B), + ok = file:write_file(ConvFile, Base58), + Run = "bin/run base58 " ++ Temp, + Out = trim(os:cmd(Run)), + [JEnc, JDec] = string:split(Out, " "), + {ok, EEncB} = file:read_file(ConvFile), + {ok, JEncB} = file:read_file(JEnc), + EHash = crypto:hash(sha512, EEncB), + JHash = crypto:hash(sha512, JEncB), + {ok, EDecB} = file:read_file(TestFile), + {ok, JDecB} = file:read_file(JDec), + EBinHash = crypto:hash(sha512, EDecB), + JBinHash = crypto:hash(sha512, JDecB), + EHash =:= JHash andalso EBinHash =:= JBinHash. + + +base58_check() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "base58_check.test"), + ConvFile = filename:join(Temp, "base58_check.erlang.txt"), + ok = filelib:ensure_dir(TestFile), + ok = file:write_file(TestFile, rand:bytes(rand:uniform(5000))), + {ok, B} = file:read_file(TestFile), + Checksum = binary:part(crypto:hash(sha256, crypto:hash(sha256, B)), 0, 4), + Base58C = base58:binary_to_base58(<>), + ok = file:write_file(ConvFile, Base58C), + Run = "bin/run base58_check " ++ Temp, + Out = trim(os:cmd(Run)), + [JEnc, JDec] = string:split(Out, " "), + {ok, EEncB} = file:read_file(ConvFile), + {ok, JEncB} = file:read_file(JEnc), + EHash = crypto:hash(sha512, EEncB), + JHash = crypto:hash(sha512, JEncB), + {ok, EDecB} = file:read_file(TestFile), + {ok, JDecB} = file:read_file(JDec), + EBinHash = crypto:hash(sha512, EDecB), + JBinHash = crypto:hash(sha512, JDecB), + EHash =:= JHash andalso EBinHash =:= JBinHash. + + +rlp() -> + Temp = temp_dir(), + Anchor = <<"I thought what I'd do was, I'd pretend I was one of those deaf-mutes.">>, + Data = + [rand:bytes(rand:uniform(20)), + [rand:bytes(rand:uniform(20)), + Anchor, + rand:bytes(rand:uniform(20)), + rand:bytes(rand:uniform(5000))], + rand:bytes(rand:uniform(2000))], + RLP = gmser_rlp:encode(Data), + RLP_File = filename:join(Temp, "rlp.test"), + ok = filelib:ensure_dir(RLP_File), + ok = file:write_file(RLP_File, RLP), + Run = "bin/run rlp " ++ Temp, + Out = trim(os:cmd(Run)), + case string:split(Out, "|") of + [Found, JPath] -> + JPathTrimmed = trim(JPath), + {ok, EEncB} = file:read_file(RLP_File), + case file:read_file(JPathTrimmed) of + {ok, JEncB} -> + EHash = crypto:hash(sha512, EEncB), + JHash = crypto:hash(sha512, JEncB), + EHash =:= JHash andalso Found =:= unicode:characters_to_list(Anchor); + {error, R} -> + ok = io:format("Failed to read RLP Java result: ~tp (Path: ~tp)~n", [R, JPathTrimmed]), + false + end; + _ -> + ok = io:format("RLP output mismatch: ~tp~n", [Out]), + false + end. + + +rlp_stream() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "rlp_stream.test"), + ok = filelib:ensure_dir(TestFile), + Data = [rand:bytes(rand:uniform(100)) || _ <- lists:seq(1, 10)], + RLP = << <<(gmser_rlp:encode(D))/binary>> || D <- Data >>, + ok = file:write_file(TestFile, RLP), + Run = "bin/run rlp_stream " ++ Temp, + Out = trim(os:cmd(Run)), + Out =:= "10". + + +rlp_fail() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "rlp_fail.test"), + ok = filelib:ensure_dir(TestFile), + Data = [<<"item1">>, <<"item2">>], + FullRLP = gmser_rlp:encode(Data), + TruncRLP = binary:part(FullRLP, 0, byte_size(FullRLP) - 2), + ok = file:write_file(TestFile, TruncRLP), + Run = "bin/run rlp_fail " ++ Temp, + Out = trim(os:cmd(Run)), + string:prefix(Out, "RLP_EXCEPTION:") =/= nomatch. + + +gaju_format() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "gaju_format.test"), + ok = filelib:ensure_dir(TestFile), + Cases = [gen_case() || _ <- lists:seq(1, 100)], + Lines = [serialize_case(C) || C <- Cases], + ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])), + Run = "bin/run gaju_format " ++ Temp, + RawOut = os:cmd(Run), + JavaResPath = trim(RawOut), + case file:read_file(JavaResPath) of + {ok, ResContent} -> + JavaResults = string:split(trim(unicode:characters_to_list(ResContent)), "\n", all), + length(Cases) =:= length(JavaResults) andalso compare_results(Cases, JavaResults); + {error, Reason} -> + Format = "Failed to read Java results from: ~tp (Reason: ~tp)~nRaw Output: ~ts~n", + io:format(Format, [JavaResPath, Reason, RawOut]), + false + end. + + +keccak256() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "keccak256.test"), + ResFile = filename:join(Temp, "keccak256.erlang.txt"), + Data = rand:bytes(rand:uniform(5000)), + ok = filelib:ensure_dir(TestFile), + ok = file:write_file(TestFile, Data), + Hash = sha3:hash(256, Data), + HexHash = bin_to_hex(Hash), + ok = file:write_file(ResFile, HexHash), + Run = "bin/run keccak256 " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOut} = file:read_file(trim(Out)), + unicode:characters_to_list(JOut) =:= HexHash. + + +blake2b() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "blake2b.test"), + ResFile = filename:join(Temp, "blake2b.erlang.txt"), + Data = rand:bytes(rand:uniform(5000)), + ok = filelib:ensure_dir(TestFile), + ok = file:write_file(TestFile, Data), + {ok, Hash} = eblake2:blake2b(32, Data), + HexHash = bin_to_hex(Hash), + ok = file:write_file(ResFile, HexHash), + Run = "bin/run blake2b " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOut} = file:read_file(trim(Out)), + unicode:characters_to_list(JOut) =:= HexHash. + + +ed25519() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "ed25519.test"), + ok = filelib:ensure_dir(TestFile), + Cases = [{rand:bytes(32), rand:bytes(rand:uniform(100))} || _ <- lists:seq(1, 20)], + ok = file:write_file(TestFile, [[bin_to_hex(S), "|", bin_to_hex(M), "\n"] || {S, M} <- Cases]), + Sequence = + fun({S, M}) -> + #{public := Pub} = ecu_eddsa:sign_seed_keypair(S), + Sig = ecu_eddsa:sign_detached(M, S), + V = ecu_eddsa:sign_verify_detached(Sig, M, Pub), + lists:flatten(io_lib:format("~s|~s|~p", [bin_to_hex(Pub), bin_to_hex(Sig), V])) + end, + Expected = lists:map(Sequence, Cases), + Run = "bin/run ed25519 " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOutContent} = file:read_file(trim(Out)), + JResults = [trim(L) || L <- string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all)], + same_same(Expected, JResults). + + +api_encode() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "api_encode.test"), + Types = [account_pubkey, account_seckey, tx_hash, contract_pubkey, signature, commitment, peer_pubkey], + Cases = [{T, rand:bytes(type_size(T))} || T <- Types], + Lines = [io_lib:format("~ts|~ts", [string:uppercase(atom_to_list(T)), bin_to_hex(B)]) || {T, B} <- Cases], + ok = filelib:ensure_dir(TestFile), + ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])), + Expected = [flatten(gmser_api_encoder:encode(T, B)) || {T, B} <- Cases], + Run = "bin/run api_encode " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOutContent} = file:read_file(trim(Out)), + JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all), + same_same(Expected, JResults). + +type_size(signature) -> 64; +type_size(_) -> 32. + + +id_serialization() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "id_serialization.test"), + Tags = [1, 2, 3, 5, 6, 7, 9], + Cases = [{Tag, rand:bytes(32)} || Tag <- Tags], + Lines = [io_lib:format("~b|~ts", [Tag, bin_to_hex(B)]) || {Tag, B} <- Cases], + ok = filelib:ensure_dir(TestFile), + ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])), + Expected = [bin_to_hex(gmser_id:encode(gmser_id:decode(<>))) || {T, B} <- Cases], + Run = "bin/run id_serialization " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOutContent} = file:read_file(trim(Out)), + JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all), + same_same(Expected, JResults). + + +fe_parity() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "fe.test"), + ok = filelib:ensure_dir(TestFile), + % Mask to 255 bits to avoid bit-255 sign bit ambiguity in fe_frombytes + RandomLittleFingers = + fun() -> + <> = rand:bytes(32), + bin_to_hex(<<(B band ((1 bsl 255) - 1)):256/little>>) + end, + Inputs = [RandomLittleFingers() || _ <- lists:seq(1, 100)], + ok = file:write_file(TestFile, unicode:characters_to_binary([[I, "\n"] || I <- Inputs])), + Run = "bin/run fe_parity " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOutContent} = file:read_file(trim(Out)), + JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all), + P = ecu_ed25519:p(), + Pee = + fun(I) -> + <> = hex_to_bin(I), + bin_to_hex(pack_p(B rem P)) + end, + Expected = lists:map(Pee, Inputs), + same_same(Expected, JResults). + + +reduce_parity() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "reduce.test"), + ok = filelib:ensure_dir(TestFile), + Inputs = [rand:bytes(64) || _ <- lists:seq(1, 100)], + ok = file:write_file(TestFile, unicode:characters_to_binary([[bin_to_hex(I), "\n"] || I <- Inputs])), + Run = "bin/run reduce_parity " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOutContent} = file:read_file(trim(Out)), + JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all), + Expected = [bin_to_hex(ecu_ed25519:scalar_reduce(I)) || I <- Inputs], + same_same(Expected, JResults). + + +smb_parity() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "smb.test"), + ok = filelib:ensure_dir(TestFile), + Inputs = [<<(rand:bytes(31))/binary, 0>> || _ <- lists:seq(1, 10)], + ok = file:write_file(TestFile, unicode:characters_to_binary([[bin_to_hex(I), "\n"] || I <- Inputs])), + Run = "bin/run smb_parity " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOutContent} = file:read_file(trim(Out)), + JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all), + Expected = [bin_to_hex(ecu_ed25519:compress(ecu_ed25519:scalar_mul_base_noclamp(I))) || I <- Inputs], + same_same(Expected, JResults). + + +femul_parity() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "femul.test"), + ok = filelib:ensure_dir(TestFile), + % Mask to 255 bits + Gen = fun() -> <> = rand:bytes(32), << (B band ((1 bsl 255) - 1)):256/little >> end, + Inputs = [{Gen(), Gen()} || _ <- lists:seq(1, 10)], + Lines = [bin_to_hex(A) ++ "|" ++ bin_to_hex(B) || {A, B} <- Inputs], + ok = file:write_file(TestFile, unicode:characters_to_binary([[L, "\n"] || L <- Lines])), + Run = "bin/run femul_parity " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOutContent} = file:read_file(trim(Out)), + JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all), + ExpectedBin = [bin_to_hex(pack_p(ecu_ed25519:f_mul(binary:decode_unsigned(A, little), binary:decode_unsigned(B, little)))) || {A, B} <- Inputs], + same_same3(Inputs, ExpectedBin, JResults). + + +same_same3(AB, L1, L2) -> + same_same3(AB, L1, L2, true). + +same_same3([_ | R1], [S | R2], [S | R3], Result) -> + same_same3(R1, R2, R3, Result); +same_same3([{A, B} | R1], [E | R2], [J | R3], _) -> + ok = io:format("A: ~ts~nB: ~ts~nE: ~ts~nJ: ~ts~n", [bin_to_hex(A), bin_to_hex(B), E, J]), + same_same3(R1, R2, R3, false); +same_same3([], [], [], Result) -> + Result. + + +same_same(L1, L2) -> + same_same(L1, L2, true). + +same_same([S | R1], [S | R2], Result) -> + same_same(R1, R2, Result); +same_same([E | R1], [J | R2], _) -> + ok = io:format("Mismatch!\nE: ~ts\nJ: ~ts\n", [E, J]), + same_same(R1, R2, false); +same_same([], [], Result) -> + Result. + + +frombytes_parity() -> + Temp = temp_dir(), + TestFile = filename:join(Temp, "frombytes.test"), + ok = filelib:ensure_dir(TestFile), + Inputs = [rand:bytes(32) || _ <- lists:seq(1, 10)], + ok = file:write_file(TestFile, unicode:characters_to_binary([[bin_to_hex(I), "\n"] || I <- Inputs])), + Run = "bin/run frombytes_parity " ++ Temp, + Out = trim(os:cmd(Run)), + {ok, JOutContent} = file:read_file(trim(Out)), + JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all), + Limb = + fun(I) -> + Val = binary:decode_unsigned(I, little), + Limbs = + fun + F(V, Idx) when Idx < 10 -> + Size = + case Idx rem 2 =:= 0 of + true -> 26; + false -> 25 + end, + L = V band ((1 bsl Size) - 1), + [L | F(V bsr Size, Idx + 1)]; + F(_, _) -> + [] + end, + string:join([integer_to_list(L) || L <- Limbs(Val, 0)], ",") + end, + Expected = lists:map(Limb, Inputs), + same_same(Expected, JResults). + + +ge_parity() -> + A_bin = <<1, 2, 3, 0:232>>, + B_bin = <<4, 5, 6, 0:232>>, + P1_erl = ecu_ed25519:scalar_mul_base_noclamp(A_bin), + P2_erl = ecu_ed25519:scalar_mul_base_noclamp(B_bin), + P3_erl = ecu_ed25519:p_add(P1_erl, P2_erl), + E_comp = bin_to_hex(ecu_ed25519:compress(P3_erl)), + Run = "bin/run ge_parity " ++ bin_to_hex(A_bin) ++ " " ++ bin_to_hex(B_bin), + Out = trim(os:cmd(Run)), + case string:split(Out, "|||") of + [JX | _] -> E_comp =:= trim(JX); + _ -> false + end. + +pack_p(V) -> + P = (1 bsl 255) - 19, + V_pos = if V < 0 -> V + P; true -> V rem P end, + Enc = binary:encode_unsigned(V_pos, little), + Size = byte_size(Enc), + if Size < 32 -> <>; true -> Enc end. + +bin_to_hex(Bin) -> + lists:flatten([io_lib:format("~2.16.0b", [X]) || X <- binary_to_list(Bin)]). + +hex_to_bin(S) -> + hex_to_bin(S, []). +hex_to_bin([], Acc) -> + list_to_binary(lists:reverse(Acc)); +hex_to_bin([X,Y|T], Acc) -> + {ok, [V], []} = io_lib:fread("~16u", [X,Y]), + hex_to_bin(T, [V | Acc]). + + + +%%% Generatorators + +gen_case() -> + Ops = [amount, approx, read], + Op = lists:nth(rand:uniform(length(Ops)), Ops), + gen_case(Op). + +gen_case(read) -> + Pucks = random_pucks(12), + Style = random_style(), + {read, hz_format:amount(gaju, Style, Pucks), Pucks}; +gen_case(amount) -> + Unit = random_unit(), + Pucks = + case Unit of + gaju -> random_pucks(12); + puck -> random_pucks(8) + end, + Style = random_style(), + Sep = lists:nth(rand:uniform(2), [$,, $_]), + Span = rand:uniform(4), + Amount = hz_format:amount(Unit, hz_style(Style, Sep, Span), Pucks), + {amount, Style, Unit, Sep, Span, Pucks, Amount}; +gen_case(approx) -> + Pucks = random_pucks(12), + Sep = lists:nth(rand:uniform(2), [$,, $_]), + Span = rand:uniform(2) + 2, + Prec = rand:uniform(18), + Approx = hz_format:approx_amount({Sep, Span}, Prec, Pucks), + {approx, us, gaju, Sep, Span, Pucks, Prec, Approx}. + +random_pucks(MaxBytes) -> + Bytes = rand:bytes(rand:uniform(MaxBytes)), + crypto:bytes_to_integer(Bytes). + +random_style() -> + lists:nth(rand:uniform(4), [us, jp, metric, legacy]). + +random_unit() -> + lists:nth(rand:uniform(2), [gaju, puck]). + +hz_style(us, Sep, Span) -> {Sep, Span}; +hz_style(Style, _, _) -> Style. + +serialize_case({amount, Style, Unit, Sep, Span, Pucks, _}) -> + FStyle = string:uppercase(atom_to_list(Style)), + FUnit = string:uppercase(atom_to_list(Unit)), + Stuff = [FStyle, FUnit, Sep, Span, Pucks], + io_lib:format("amount|~ts|~ts|~c|~b|~b", Stuff); +serialize_case({approx, Style, Unit, Sep, Span, Pucks, Prec, _}) -> + FStyle = string:uppercase(atom_to_list(Style)), + FUnit = string:uppercase(atom_to_list(Unit)), + Stuff = [FStyle, FUnit, Sep, Span, Pucks, Prec], + io_lib:format("approx|~ts|~ts|~c|~b|~b|~b", Stuff); +serialize_case({read, Input, _}) -> + InputStr = unicode:characters_to_list(Input), + io_lib:format("read|~ts", [InputStr]). + +compare_results([], []) -> + true; +compare_results([Case | Cases], [Result | Results]) -> + case check_case(Case, Result) of + true -> + compare_results(Cases, Results); + false -> + io:format("Parity failure!~nCase: ~tp~nJava Result: ~tp~n", [Case, Result]), + false + end. + +check_case({amount, _, _, _, _, _, Expected}, Result) -> + flatten(Expected) =:= flatten(Result); +check_case({approx, _, _, _, _, _, _, Expected}, Result) -> + flatten(Expected) =:= flatten(Result); +check_case({read, _, Expected}, Result) -> + integer_to_list(Expected) =:= flatten(Result). + +flatten(B) when is_binary(B) -> unicode:characters_to_list(B); +flatten(L) when is_list(L) -> unicode:characters_to_list(L). diff --git a/gajumaru-core/test/zomp.meta b/test/zomp.meta similarity index 92% rename from gajumaru-core/test/zomp.meta rename to test/zomp.meta index 31ba140..96a17f1 100644 --- a/gajumaru-core/test/zomp.meta +++ b/test/zomp.meta @@ -6,7 +6,8 @@ {author,"Craig Everett"}. {desc,"An inter-language test suite for Gajumaru core libs"}. {package_id,{"qpq","gm_libtester",{0,1,0}}}. -{deps,[{"otpr","hakuzaru",{0,9,1}}, +{deps,[{"otpr","sha3",{0,1,4}}, + {"otpr","hakuzaru",{0,9,1}}, {"otpr","getopt",{1,0,2}}, {"otpr","zj",{1,1,0}}, {"otpr","ec_utils",{1,0,0}},