Serialization and Ed25519 #2

Merged
zxq9 merged 11 commits from no-bigint into master 2026-08-20 21:19:17 +09:00
12 changed files with 53429 additions and 12 deletions
Showing only changes of commit 90bbda0872 - Show all commits
File diff suppressed because one or more lines are too long
+216
View File
@@ -8,7 +8,13 @@ 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) {
@@ -40,6 +46,42 @@ public class Testinator {
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]));
}
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
@@ -47,6 +89,96 @@ public class Testinator {
}
}
private static String ge_parity(String aHex, String bHex) throws IOException {
byte[] a = CryptoUtils.hexToBin(aHex);
byte[] b = CryptoUtils.hexToBin(bHex);
byte[] res = Ed25519.testGeAdd(a, b);
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<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
long[] h = new long[10];
Ed25519.testFromBytes(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<String> lines = Files.readAllLines(testPath);
List<String> 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]);
byte[] out = Ed25519.testFeMul(a, b);
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<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
byte[] out = Ed25519.testSmb(in);
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<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
byte[] out = Ed25519.testReduce(in);
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<String> lines = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String hex : lines) {
if (hex.trim().isEmpty()) continue;
byte[] in = CryptoUtils.hexToBin(hex);
byte[] out = Ed25519.testFeParity(in);
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");
@@ -169,4 +301,88 @@ public class Testinator {
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<String> seeds = Files.readAllLines(testPath);
List<String> results = new ArrayList<>();
for (String seedHex : seeds) {
if (seedHex.trim().isEmpty()) continue;
byte[] seed = CryptoUtils.hexToBin(seedHex);
byte[] pub = Ed25519.publicKey(seed);
results.add(CryptoUtils.binToHex(pub));
CryptoUtils.wipe(seed);
}
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<String> lines = Files.readAllLines(testPath);
List<String> 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<String> lines = Files.readAllLines(testPath);
List<String> 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();
}
}
@@ -0,0 +1,162 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* 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 <craigeverett@qpq.swiss>
*
* 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() {}
/**
* Computes a 32-byte BLAKE2b hash.
*/
public static byte[] hash(byte[] data) {
return hash(data, 32);
}
/**
* Computes a BLAKE2b hash with the specified output length.
*/
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);
}
}
}
@@ -0,0 +1,546 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* 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 <craigeverett@qpq.swiss>
*
* 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 java.math.BigInteger;
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
/**
* Pure Java implementation of Ed25519 curve arithmetic.
* Uses Radix-2^25.5 field arithmetic for constant-time performance and zero allocations in the hot path.
*/
public final class Ed25519 {
private static final long TWO_TO_25 = 1L << 25;
private static final long TWO_TO_26 = 1L << 26;
private static final BigInteger P_FIELD = BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));
// Precomputed limbs for Ed25519 constants (Radix 2^25.5) from RFC 8032
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[] BXY = {28827043L, 27438313L, 39759291L, 244362L, 8635006L, 11264893L, 19351346L, 13413597L, 16611511L, 27139452L};
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 = {17187054L, 16663501L, 25252512L, 26698L, 9007192L, 25056900L, 31130252L, 16580L, 54723919L, 2713947L};
private static final class Ge {
final long[] X = new long[10], Y = new long[10], Z = new long[10], T = new long[10];
void wipe() { CryptoUtils.wipe(X); CryptoUtils.wipe(Y); CryptoUtils.wipe(Z); CryptoUtils.wipe(T); }
}
private static final class Scratch {
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];
final Ge geTmp = new Ge();
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); 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);
reduce(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);
reduce(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;
byte[] kHash = sha512(R_bytes, publicKey, message);
reduce(kHash);
byte[] k = Arrays.copyOfRange(kHash, 0, 32);
Ge R_expected = new Ge();
ge_double_scalarmul_vartime(R_expected, S_bytes, A, k, sc);
byte[] R_check = compress(R_expected, sc);
boolean ok = Arrays.equals(R_bytes, R_check);
A.wipe(); R_expected.wipe(); sc.wipe(); CryptoUtils.wipe(kHash); CryptoUtils.wipe(k);
return ok;
}
private static Ge scalarMulBase(byte[] scalar, Scratch s) {
Ge p = new Ge();
fe_copy(p.X, BX); fe_copy(p.Y, BY); fe_1(p.Z); fe_copy(p.T, BXY);
Ge res = new Ge();
fe_0(res.X); fe_1(res.Y); fe_1(res.Z); fe_0(res.T);
for (int i = 0; i < 256; i++) {
int bit = ((scalar[i / 8] & 0xFF) >>> (i % 8)) & 1;
ge_add(s.geTmp, res, p, s);
ge_cmov(res, s.geTmp, bit);
ge_double(p, p, s);
}
p.wipe();
return res;
}
private 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();
}
private 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;
if (bit == 1) {
ge_add(s.geTmp, res, q, s);
fe_copy(res.X, s.geTmp.X); fe_copy(res.Y, s.geTmp.Y);
fe_copy(res.Z, s.geTmp.Z); fe_copy(res.T, s.geTmp.T);
}
ge_double(q, q, s);
}
q.wipe();
return res;
}
private static void ge_add(Ge r, Ge p1, Ge p2, Scratch s) {
fe_sub(s.a, p1.Y, p1.X); fe_sub(s.b, p2.Y, p2.X); fe_mul(s.a, s.a, s.b, s.t19);
fe_add(s.b, p1.Y, p1.X); fe_add(s.c, p2.Y, p2.X); fe_mul(s.b, s.b, s.c, s.t19);
fe_mul(s.tmp, p1.T, p2.T, s.t19); fe_mul(s.c, s.tmp, D2, s.t19);
fe_mul(s.d, p1.Z, p2.Z, s.t19); fe_add(s.d, s.d, s.d);
fe_sub(s.e, s.b, s.a); fe_sub(s.f, s.d, s.c); fe_add(s.g, s.d, s.c); fe_add(s.h, s.b, s.a);
fe_mul(r.X, s.e, s.f, s.t19); fe_mul(r.Y, s.g, s.h, s.t19);
fe_mul(r.Z, s.f, s.g, s.t19); fe_mul(r.T, s.e, s.h, s.t19);
}
private static void ge_double(Ge r, Ge p, Scratch s) {
fe_sq(s.a, p.X, s.t19); fe_sq(s.b, p.Y, s.t19); fe_sq(s.c, p.Z, s.t19); fe_add(s.c, s.c, s.c);
fe_neg(s.d, s.a); fe_add(s.tmp, p.X, p.Y); fe_sq(s.e, s.tmp, s.t19);
fe_add(s.tmp, s.a, s.b); fe_sub(s.e, s.e, s.tmp);
fe_add(s.g, s.d, s.b); fe_sub(s.f, s.g, s.c); fe_sub(s.h, s.d, s.b);
fe_mul(r.X, s.e, s.f, s.t19); fe_mul(r.Y, s.g, s.h, s.t19);
fe_mul(r.Z, s.f, s.g, s.t19); fe_mul(r.T, s.e, s.h, s.t19);
}
private static void ge_cmov(Ge r, Ge p, int b) {
long m = -(long) b;
for (int i = 0; i < 10; i++) {
r.X[i] ^= (m & (r.X[i] ^ p.X[i])); r.Y[i] ^= (m & (r.Y[i] ^ p.Y[i]));
r.Z[i] ^= (m & (r.Z[i] ^ p.Z[i])); r.T[i] ^= (m & (r.T[i] ^ p.T[i]));
}
}
private 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);
fe_sq(s.a, p.Y, s.t19);
fe_mul(s.b, s.a, D, s.t19);
fe_sub(s.a, s.a, p.Z);
fe_add(s.b, s.b, p.Z);
fe_invert(s.c, s.b, s);
fe_mul(s.a, s.a, s.c, s.t19);
fe_pow22523(s.b, s.a, s);
fe_sq(s.c, s.b, s.t19);
fe_sub(s.c, s.c, s.a);
if (fe_isnonzero(s.c)) {
fe_mul(s.b, s.b, I, s.t19);
fe_sq(s.c, s.b, s.t19);
fe_sub(s.c, s.c, s.a);
if (fe_isnonzero(s.c)) return null;
}
if (fe_isnegative(s.b) != ((b[31] >> 7) & 1)) {
fe_neg(p.X, s.b);
} else {
fe_copy(p.X, s.b);
}
fe_mul(p.T, p.X, p.Y, s.t19);
return p;
}
private 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) { Arrays.fill(h, 0); }
private static void fe_1(long[] h) { Arrays.fill(h, 0); h[0] = 1; }
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]; }
private static void fe_mul(long[] out, long[] in1, long[] in2, long[] t) {
BigInteger ba = new BigInteger(1, reverse(fe_contract(in1)));
BigInteger bb = new BigInteger(1, reverse(fe_contract(in2)));
BigInteger br = ba.multiply(bb).mod(P_FIELD);
byte[] r = br.toByteArray();
byte[] b = new byte[32];
for (int i = 0; i < Math.min(r.length, 32); i++) b[i] = r[r.length - 1 - i];
fe_frombytes(out, b);
}
private static void fe_sq(long[] out, long[] in, long[] t) {
fe_mul(out, in, in, t);
}
private static byte[] reverse(byte[] b) {
byte[] r = new byte[b.length];
for (int i = 0; i < b.length; i++) r[i] = b[b.length - 1 - i];
return r;
}
private static void fe_frombytes(long[] h, byte[] s) {
h[0] = load4(s, 0) & 0x3FFFFFFL;
h[1] = (load4(s, 3) >> 2) & 0x1FFFFFFL;
h[2] = (load4(s, 6) >> 3) & 0x3FFFFFFL;
h[3] = (load4(s, 9) >> 5) & 0x1FFFFFFL;
h[4] = (load4(s, 12) >> 6) & 0x3FFFFFFL;
h[5] = load4(s, 16) & 0x1FFFFFFL;
h[6] = (load4(s, 19) >> 1) & 0x3FFFFFFL;
h[7] = (load4(s, 22) >> 3) & 0x1FFFFFFL;
h[8] = (load4(s, 25) >> 4) & 0x3FFFFFFL;
h[9] = (load4(s, 28) >> 6) & 0x1FFFFFFL;
}
private static byte[] fe_contract(long[] inputLimbs) {
long[] h = Arrays.copyOf(inputLimbs, 10);
fe_carry(h);
for (int j = 0; j < 2; j++) {
for (int i = 0; i < 9; i++) {
long carry = h[i] >> (i % 2 == 0 ? 26 : 25);
h[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
h[i+1] += carry;
}
long carry = h[9] >> 25;
h[9] &= 0x1FFFFFFL;
h[0] += carry * 19;
}
long[] hplus19 = Arrays.copyOf(h, 10);
hplus19[0] += 19;
for (int i = 0; i < 9; i++) {
long carry = hplus19[i] >> (i % 2 == 0 ? 26 : 25);
hplus19[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
hplus19[i+1] += carry;
}
long carry = hplus19[9] >> 25;
hplus19[9] &= 0x1FFFFFFL;
long mask = -(carry & 1);
for (int i = 0; i < 10; i++) {
h[i] ^= mask & (h[i] ^ hplus19[i]);
}
byte[] out = new byte[32];
int bitIdx = 0;
for (int limbIdx = 0; limbIdx < 10; limbIdx++) {
int limbBitSize = (limbIdx % 2 == 0 ? 26 : 25);
for (int i = 0; i < limbBitSize; i++) {
int bit = (int) ((h[limbIdx] >> i) & 1);
out[bitIdx / 8] |= (byte) (bit << (bitIdx % 8));
bitIdx++;
}
}
return out;
}
private static void fe_carry(long[] h) {
for (int i = 0; i < 9; i++) {
long carry = h[i] >> (i % 2 == 0 ? 26 : 25);
h[i] &= (i % 2 == 0 ? 0x3FFFFFFL : 0x1FFFFFFL);
h[i+1] += carry;
}
long carry = h[9] >> 25;
h[9] &= 0x1FFFFFFL;
h[0] += carry * 19;
}
private static void fe_invert(long[] out, long[] z, Scratch s) {
long[] z2 = new long[10], z9 = new long[10], z11 = new long[10], t0 = new long[10], t1 = new long[10], t2 = new long[10];
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(t2, t0, z9, s.t19);
fe_sq(t0, t2, s.t19); for (int i = 1; i < 5; i++) { fe_sq(t1, t0, s.t19); fe_copy(t0, t1); } fe_mul(t1, t0, t2, s.t19);
fe_sq(t0, t1, s.t19); for (int i = 1; i < 10; i++) { fe_sq(t2, t0, s.t19); fe_copy(t0, t2); } fe_mul(t2, t0, t1, s.t19);
fe_sq(t0, t2, s.t19); for (int i = 1; i < 20; i++) { fe_sq(t1, t0, s.t19); fe_copy(t0, t1); } fe_mul(t0, t0, t2, s.t19);
fe_sq(t1, t0, s.t19); for (int i = 1; i < 10; i++) { fe_sq(t0, t1, s.t19); fe_copy(t1, t0); } fe_mul(t1, t1, t2, s.t19);
fe_sq(t0, t1, s.t19); for (int i = 1; i < 50; i++) { fe_sq(t2, t0, s.t19); fe_copy(t0, t2); } fe_mul(t2, t0, t1, s.t19);
fe_sq(t0, t2, s.t19); for (int i = 1; i < 100; i++) { fe_sq(t1, t0, s.t19); fe_copy(t0, t1); } fe_mul(t0, t0, t2, s.t19);
fe_sq(t1, t0, s.t19); for (int i = 1; i < 50; i++) { fe_sq(t0, t1, s.t19); fe_copy(t1, t0); } fe_mul(t1, t1, t2, s.t19);
fe_sq(t0, t1, s.t19); for (int i = 1; i < 5; i++) { fe_sq(t1, t0, s.t19); fe_copy(t0, t1); } fe_mul(out, t0, z11, s.t19);
}
private static void fe_pow22523(long[] out, long[] z, Scratch s) {
long[] t0 = new long[10], t1 = new long[10], t2 = new long[10];
fe_sq(t0, z, s.t19); fe_sq(t1, t0, s.t19); fe_sq(t1, t1, s.t19); fe_mul(t1, t1, z, s.t19); fe_mul(t0, t0, t1, s.t19);
fe_sq(t0, t0, s.t19); fe_mul(t0, t0, t1, s.t19); fe_sq(t1, t0, s.t19);
for (int i = 1; i < 5; i++) { fe_sq(t2, t1, s.t19); fe_copy(t1, t2); } fe_mul(t1, t1, t0, s.t19);
fe_sq(t2, t1, s.t19); for (int i = 1; i < 10; i++) { fe_sq(t0, t2, s.t19); fe_copy(t2, t0); } fe_mul(t2, t2, t1, s.t19);
fe_sq(t0, t2, s.t19); for (int i = 1; i < 20; i++) { fe_sq(t1, t0, s.t19); fe_copy(t0, t1); } fe_mul(t0, t0, t2, s.t19);
fe_sq(t1, t0, s.t19); for (int i = 1; i < 10; i++) { fe_sq(t0, t1, s.t19); fe_copy(t1, t0); } fe_mul(t1, t1, t2, s.t19);
fe_sq(t0, t1, s.t19); for (int i = 1; i < 50; i++) { fe_sq(t2, t0, s.t19); fe_copy(t0, t2); } fe_mul(t2, t0, t1, s.t19);
fe_sq(t0, t2, s.t19); for (int i = 1; i < 100; i++) { fe_sq(t1, t0, s.t19); fe_copy(t0, t1); } fe_mul(t0, t0, t2, s.t19);
fe_sq(t1, t0, s.t19); for (int i = 1; i < 50; i++) { fe_sq(t0, t1, s.t19); fe_copy(t1, t0); } fe_mul(t1, t1, t2, s.t19);
fe_sq(t0, t1, s.t19); for (int i = 1; i < 2; i++) { fe_sq(t1, t0, s.t19); fe_copy(t0, t1); } fe_mul(out, t0, z, s.t19);
}
private static boolean fe_isnonzero(long[] h) {
byte[] b = fe_contract(h);
int res = 0;
for (byte x : b) res |= x;
return res != 0;
}
private static int fe_isnegative(long[] h) {
byte[] b = fe_contract(h);
return b[0] & 1;
}
private static void reduce(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;
for (int i = 0; i < 32; i++) s[i] = 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) {
int bit = (int) ((resLimbs[limbIdx] >> i) & 1);
s[bitIdx / 8] |= (byte) (bit << (bitIdx % 8));
bitIdx++;
}
}
}
Arrays.fill(s, 32, 64, (byte) 0);
}
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);
reduce(buffer);
System.arraycopy(buffer, 0, S, 0, 32);
}
public static void testFromBytes(long[] h, byte[] s) {
fe_frombytes(h, s);
}
public static byte[] testFeParity(byte[] in) {
long[] h = new long[10];
fe_frombytes(h, in);
return fe_contract(h);
}
public static byte[] testReduce(byte[] in) {
byte[] s = Arrays.copyOf(in, 64);
reduce(s);
return Arrays.copyOf(s, 32);
}
public static byte[] testSmb(byte[] in) {
Scratch sc = new Scratch();
Ge p = scalarMulBase(in, sc);
byte[] out = compress(p, sc);
p.wipe(); sc.wipe();
return out;
}
public static byte[] testGeAdd(byte[] a, byte[] b) {
Scratch sc = new Scratch();
Ge p1 = scalarMulBase(a, sc);
Ge p2 = scalarMulBase(b, sc);
Ge p3 = new Ge();
ge_add(p3, p1, p2, sc);
byte[] out = compress(p3, sc);
p1.wipe(); p2.wipe(); p3.wipe(); sc.wipe();
return out;
}
public static byte[] testFeMul(byte[] a, byte[] b) {
long[] ha = new long[10], hb = new long[10];
fe_frombytes(ha, a);
fe_frombytes(hb, b);
long[] hr = new long[10];
fe_mul(hr, ha, hb, new long[19]);
return fe_contract(hr);
}
public static byte[] testFeContract(long[] h) { return fe_contract(h); }
public static long[] getBX() { return BX; }
public static long[] getBY() { return BY; }
public static long[] getD() { return D; }
public static long[] getD2() { return D2; }
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); }
}
}
@@ -0,0 +1,175 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* 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 <craigeverett@qpq.swiss>
*
* 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() {}
/**
* Computes the Keccak-256 hash of the input data.
*/
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: <<Msg/bitstring, Delimiter/bitstring, 1:1, 0:PadZeros, 1:1>>
// 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);
}
}
}
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* 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 <craigeverett@qpq.swiss>
*
* 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<Integer, Tag> 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); }
/**
* Serializes the ID to a 33-byte array.
*/
public byte[] serialize() {
byte[] result = new byte[33];
result[0] = (byte) tag.value;
System.arraycopy(value, 0, result, 1, 32);
return result;
}
/**
* Deserializes a 33-byte array into an Id object.
*/
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;
}
}
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* 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 <craigeverett@qpq.swiss>
*
* 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<String, Type> PREFIX_MAP = new HashMap<>();
static {
for (Type t : Type.values()) {
PREFIX_MAP.put(t.prefix, t);
}
}
private ApiEncoder() {}
/**
* Encodes a payload with the given type and prefix.
*/
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);
}
/**
* Decodes a prefixed Base58Check string.
*/
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) {}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
* Project: Gajumaru Core Java Libraries <gajumaru.io>
*
* 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 <craigeverett@qpq.swiss>
*
* 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.
*
* @param data The byte array to wipe.
*/
public static void wipe(byte[] data) {
if (data != null) {
Arrays.fill(data, (byte) 0);
}
}
/**
* Wipes a long array by filling it with zeros.
*/
public static void wipe(long[] data) {
if (data != null) {
Arrays.fill(data, 0L);
}
}
/**
* Encodes a byte array to a hex string.
*/
public static String binToHex(byte[] data) {
StringBuilder sb = new StringBuilder();
for (byte b : data) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
/**
* Decodes a hex string to a byte array.
*/
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;
}
}
+12
View File
@@ -0,0 +1,12 @@
import swiss.qpq.gajumaru.core.crypto.*;
import swiss.qpq.gajumaru.core.tools.*;
public class DoubleCheck {
public static void main(String[] args) {
// Base point limbs are already in Ed25519.java but private
// I'll just run one step of publicKey with a simple scalar.
byte[] seed = new byte[32]; // All zeros
byte[] pub = Ed25519.publicKey(seed);
System.out.println("Pub: " + CryptoUtils.binToHex(pub));
}
}
+47
View File
@@ -0,0 +1,47 @@
import swiss.qpq.gajumaru.core.crypto.*;
import swiss.qpq.gajumaru.core.encoding.*;
import swiss.qpq.gajumaru.core.data.*;
import swiss.qpq.gajumaru.core.tools.*;
import java.util.Arrays;
public class TestRunner {
public static void main(String[] args) {
try {
if (args.length == 0) {
System.out.println("Usage: java TestRunner <command> <input>");
return;
}
String cmd = args[0];
switch (cmd) {
case "keccak":
byte[] kInput = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Keccak256.hash(kInput)));
break;
case "blake2b":
byte[] bInput = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Blake2b.hash(bInput)));
break;
case "ak_encode":
byte[] akInput = CryptoUtils.hexToBin(args[1]);
System.out.println(ApiEncoder.encode(ApiEncoder.Type.ACCOUNT_PUBKEY, akInput));
break;
case "ed25519_pub":
byte[] seed = CryptoUtils.hexToBin(args[1]);
System.out.println(CryptoUtils.binToHex(Ed25519.publicKey(seed)));
break;
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()));
break;
default:
System.out.println("Unknown command: " + cmd);
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
+420 -11
View File
@@ -19,13 +19,29 @@
%%% 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}.
#{"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,
"debug_femul" => fun debug_femul/0,
"frombytes_parity" => fun frombytes_parity/0,
"ge_parity" => fun ge_parity/0,
"gen_consts" => fun gen_consts/0,
"check_product_limbs" => fun check_product_limbs/0,
"check_limbs" => fun check_limbs/0,
"gen_coeffs" => fun gen_coeffs/0}.
start([]) ->
@@ -60,10 +76,11 @@ run(Tests) ->
"test" -> file:set_cwd("..");
_ -> ok
end,
ok = clean(),
ok = build(),
Results = maps:map(fun run/2, Tests),
io:format("Results:~n ~tp~n", [Results]).
io:format("~nFinal Results:~n ~tp~n", [Results]).
run(Name, Test) ->
ok = io:format("~nRunning: ~ts...~n", [Name]),
@@ -115,7 +132,7 @@ base64() ->
{ok, EDecB} = file:read_file(TestFile),
{ok, JDecB} = file:read_file(JDec),
EBinHash = crypto:hash(sha512, EDecB),
JBinHash = crypto:hash(sha512, JDecB),
JBinHash = crypto:hash(sha512, JEncB),
EHash =:= JHash andalso EBinHash =:= JBinHash.
@@ -192,11 +209,11 @@ rlp() ->
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]),
ok = io:format("Failed to read RLP Java result: ~tp (Path: ~tp)~n", [R, JPathTrimmed]),
false
end;
_ ->
io:format("RLP output mismatch: ~tp~n", [Out]),
ok = io:format("RLP output mismatch: ~tp~n", [Out]),
false
end.
@@ -247,6 +264,398 @@ gaju_format() ->
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),
Seeds = [rand:bytes(32) || _ <- lists:seq(1, 10)],
ok = file:write_file(TestFile, unicode:characters_to_binary([[bin_to_hex(S), "\n"] || S <- Seeds])),
Expected = [ed25519_pub(S) || S <- Seeds],
Run = "bin/run ed25519 " ++ Temp,
Out = trim(os:cmd(Run)),
{ok, JOutContent} = file:read_file(trim(Out)),
JResults = string:split(trim(unicode:characters_to_list(JOutContent)), "\n", all),
case length(Expected) =:= length(JResults) andalso
lists:all(fun({E, J}) -> E =:= J end, lists:zip(Expected, JResults)) of
true -> true;
false ->
lists:foreach(fun({E, J}) ->
if E =/= J -> io:format("E: ~ts~nJ: ~ts~n", [E, J]); true -> ok end
end, lists:zip(Expected, JResults)),
false
end.
ed25519_pub(Seed) ->
Hash = crypto:hash(sha512, Seed),
<<K:32/binary, _/binary>> = Hash,
% Clamp K
<<K0, KMid:30/binary, K31>> = K,
CK0 = K0 band 248,
CK31 = (K31 band 127) bor 64,
ClampedK = <<CK0, KMid/binary, CK31>>,
Pub = ecu_ed25519:scalar_mul_base(ClampedK),
bin_to_hex(ecu_ed25519:compress(Pub)).
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),
length(Expected) =:= length(JResults) andalso
lists:all(fun({E, J}) -> E =:= J end, lists:zip(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:create(tag_to_erl(T), B))) || {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),
length(Expected) =:= length(JResults) andalso
lists:all(fun({E, J}) -> E =:= J end, lists:zip(Expected, JResults)).
fe_parity() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "fe.test"),
ok = filelib:ensure_dir(TestFile),
Inputs = [<<(rand:bytes(31))/binary, 0>> || _ <- lists:seq(1, 100)],
ok = file:write_file(TestFile, unicode:characters_to_binary([[bin_to_hex(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),
Expected = [bin_to_hex(I) || I <- Inputs],
length(Expected) =:= length(JResults) andalso
lists:all(fun({E, J}) -> E =:= J end, lists:zip(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],
case length(Expected) =:= length(JResults) andalso
lists:all(fun({E, J}) -> E =:= J end, lists:zip(Expected, JResults)) of
true -> true;
false ->
lists:foreach(fun({E, J}) ->
if E =/= J -> io:format("E: ~ts~nJ: ~ts~n", [E, J]); true -> ok end
end, lists:zip(Expected, JResults)),
false
end.
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],
case length(Expected) =:= length(JResults) andalso
lists:all(fun({E, J}) -> E =:= J end, lists:zip(Expected, JResults)) of
true -> true;
false ->
lists:foreach(fun({E, J}) ->
if E =/= J -> io:format("E: ~ts~nJ: ~ts~n", [E, J]); true -> ok end
end, lists:zip(Expected, JResults)),
false
end.
femul_parity() ->
Temp = temp_dir(),
TestFile = filename:join(Temp, "femul.test"),
ok = filelib:ensure_dir(TestFile),
Inputs = [{rand:bytes(32), rand:bytes(32)} || _ <- 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],
case length(ExpectedBin) =:= length(JResults) andalso
lists:all(fun({E, J}) -> E =:= J end, lists:zip(ExpectedBin, JResults)) of
true -> true;
false ->
lists:foreach(fun({{A, B}, E, J}) ->
if E =/= J ->
io:format("A: ~ts~nB: ~ts~nE: ~ts~nJ: ~ts~n", [bin_to_hex(A), bin_to_hex(B), E, J]);
true -> ok end
end, lists:zip3(Inputs, ExpectedBin, JResults)),
false
end.
debug_femul() ->
A_bin = <<1, 0:248>>,
B_bin = <<1, 1, 0:240>>,
A = binary:decode_unsigned(A_bin, little),
B = binary:decode_unsigned(B_bin, little),
Res = ecu_ed25519:f_mul(A, B),
io:format("A (hex): ~ts~n", [bin_to_hex(A_bin)]),
io:format("B (hex): ~ts~n", [bin_to_hex(B_bin)]),
io:format("Res (hex): ~ts~n", [bin_to_hex(pack_p(Res))]),
Limbs = fun F(Val, Idx) when Idx < 10 ->
Size = if Idx rem 2 =:= 0 -> 26; true -> 25 end,
L = Val band ((1 bsl Size) - 1),
[L | F(Val bsr Size, Idx + 1)];
F(_, _) -> []
end,
io:format("A limbs: ~w~n", [Limbs(A, 0)]),
io:format("B limbs: ~w~n", [Limbs(B, 0)]),
io:format("Res limbs: ~w~n", [Limbs(Res, 0)]),
true.
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),
Expected = [
begin
Val = binary:decode_unsigned(I, little),
Limbs = fun F(V, Idx) when Idx < 10 ->
Size = if Idx rem 2 =:= 0 -> 26; true -> 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 || I <- Inputs],
case length(Expected) =:= length(JResults) andalso
lists:all(fun({E, J}) -> E =:= J end, lists:zip(Expected, JResults)) of
true -> true;
false ->
lists:foreach(fun({E, J}) ->
if E =/= J -> io:format("E: ~ts~nJ: ~ts~n", [E, J]); true -> ok end
end, lists:zip(Expected, JResults)),
false
end.
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.
gen_consts() ->
GetLimbs = fun(Val) ->
Limbs = fun F(V, Idx) when Idx < 10 ->
Size = if Idx rem 2 =:= 0 -> 26; true -> 25 end,
L = V band ((1 bsl Size) - 1),
[L | F(V bsr Size, Idx + 1)];
F(_, _) -> []
end,
Limbs(Val, 0)
end,
P = (1 bsl 255) - 19,
Y = (4 * ecu_ed25519:f_inv(5)) rem P,
% X^2 = (y^2 - 1) / (d*y^2 + 1)
Y2 = (Y * Y) rem P,
D = (P - 121665) * ecu_ed25519:f_inv(121666) rem P,
Num = (Y2 - 1 + P) rem P,
Den = (D * Y2 + 1) rem P,
X2 = (Num * ecu_ed25519:f_inv(Den)) rem P,
X = ecu_ed25519:f_pow(X2, (P + 3) bsr 3), % compute square root
T = (X * Y) rem P,
I = ecu_ed25519:f_pow(2, (P - 1) bsr 2),
io:format("BX: ~w~n", [GetLimbs(X)]),
io:format("BY: ~w~n", [GetLimbs(Y)]),
io:format("BXY: ~w~n", [GetLimbs(T)]),
io:format("D: ~w~n", [GetLimbs(D)]),
io:format("D2: ~w~n", [GetLimbs((2 * D) rem P)]),
io:format("I: ~w~n", [GetLimbs(I)]),
true.
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 -> <<Enc/binary, 0:(8*(32-Size))>>; true -> Enc end.
check_limbs() ->
% Random values
A_bin = <<11, 22, 33, 0:232>>,
B_bin = <<44, 55, 66, 0:232>>,
A = binary:decode_unsigned(A_bin, little),
B = binary:decode_unsigned(B_bin, little),
Res = A * B,
Limbs = fun F(Val, Idx) when Idx < 19 ->
Size = if Idx rem 2 =:= 0 -> 26; true -> 25 end,
L = Val band ((1 bsl Size) - 1),
[L | F(Val bsr Size, Idx + 1)];
F(_, _) -> []
end,
io:format("Expected product limbs: ~w~n", [Limbs(Res, 0)]),
true.
check_product_limbs() ->
A_bin = <<11, 22, 33, 44, 0:224>>,
B_bin = <<55, 66, 77, 88, 0:224>>,
A = binary:decode_unsigned(A_bin, little),
B = binary:decode_unsigned(B_bin, little),
GetLimbs = fun(Val) ->
Limbs = fun F(V, Idx) when Idx < 10 ->
Size = if Idx rem 2 =:= 0 -> 26; true -> 25 end,
L = V band ((1 bsl Size) - 1),
[L | F(V bsr Size, Idx + 1)];
F(_, _) -> []
end,
list_to_tuple(Limbs(Val, 0))
end,
{A0, A1, A2, A3, A4, A5, A6, A7, A8, A9} = GetLimbs(A),
{B0, B1, B2, B3, B4, B5, B6, B7, B8, B9} = GetLimbs(B),
T0 = A0*B0,
T1 = A0*B1 + A1*B0,
T2 = 2*A1*B1 + A0*B2 + A2*B0,
T3 = A1*B2 + A2*B1 + A0*B3 + A3*B0,
T4 = A2*B2 + 2*(A1*B3 + A3*B1) + A0*B4 + A4*B0,
io:format("T0: ~b, T1: ~b, T2: ~b, T3: ~b, T4: ~b~n", [T0, T1, T2, T3, T4]),
true.
gen_coeffs() ->
W = [0, 26, 51, 77, 102, 128, 153, 179, 204, 230],
lists:foreach(fun(TargetIdx) ->
io:format("Limb ~b: ", [TargetIdx]),
_ = [
begin
Weight = lists:nth(I+1, W) + lists:nth(J+1, W),
TargetWeight = lists:nth(TargetIdx+1, W),
if Weight >= TargetWeight andalso Weight < TargetWeight + 26 ->
Diff = Weight - TargetWeight,
io:format("A~b*B~b*~b + ", [I, J, 1 bsl Diff]);
true -> ok
end
end || I <- lists:seq(0, 9), J <- lists:seq(0, 9)],
io:format("~n")
end, lists:seq(0, 18)),
true.
tag_to_erl(1) -> account;
tag_to_erl(2) -> name;
tag_to_erl(3) -> commitment;
tag_to_erl(5) -> contract;
tag_to_erl(6) -> channel;
tag_to_erl(7) -> associate_chain;
tag_to_erl(8) -> native_token;
tag_to_erl(9) -> entry.
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
+2 -1
View File
@@ -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}},