Compare commits
17
Commits
7493b473a7
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8443e9a321 | ||
|
|
381c8f1b9a | ||
|
|
6714353f8c | ||
|
|
45c6f9c730 | ||
|
|
7cd299d250 | ||
|
|
9287cbb4b7 | ||
|
|
64c592285a | ||
|
|
954d8760ac | ||
|
|
20e462ec03 | ||
|
|
11516b1cca | ||
|
|
c1c13213c1 | ||
|
|
5407431566 | ||
|
|
ea7a013abe | ||
|
|
ddc811b63d | ||
|
|
c21f399b69 | ||
|
|
97bb833d35 | ||
|
|
77dea4f3cf |
@@ -30,7 +30,7 @@ does not have floats, and they do not exist at all in the Sophia language.
|
||||
|
||||
## Terms
|
||||
|
||||
Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
Project: Gajumaru Core Java Libraries
|
||||
|
||||
This program is dual-licensed:
|
||||
@@ -38,10 +38,10 @@ This program is dual-licensed:
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version (AGPL-3.0-or-later). (See LICENSE file.)
|
||||
|
||||
2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
purchase a commercial license from QPQ IaaS AG.
|
||||
|
||||
Authors:
|
||||
- Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
# Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
|
||||
set -e
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
# Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
|
||||
set -e
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
@@ -21,7 +21,6 @@
|
||||
package swiss.qpq.gajumaru.core.crypto;
|
||||
|
||||
import java.util.Arrays;
|
||||
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
|
||||
|
||||
// Pure Java implementation of BLAKE2b.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
@@ -30,11 +30,33 @@ import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
// Pure Java implementation of Ed25519 curve arithmetic.
|
||||
// Uses exquisitely annoying Radix-2^25.5 field arithmetic. Never do this if you can avoid it.
|
||||
// Verified against the canonical Erlang ec_utils (see test/README.md for how).
|
||||
|
||||
//
|
||||
// SAFETY NOTE:
|
||||
// Despite the effort at zeroing sensitive memory in this module, Java heap memory does not
|
||||
// provide reliable secret-memory erasure or even really permit it. If protection against
|
||||
// copies of secret material in managed memory is a requirement of the deployment environment,
|
||||
// don't use this implementation; use explicitly managed native memory instead.
|
||||
//
|
||||
// This implementation is intended for isolated environments where the Java heap is an
|
||||
// acceptable trust boundary. It is not intended to provide secure-memory guarantees
|
||||
// against a hostile or inspecting runtime.
|
||||
//
|
||||
// Android is one such platform where a vendor-provided C implementation of Ed25519
|
||||
// interacting over a JNI boundary with off-heap memory (via java.nio.ByteBuffer)
|
||||
// and aggressive memory sanitation is necessary.
|
||||
//
|
||||
// WARNING:
|
||||
// Do NOT compile this code with compiler optimizations turned on. It will remove the memory
|
||||
// protection functionality entirely as it is "no impact" code from the perspective of
|
||||
// performance optimization. Configure your build system to deliberately turn off optimizing
|
||||
// profilers for this module. You will need to do the same for any C code you might interface
|
||||
// with over JNI as well. Constant time execution, memory wiping, etc. are all viewed as
|
||||
// no-impact busywork or pessimization from the perspective of a performance profiler.
|
||||
//
|
||||
// References:
|
||||
// I don't even know where to start with references, but the tink-java library and pretty much
|
||||
// everything (and everyone!) referenced on the lib25519 page deserves a mention.
|
||||
// Of special mention, of course, is SUPERCOP.
|
||||
// I don't even know where to start with references, but the tink-java library and pretty much
|
||||
// everything (and everyone!) referenced on the lib25519 page deserves a mention.
|
||||
// Of special mention, of course, is SUPERCOP.
|
||||
//
|
||||
// tink-java: https://github.com/tink-crypto/tink-java
|
||||
// lib25519 : https://lib25519.cr.yp.to/people.html
|
||||
@@ -63,9 +85,26 @@ public final class Ed25519 {
|
||||
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};
|
||||
|
||||
// Group order L (little-endian)
|
||||
private static final byte[] L = {
|
||||
(byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
|
||||
(byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10
|
||||
};
|
||||
|
||||
// Field prime P = 2^255 - 19 (little-endian, 255 bits used)
|
||||
private static final byte[] P = {
|
||||
(byte) 0xed, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x7f
|
||||
};
|
||||
|
||||
public static final class Ge {
|
||||
public final long[] X = new long[10], Y = new long[10], Z = new long[10], T = new long[10];
|
||||
|
||||
// Internal: Wipes the coordinates.
|
||||
public void wipe() {
|
||||
CryptoUtils.wipe(X);
|
||||
CryptoUtils.wipe(Y);
|
||||
@@ -82,6 +121,7 @@ public final class Ed25519 {
|
||||
public final long[][] stack = new long[10][10];
|
||||
public final Ge geTmp = new Ge();
|
||||
|
||||
// Internal: Wipes the scratch space.
|
||||
public void wipe() {
|
||||
CryptoUtils.wipe(a);
|
||||
CryptoUtils.wipe(b);
|
||||
@@ -156,7 +196,7 @@ public final class Ed25519 {
|
||||
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;
|
||||
if (!isLessThan(S_bytes, L)) return false;
|
||||
Scratch sc = new Scratch();
|
||||
Ge A = decompress(publicKey, sc);
|
||||
if (A == null) return false;
|
||||
@@ -188,6 +228,25 @@ public final class Ed25519 {
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a message using a 64-byte secret key (seed | public_key).
|
||||
* This matches the libsodium behavior.
|
||||
*/
|
||||
public static byte[] signWithExpandedKey(byte[] message, byte[] sk64) {
|
||||
byte[] seed = new byte[32];
|
||||
System.arraycopy(sk64, 0, seed, 0, 32);
|
||||
try {
|
||||
return sign(seed, message);
|
||||
} finally {
|
||||
CryptoUtils.wipe(seed);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// Internal curve and field arithmetic machinery.
|
||||
// These are public only for parity testing against reference implementations.
|
||||
|
||||
// Internal: Scalar multiplication by base point.
|
||||
public static Ge scalarMulBase(byte[] scalar, Scratch s) {
|
||||
Ge res = new Ge();
|
||||
fe_0(res.X);
|
||||
@@ -209,16 +268,7 @@ public final class Ed25519 {
|
||||
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();
|
||||
}
|
||||
|
||||
// Internal: General scalar multiplication.
|
||||
public static Ge scalarMul(Ge p, byte[] scalar, Scratch s) {
|
||||
Ge res = new Ge();
|
||||
fe_0(res.X);
|
||||
@@ -240,6 +290,7 @@ public final class Ed25519 {
|
||||
return res;
|
||||
}
|
||||
|
||||
// Internal: Point addition.
|
||||
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];
|
||||
@@ -264,6 +315,7 @@ public final class Ed25519 {
|
||||
fe_mul(r.T, E, H, s.t19);
|
||||
}
|
||||
|
||||
// Internal: Point doubling.
|
||||
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];
|
||||
@@ -300,8 +352,15 @@ public final class Ed25519 {
|
||||
}
|
||||
}
|
||||
|
||||
// Internal: Point decompression.
|
||||
public static Ge decompress(byte[] b, Scratch s) {
|
||||
if (b.length != 32) return null;
|
||||
|
||||
// Ensure y is canonical (y < P)
|
||||
byte[] y_check = Arrays.copyOf(b, 32);
|
||||
y_check[31] &= 0x7F;
|
||||
if (!isLessThan(y_check, P)) return null;
|
||||
|
||||
Ge p = new Ge();
|
||||
fe_frombytes(p.Y, b);
|
||||
fe_1(p.Z);
|
||||
@@ -338,6 +397,7 @@ public final class Ed25519 {
|
||||
return p;
|
||||
}
|
||||
|
||||
// Internal: Point compression.
|
||||
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);
|
||||
@@ -372,6 +432,17 @@ public final class Ed25519 {
|
||||
for (int i = 0; i < 10; i++) h[i] = -f[i];
|
||||
}
|
||||
|
||||
private static boolean isLessThan(byte[] a, byte[] b) {
|
||||
for (int i = 31; i >= 0; i--) {
|
||||
int ai = a[i] & 0xff;
|
||||
int bi = b[i] & 0xff;
|
||||
if (ai < bi) return true;
|
||||
if (ai > bi) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Internal: Field multiplication.
|
||||
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];
|
||||
@@ -395,6 +466,7 @@ public final class Ed25519 {
|
||||
fe_reduce(out, t);
|
||||
}
|
||||
|
||||
/// Internal: Field squaring.
|
||||
public static void fe_sq(long[] out, long[] f, long[] t) {
|
||||
t[0] = f[0] * f[0];
|
||||
t[1] = 2 * f[0] * f[1];
|
||||
@@ -418,6 +490,7 @@ public final class Ed25519 {
|
||||
fe_reduce(out, t);
|
||||
}
|
||||
|
||||
// Internal: Field reduction.
|
||||
public static void fe_reduce(long[] h, long[] t) {
|
||||
t[0] += t[10] * 19;
|
||||
t[1] += t[11] * 19;
|
||||
@@ -441,6 +514,7 @@ public final class Ed25519 {
|
||||
for (int i = 0; i < 10; i++) h[i] = t[i];
|
||||
}
|
||||
|
||||
// Internal: Load field element from bytes.
|
||||
public static void fe_frombytes(long[] h, byte[] s) {
|
||||
for (int i = 0; i < 10; i++) h[i] = 0;
|
||||
int bitIdx = 0;
|
||||
@@ -455,6 +529,7 @@ public final class Ed25519 {
|
||||
}
|
||||
}
|
||||
|
||||
// Internal: Contract field element to bytes.
|
||||
public static byte[] fe_contract(long[] h) {
|
||||
long[] val = Arrays.copyOf(h, 10);
|
||||
for (int p = 0; p < 2; p++) {
|
||||
@@ -588,6 +663,7 @@ public final class Ed25519 {
|
||||
return s[0] & 1;
|
||||
}
|
||||
|
||||
// Internal: Scalar reduction.
|
||||
public static void reduceScalar(byte[] s) {
|
||||
long s0 = 2097151 & load3(s, 0);
|
||||
long s1 = 2097151 & (load4(s, 2) >> 5);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
@@ -29,7 +29,6 @@ import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
@@ -20,59 +20,71 @@
|
||||
|
||||
package swiss.qpq.gajumaru.core.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
|
||||
// Vault provides secure AES/GCM encryption and decryption with memory hygiene.
|
||||
// It uses standard JCE providers but ensures that application-level buffers can be wiped.
|
||||
//
|
||||
// Purpose:
|
||||
// This provides a way to use opaque handles to javax.crypto.SecretKey objects in discrete memory
|
||||
// instead of exposing your plaintext secret keys to other parts of the system that might leave
|
||||
// dead references on the heap somewhere that are vulnerable until GC finally hits.
|
||||
//
|
||||
// The important thing to remember is that there is still a burden on the caller to take advantage
|
||||
// of whatever the current platform's best key management facilities are and stick to them.
|
||||
|
||||
/**
|
||||
* Vault provides secure AES/GCM encryption and decryption with memory hygiene.
|
||||
*/
|
||||
public final class Vault {
|
||||
|
||||
private static final String AES_GCM = "AES/GCM/NoPadding";
|
||||
private static final int GCM_TAG_LENGTH = 128; // Bits
|
||||
private static final int IV_LENGTH = 12; // Bytes
|
||||
|
||||
private Vault() {}
|
||||
|
||||
/**
|
||||
* Holds the results of an encryption operation.
|
||||
*/
|
||||
public record Ciphertext(byte[] iv, byte[] data) {}
|
||||
|
||||
/**
|
||||
* Encrypts plaintext using AES/GCM.
|
||||
*
|
||||
* @param key The SecretKey to use.
|
||||
* @param iv The initialization vector (should be 12 bytes).
|
||||
* @param plaintext The data to encrypt.
|
||||
* @return The ciphertext including the authentication tag.
|
||||
* @throws Exception if encryption fails.
|
||||
* Manually generates the IV to ensure deterministic tag length and cross-provider consistency.
|
||||
*/
|
||||
public static Ciphertext encrypt(SecretKey key, byte[] plaintext) throws Exception {
|
||||
try {
|
||||
byte[] iv = new byte[IV_LENGTH];
|
||||
new SecureRandom().nextBytes(iv);
|
||||
|
||||
byte[] ciphertext = encrypt(key, iv, plaintext);
|
||||
return new Ciphertext(iv, ciphertext);
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Vault.encrypt failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts plaintext using AES/GCM with a specific IV.
|
||||
*/
|
||||
public static byte[] encrypt(SecretKey key, byte[] iv, byte[] plaintext) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
|
||||
return cipher.doFinal(plaintext);
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (Exception e) {
|
||||
throw new Exception("Vault.encrypt(iv) failed: " + e.getClass().getName() + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts ciphertext using AES/GCM.
|
||||
*
|
||||
* @param key The SecretKey to use.
|
||||
* @param iv The initialization vector used during encryption.
|
||||
* @param ciphertext The data to decrypt (including the authentication tag).
|
||||
* @return The plaintext data. The caller is RESPONSIBLE for wiping this buffer
|
||||
* using CryptoUtils.wipe() once it is no longer needed.
|
||||
* @throws Exception if decryption or authentication fails.
|
||||
*/
|
||||
public static byte[] decrypt(SecretKey key, byte[] iv, byte[] ciphertext) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
return cipher.doFinal(ciphertext);
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
return cipher.doFinal(ciphertext);
|
||||
} catch (Exception e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg == null) msg = e.toString();
|
||||
throw new Exception("Vault.decrypt failed [" + e.getClass().getSimpleName() + "]: " + msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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.util.Arrays;
|
||||
|
||||
// Gf256 implements finite field arithmetic over GF(256).
|
||||
// Ported from gf256.erl.
|
||||
|
||||
public final class Gf256 {
|
||||
|
||||
private final int[] exp;
|
||||
private final int[] log;
|
||||
|
||||
public Gf256(int primeModulus) {
|
||||
this.exp = new int[512];
|
||||
this.log = new int[256];
|
||||
int x = 1;
|
||||
for (int i = 0; i < 255; i++) {
|
||||
exp[i] = x;
|
||||
exp[i + 255] = x;
|
||||
log[x] = i;
|
||||
x <<= 1;
|
||||
if (x > 255) {
|
||||
x ^= primeModulus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int add(int a, int b) {
|
||||
return a ^ b;
|
||||
}
|
||||
|
||||
public int multiply(int a, int b) {
|
||||
if (a == 0 || b == 0) return 0;
|
||||
return exp[log[a] + log[b]];
|
||||
}
|
||||
|
||||
public int inverse(int x) {
|
||||
if (x == 0) throw new ArithmeticException("Division by zero");
|
||||
return exp[255 - log[x]];
|
||||
}
|
||||
|
||||
public int exponent(int i) {
|
||||
return exp[i % 255];
|
||||
}
|
||||
|
||||
public int[] monomialProduct(int[] poly, int coeff, int degree) {
|
||||
if (coeff == 0) return new int[]{0};
|
||||
int[] result = new int[poly.length + degree];
|
||||
for (int i = 0; i < poly.length; i++) {
|
||||
result[i] = multiply(poly[i], coeff);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public int[] polynomialProduct(int[] p1, int[] p2) {
|
||||
if (isZero(p1) || isZero(p2)) return new int[]{0};
|
||||
int[] result = new int[p1.length + p2.length - 1];
|
||||
for (int i = 0; i < p1.length; i++) {
|
||||
if (p1[i] == 0) continue;
|
||||
for (int j = 0; j < p2.length; j++) {
|
||||
result[i + j] ^= multiply(p1[i], p2[j]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public int[] divide(int[] a, int[] b) {
|
||||
if (isZero(b)) throw new ArithmeticException("Division by zero");
|
||||
int inv = inverse(b[0]);
|
||||
int[] r = Arrays.copyOf(a, a.length);
|
||||
|
||||
int steps = a.length - b.length + 1;
|
||||
for (int i = 0; i < steps; i++) {
|
||||
if (r[i] == 0) continue;
|
||||
int scale = multiply(r[i], inv);
|
||||
for (int j = 0; j < b.length; j++) {
|
||||
r[i + j] ^= multiply(b[j], scale);
|
||||
}
|
||||
}
|
||||
// Remainder is the last b.length - 1 elements
|
||||
return Arrays.copyOfRange(r, a.length - b.length + 1, a.length);
|
||||
}
|
||||
|
||||
private boolean isZero(int[] poly) {
|
||||
return poly.length == 0 || (poly.length == 1 && poly[0] == 0);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
@@ -36,7 +36,7 @@ public final class Mnemonic {
|
||||
private static final int WIDTH = 12; // bits per word
|
||||
private static final int MAX_DATA_CHUNKS = 22; // For 256-bit seed
|
||||
|
||||
private static final String[] WORDS = {
|
||||
public static final String[] WORDS = {
|
||||
"aardvark", "abacus", "abalone", "abandon", "abbey", "abdomen", "abduct", "abhor", "abide",
|
||||
"ability", "able", "abnormal", "aboard", "abolish", "abort", "above", "abrasive", "abridged",
|
||||
"abroad", "abrupt", "abscond", "absence", "absinthe", "absorb", "abstract", "absurd", "abundant",
|
||||
@@ -99,7 +99,7 @@ public final class Mnemonic {
|
||||
"broken", "bronze", "broom", "browse", "bruise", "brunt", "brute", "bubble", "buckle", "buddy",
|
||||
"budget", "buffet", "builder", "bulb", "bulge", "bulimia", "bulky", "bulletin", "bummed", "bumpy",
|
||||
"bundle", "bungalow", "bunion", "bunker", "buoyant", "burden", "bureau", "burger", "burial",
|
||||
"burlap", "burnout", "blurp", "burrito", "burst", "bushel", "business", "bustle", "busy", "butane",
|
||||
"burlap", "burnout", "burp", "burrito", "burst", "bushel", "business", "bustle", "busy", "butane",
|
||||
"butcher", "butler", "button", "buxom", "buyer", "buyout", "buzz", "bylaw", "bypass", "cabaret",
|
||||
"cabbage", "cabin", "cactus", "cadaver", "cadet", "caffeine", "caftan", "cage", "cairn", "cajole",
|
||||
"calamity", "calcium", "calendar", "calico", "callous", "calm", "calorie", "calypso", "camera",
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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.util.Arrays;
|
||||
|
||||
// QR encoder for Byte mode.
|
||||
// Ported from qr.erl.
|
||||
|
||||
public final class QR {
|
||||
|
||||
private QR() {}
|
||||
|
||||
public enum ECC {
|
||||
M(0);
|
||||
final int bits;
|
||||
ECC(int bits) { this.bits = bits; }
|
||||
}
|
||||
|
||||
private static final int PRIME_MODULUS = 285;
|
||||
private static final int FORMAT_INFO_POLY = 1335;
|
||||
private static final int FORMAT_INFO_MASK = 21522;
|
||||
private static final int VERSION_INFO_POLY = 7973;
|
||||
|
||||
private static final int[][] ALIGNMENT_COORDINATES = {
|
||||
{}, // V0
|
||||
{}, // V1
|
||||
{6, 18}, {6, 22}, {6, 26}, {6, 30}, {6, 34}, {6, 22, 38}, {6, 24, 42}, {6, 26, 46},
|
||||
{6, 28, 50}, {6, 30, 54}, {6, 32, 58}, {6, 34, 62}, {6, 26, 46, 66}, {6, 26, 48, 70},
|
||||
{6, 26, 50, 74}, {6, 30, 54, 78}, {6, 30, 56, 82}, {6, 30, 58, 86}, {6, 34, 62, 90},
|
||||
{6, 28, 50, 72, 94}, {6, 26, 50, 74, 98}, {6, 30, 54, 78, 102}, {6, 28, 54, 80, 106},
|
||||
{6, 32, 58, 84, 110}, {6, 30, 58, 86, 114}, {6, 34, 62, 90, 118}, {6, 26, 50, 74, 98, 122},
|
||||
{6, 30, 54, 78, 102, 126}, {6, 26, 52, 78, 104, 130}, {6, 30, 56, 82, 108, 134},
|
||||
{6, 34, 60, 86, 112, 138}, {6, 30, 58, 86, 114, 142}, {6, 34, 62, 90, 118, 146},
|
||||
{6, 30, 54, 78, 102, 126, 150}, {6, 24, 50, 76, 102, 128, 154}, {6, 28, 54, 80, 106, 132, 158},
|
||||
{6, 32, 58, 84, 110, 136, 162}, {6, 26, 54, 82, 110, 138, 166}, {6, 30, 58, 86, 114, 142, 170}
|
||||
};
|
||||
|
||||
private record VersionInfo(ECC ecc, int version, int byteCapacity, int[][] blocks, int remainder) {}
|
||||
|
||||
private static final VersionInfo[] VERSION_TABLE = {
|
||||
new VersionInfo(ECC.M, 1, 14, new int[][]{{1, 26, 16}}, 0),
|
||||
new VersionInfo(ECC.M, 2, 26, new int[][]{{1, 44, 28}}, 7),
|
||||
new VersionInfo(ECC.M, 3, 42, new int[][]{{1, 70, 44}}, 7),
|
||||
new VersionInfo(ECC.M, 4, 62, new int[][]{{2, 50, 32}}, 7),
|
||||
new VersionInfo(ECC.M, 5, 84, new int[][]{{2, 67, 43}}, 7),
|
||||
new VersionInfo(ECC.M, 6, 106, new int[][]{{4, 43, 27}}, 7),
|
||||
new VersionInfo(ECC.M, 7, 122, new int[][]{{4, 49, 31}}, 0),
|
||||
new VersionInfo(ECC.M, 8, 152, new int[][]{{2, 60, 38}, {2, 61, 39}}, 0),
|
||||
new VersionInfo(ECC.M, 9, 180, new int[][]{{3, 58, 36}, {2, 59, 37}}, 0),
|
||||
new VersionInfo(ECC.M, 10, 213, new int[][]{{4, 69, 43}, {1, 70, 44}}, 0),
|
||||
new VersionInfo(ECC.M, 11, 251, new int[][]{{1, 80, 50}, {4, 81, 51}}, 0),
|
||||
new VersionInfo(ECC.M, 12, 287, new int[][]{{6, 58, 36}, {2, 59, 37}}, 0),
|
||||
new VersionInfo(ECC.M, 13, 331, new int[][]{{8, 59, 37}, {1, 60, 38}}, 0),
|
||||
new VersionInfo(ECC.M, 14, 362, new int[][]{{4, 64, 40}, {5, 65, 41}}, 3),
|
||||
new VersionInfo(ECC.M, 15, 412, new int[][]{{5, 65, 41}, {5, 66, 42}}, 3),
|
||||
new VersionInfo(ECC.M, 40, 3391, new int[][]{{1, 3391, 2953}}, 0)
|
||||
};
|
||||
|
||||
public static boolean[][] encode(String text) {
|
||||
return encode(text.getBytes(java.nio.charset.StandardCharsets.UTF_8), ECC.M);
|
||||
}
|
||||
|
||||
public static boolean[][] encode(byte[] data, ECC ecc) {
|
||||
VersionInfo vi = chooseVersion(data.length, ecc);
|
||||
if (vi == null) throw new IllegalArgumentException("Data too large for supported versions");
|
||||
|
||||
BitBuffer buffer = new BitBuffer();
|
||||
buffer.add(4, 4); // Byte mode
|
||||
int cciBits = vi.version <= 9 ? 8 : 16;
|
||||
buffer.add(data.length, cciBits);
|
||||
for (byte b : data) buffer.add(b & 0xFF, 8);
|
||||
buffer.add(0, 4); // Terminator
|
||||
|
||||
int totalDataBytesCount = 0;
|
||||
for (int[] b : vi.blocks) totalDataBytesCount += b[0] * b[2];
|
||||
|
||||
if (buffer.bitCount() % 8 != 0) buffer.add(0, 8 - (buffer.bitCount() % 8));
|
||||
|
||||
boolean toggle = true;
|
||||
while (buffer.bitCount() / 8 < totalDataBytesCount) {
|
||||
buffer.add(toggle ? 0xEC : 0x11, 8);
|
||||
toggle = !toggle;
|
||||
}
|
||||
|
||||
byte[] codewords = generateCodewords(vi, buffer.toByteArray());
|
||||
|
||||
int dim = 17 + vi.version * 4;
|
||||
boolean[][] matrix = new boolean[dim][dim];
|
||||
boolean[][] reserved = new boolean[dim][dim];
|
||||
|
||||
placeFixedPatterns(vi, matrix, reserved);
|
||||
placeData(vi, matrix, reserved, codewords, vi.remainder);
|
||||
|
||||
applyMask(matrix, reserved, 0);
|
||||
placeFormatInfo(vi, matrix, 0);
|
||||
if (vi.version >= 7) placeVersionInfo(vi, matrix);
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
private static VersionInfo chooseVersion(int length, ECC ecc) {
|
||||
for (VersionInfo vi : VERSION_TABLE) {
|
||||
if (vi.ecc == ecc && vi.byteCapacity >= length) return vi;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] generateCodewords(VersionInfo vi, byte[] data) {
|
||||
Gf256 field = new Gf256(PRIME_MODULUS);
|
||||
int totalBlocks = 0;
|
||||
for (int[] b : vi.blocks) totalBlocks += b[0];
|
||||
|
||||
byte[][] dataBlocks = new byte[totalBlocks][];
|
||||
byte[][] eccBlocks = new byte[totalBlocks][];
|
||||
|
||||
int blockIdx = 0;
|
||||
int dataOffset = 0;
|
||||
for (int[] b : vi.blocks) {
|
||||
int count = b[0];
|
||||
int dataBytes = b[2];
|
||||
int totalBytes = b[1];
|
||||
int eccBytes = totalBytes - dataBytes;
|
||||
int[] gen = generator(field, eccBytes);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
byte[] block = Arrays.copyOfRange(data, dataOffset, dataOffset + dataBytes);
|
||||
dataBlocks[blockIdx] = block;
|
||||
eccBlocks[blockIdx] = encodeRS(field, block, gen, eccBytes);
|
||||
dataOffset += dataBytes;
|
||||
blockIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
int totalCodewords = 0;
|
||||
for (int[] b : vi.blocks) totalCodewords += b[0] * b[1];
|
||||
byte[] result = new byte[totalCodewords];
|
||||
int resOffset = 0;
|
||||
|
||||
int maxData = 0;
|
||||
for (byte[] b : dataBlocks) maxData = Math.max(maxData, b.length);
|
||||
for (int i = 0; i < maxData; i++) {
|
||||
for (byte[] b : dataBlocks) {
|
||||
if (i < b.length) result[resOffset++] = b[i];
|
||||
}
|
||||
}
|
||||
int eccLen = eccBlocks[0].length;
|
||||
for (int i = 0; i < eccLen; i++) {
|
||||
for (byte[] b : eccBlocks) {
|
||||
result[resOffset++] = b[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int[] generator(Gf256 f, int degree) {
|
||||
int[] g = {1};
|
||||
for (int i = 0; i < degree; i++) {
|
||||
g = f.polynomialProduct(g, new int[]{1, f.exponent(i)});
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
private static byte[] encodeRS(Gf256 f, byte[] data, int[] gen, int eccBytes) {
|
||||
int[] poly = new int[data.length + eccBytes];
|
||||
for (int i = 0; i < data.length; i++) poly[i] = data[i] & 0xFF;
|
||||
int[] rem = f.divide(poly, gen);
|
||||
byte[] res = new byte[eccBytes];
|
||||
int offset = eccBytes - rem.length;
|
||||
for (int i = 0; i < rem.length; i++) res[offset + i] = (byte) rem[i];
|
||||
return res;
|
||||
}
|
||||
|
||||
private static void placeFixedPatterns(VersionInfo vi, boolean[][] matrix, boolean[][] reserved) {
|
||||
int dim = matrix.length;
|
||||
placeFinder(matrix, reserved, 0, 0);
|
||||
placeFinder(matrix, reserved, dim - 7, 0);
|
||||
placeFinder(matrix, reserved, 0, dim - 7);
|
||||
|
||||
// Separators around finders
|
||||
for (int i = 0; i < 8; i++) {
|
||||
reserved[7][i] = reserved[i][7] = true;
|
||||
reserved[7][dim - 1 - i] = reserved[i][dim - 8] = true;
|
||||
reserved[dim - 8][i] = reserved[dim - 1 - i][7] = true;
|
||||
}
|
||||
|
||||
// Timing patterns
|
||||
for (int i = 8; i < dim - 8; i++) {
|
||||
matrix[6][i] = matrix[i][6] = (i % 2 == 0);
|
||||
reserved[6][i] = reserved[i][6] = true;
|
||||
}
|
||||
|
||||
// Alignment patterns
|
||||
int[] coords = ALIGNMENT_COORDINATES[vi.version];
|
||||
for (int y : coords) {
|
||||
for (int x : coords) {
|
||||
if (isFinderArea(x, y, dim)) continue;
|
||||
placeAlignment(matrix, reserved, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Dark module
|
||||
matrix[4 * vi.version + 9][8] = true;
|
||||
reserved[4 * vi.version + 9][8] = true;
|
||||
|
||||
// Reserved areas for format and version info
|
||||
for (int i = 0; i < 9; i++) reserved[i][8] = reserved[8][i] = true;
|
||||
for (int i = 0; i < 8; i++) reserved[dim - 1 - i][8] = reserved[8][dim - 1 - i] = true;
|
||||
|
||||
if (vi.version >= 7) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
reserved[dim - 11 + j][i] = reserved[i][dim - 11 + j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isFinderArea(int x, int y, int dim) {
|
||||
return (x < 9 && y < 9) || (x > dim - 10 && y < 9) || (x < 9 && y > dim - 10);
|
||||
}
|
||||
|
||||
private static void placeFinder(boolean[][] m, boolean[][] r, int x, int y) {
|
||||
for (int j = 0; j < 7; j++) {
|
||||
for (int i = 0; i < 7; i++) {
|
||||
int px = x + i, py = y + j;
|
||||
r[py][px] = true;
|
||||
boolean black = (i == 0 || i == 6 || j == 0 || j == 6 || (i >= 2 && i <= 4 && j >= 2 && j <= 4));
|
||||
m[py][px] = black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeAlignment(boolean[][] m, boolean[][] r, int x, int y) {
|
||||
for (int j = -2; j <= 2; j++) {
|
||||
for (int i = -2; i <= 2; i++) {
|
||||
r[y + j][x + i] = true;
|
||||
m[y + j][x + i] = (Math.abs(i) == 2 || Math.abs(j) == 2 || (i == 0 && j == 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeData(VersionInfo vi, boolean[][] m, boolean[][] r, byte[] data, int remainderBits) {
|
||||
int dim = m.length;
|
||||
int bitIdx = 0;
|
||||
int totalBits = data.length * 8 + remainderBits;
|
||||
int x = dim - 1, y = dim - 1, dir = -1;
|
||||
|
||||
while (x >= 0) {
|
||||
if (x == 6) x--;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
int cx = x - i;
|
||||
if (!r[y][cx]) {
|
||||
if (bitIdx < totalBits) {
|
||||
int byteIdx = bitIdx / 8;
|
||||
int shift = 7 - (bitIdx % 8);
|
||||
boolean bit = byteIdx < data.length && ((data[byteIdx] >> shift) & 1) != 0;
|
||||
m[y][cx] = bit;
|
||||
bitIdx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
y += dir;
|
||||
if (y < 0 || y >= dim) {
|
||||
y -= dir; x -= 2; dir = -dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyMask(boolean[][] m, boolean[][] r, int pattern) {
|
||||
for (int y = 0; y < m.length; y++) {
|
||||
for (int x = 0; x < m.length; x++) {
|
||||
if (!r[y][x] && isMasked(x, y, pattern)) m[y][x] = !m[y][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMasked(int x, int y, int pattern) {
|
||||
return switch (pattern) {
|
||||
case 0 -> (x + y) % 2 == 0;
|
||||
case 1 -> y % 2 == 0;
|
||||
case 2 -> x % 3 == 0;
|
||||
case 3 -> (x + y) % 3 == 0;
|
||||
case 4 -> (y / 2 + x / 3) % 2 == 0;
|
||||
case 5 -> ((x * y) % 2) + ((x * y) % 3) == 0;
|
||||
case 6 -> (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;
|
||||
case 7 -> (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static final int[][] FORMAT_INFO_COORDS_TL = {
|
||||
{8, 0}, {8, 1}, {8, 2}, {8, 3}, {8, 4}, {8, 5}, {8, 7}, {8, 8}, {7, 8}, {5, 8}, {4, 8}, {3, 8}, {2, 8}, {1, 8}, {0, 8}
|
||||
};
|
||||
|
||||
private static void placeFormatInfo(VersionInfo vi, boolean[][] m, int mask) {
|
||||
int info = (vi.ecc.bits << 3) | mask;
|
||||
int bch = bch(info, FORMAT_INFO_POLY, 10);
|
||||
int full = ((info << 10) | bch) ^ FORMAT_INFO_MASK;
|
||||
int dim = m.length;
|
||||
|
||||
for (int i = 0; i < 15; i++) {
|
||||
boolean bit = ((full >> i) & 1) != 0;
|
||||
// Top-left strip
|
||||
m[FORMAT_INFO_COORDS_TL[i][0]][FORMAT_INFO_COORDS_TL[i][1]] = bit;
|
||||
|
||||
// Bottom-left / Top-right strips
|
||||
if (i < 8) m[8][dim - 1 - i] = bit;
|
||||
else m[dim - 15 + i][8] = bit;
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeVersionInfo(VersionInfo vi, boolean[][] m) {
|
||||
int bch = bch(vi.version, VERSION_INFO_POLY, 12);
|
||||
int full = (vi.version << 12) | bch;
|
||||
int dim = m.length;
|
||||
for (int i = 0; i < 18; i++) {
|
||||
boolean bit = ((full >> i) & 1) != 0;
|
||||
m[dim - 11 + i % 3][i / 3] = bit;
|
||||
m[i / 3][dim - 11 + i % 3] = bit;
|
||||
}
|
||||
}
|
||||
|
||||
private static int bch(int data, int poly, int degree) {
|
||||
int d = data << degree;
|
||||
int msb = 31 - Integer.numberOfLeadingZeros(poly);
|
||||
for (int i = 31 - Integer.numberOfLeadingZeros(d); i >= degree; i--) {
|
||||
if (((d >> i) & 1) != 0) d ^= (poly << (i - msb));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private static class BitBuffer {
|
||||
private byte[] data = new byte[32];
|
||||
private int bitIdx = 0;
|
||||
public void add(int value, int bits) {
|
||||
ensureCapacity((bitIdx + bits + 7) / 8);
|
||||
for (int i = bits - 1; i >= 0; i--) {
|
||||
if (((value >> i) & 1) != 0) data[bitIdx / 8] |= (1 << (7 - (bitIdx % 8)));
|
||||
bitIdx++;
|
||||
}
|
||||
}
|
||||
public int bitCount() { return bitIdx; }
|
||||
public byte[] toByteArray() { return Arrays.copyOf(data, (bitIdx + 7) / 8); }
|
||||
private void ensureCapacity(int bytes) {
|
||||
if (bytes > data.length) data = Arrays.copyOf(data, Math.max(data.length * 2, bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// ZJ: Tiny JSON parser, ported from zj.erl.
|
||||
//
|
||||
// NOTE:
|
||||
// Does not *quite* live up to ZJ in every respect, but is sufficient for GRIDS.
|
||||
// Uses standard Java exceptions for errors.
|
||||
|
||||
public final class ZJ {
|
||||
|
||||
public static class ParseException extends RuntimeException {
|
||||
public final int position;
|
||||
public ParseException(String message, int position) {
|
||||
super(message + " at position " + position);
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
private ZJ() {}
|
||||
|
||||
public static Object decode(String json) {
|
||||
if (json == null || json.isEmpty()) return null;
|
||||
return new Parser(json).parse();
|
||||
}
|
||||
|
||||
public static String encode(Object value) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
encodeValue(value, sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void encodeValue(Object value, StringBuilder sb) {
|
||||
if (value == null) {
|
||||
sb.append("null");
|
||||
} else if (value instanceof String) {
|
||||
sb.append('"');
|
||||
escape((String) value, sb);
|
||||
sb.append('"');
|
||||
} else if (value instanceof Boolean) {
|
||||
sb.append(value);
|
||||
} else if (value instanceof Number) {
|
||||
sb.append(value);
|
||||
} else if (value instanceof Map) {
|
||||
encodeMap((Map<?, ?>) value, sb);
|
||||
} else if (value instanceof List) {
|
||||
encodeList((List<?>) value, sb);
|
||||
} else if (value instanceof Object[]) {
|
||||
encodeArray((Object[]) value, sb);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported JSON value type: " + value.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static void encodeMap(Map<?, ?> map, StringBuilder sb) {
|
||||
sb.append('{');
|
||||
boolean first = true;
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
sb.append('"');
|
||||
escape(entry.getKey().toString(), sb);
|
||||
sb.append("\":");
|
||||
encodeValue(entry.getValue(), sb);
|
||||
}
|
||||
sb.append('}');
|
||||
}
|
||||
|
||||
private static void encodeList(List<?> list, StringBuilder sb) {
|
||||
sb.append('[');
|
||||
boolean first = true;
|
||||
for (Object item : list) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
encodeValue(item, sb);
|
||||
}
|
||||
sb.append(']');
|
||||
}
|
||||
|
||||
private static void encodeArray(Object[] array, StringBuilder sb) {
|
||||
sb.append('[');
|
||||
boolean first = true;
|
||||
for (Object item : array) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
encodeValue(item, sb);
|
||||
}
|
||||
sb.append(']');
|
||||
}
|
||||
|
||||
private static void escape(String s, StringBuilder sb) {
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
switch (c) {
|
||||
case '"' -> sb.append("\\\"");
|
||||
case '\\' -> sb.append("\\\\");
|
||||
case '\b' -> sb.append("\\b");
|
||||
case '\f' -> sb.append("\\f");
|
||||
case '\n' -> sb.append("\\n");
|
||||
case '\r' -> sb.append("\\r");
|
||||
case '\t' -> sb.append("\\t");
|
||||
default -> {
|
||||
if (c < 32 || c > 126) {
|
||||
sb.append(String.format("\\u%04x", (int) c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Parser {
|
||||
private final String json;
|
||||
private int pos = 0;
|
||||
|
||||
Parser(String json) {
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
Object parse() {
|
||||
seek();
|
||||
if (pos >= json.length()) return null;
|
||||
Object result = value();
|
||||
seek();
|
||||
if (pos != json.length()) {
|
||||
throw new ParseException("Unexpected trailing data", pos);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object value() {
|
||||
char c = json.charAt(pos);
|
||||
return switch (c) {
|
||||
case '{' -> object();
|
||||
case '[' -> array();
|
||||
case '"' -> string();
|
||||
case 't' -> bool(true);
|
||||
case 'f' -> bool(false);
|
||||
case 'n' -> nil();
|
||||
default -> {
|
||||
if (c == '-' || (c >= '0' && c <= '9')) {
|
||||
yield number();
|
||||
}
|
||||
throw new ParseException("Unexpected character: " + c, pos);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, Object> object() {
|
||||
pos++; // skip '{'
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
seek();
|
||||
if (pos < json.length() && json.charAt(pos) == '}') {
|
||||
pos++;
|
||||
return map;
|
||||
}
|
||||
while (true) {
|
||||
seek();
|
||||
String key = string();
|
||||
seek();
|
||||
if (pos >= json.length() || json.charAt(pos) != ':') {
|
||||
throw new ParseException("Expected ':'", pos);
|
||||
}
|
||||
pos++;
|
||||
seek();
|
||||
Object val = value();
|
||||
map.put(key, val);
|
||||
seek();
|
||||
if (pos < json.length() && json.charAt(pos) == '}') {
|
||||
pos++;
|
||||
break;
|
||||
}
|
||||
if (pos >= json.length() || json.charAt(pos) != ',') {
|
||||
throw new ParseException("Expected ',' or '}'", pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<Object> array() {
|
||||
pos++; // skip '['
|
||||
List<Object> list = new ArrayList<>();
|
||||
seek();
|
||||
if (pos < json.length() && json.charAt(pos) == ']') {
|
||||
pos++;
|
||||
return list;
|
||||
}
|
||||
while (true) {
|
||||
seek();
|
||||
list.add(value());
|
||||
seek();
|
||||
if (pos < json.length() && json.charAt(pos) == ']') {
|
||||
pos++;
|
||||
break;
|
||||
}
|
||||
if (pos >= json.length() || json.charAt(pos) != ',') {
|
||||
throw new ParseException("Expected ',' or ']'", pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String string() {
|
||||
if (json.charAt(pos) != '"') throw new ParseException("Expected '\"'", pos);
|
||||
pos++;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (pos < json.length()) {
|
||||
char c = json.charAt(pos++);
|
||||
if (c == '"') return sb.toString();
|
||||
if (c < 0x20) throw new ParseException("Unescaped control character", pos - 1);
|
||||
if (c == '\\') {
|
||||
if (pos >= json.length()) throw new ParseException("Unterminated escape", pos);
|
||||
char esc = json.charAt(pos++);
|
||||
switch (esc) {
|
||||
case '"' -> sb.append('"');
|
||||
case '\\' -> sb.append('\\');
|
||||
case '/' -> sb.append('/');
|
||||
case 'b' -> sb.append('\b');
|
||||
case 'f' -> sb.append('\f');
|
||||
case 'n' -> sb.append('\n');
|
||||
case 'r' -> sb.append('\r');
|
||||
case 't' -> sb.append('\t');
|
||||
case 'u' -> {
|
||||
if (pos + 4 > json.length()) throw new ParseException("Invalid unicode escape", pos);
|
||||
String hex = json.substring(pos, pos + 4);
|
||||
try {
|
||||
sb.append((char) Integer.parseInt(hex, 16));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParseException("Invalid hex in unicode escape", pos);
|
||||
}
|
||||
pos += 4;
|
||||
}
|
||||
default -> throw new ParseException("Unknown escape: " + esc, pos - 1);
|
||||
}
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
throw new ParseException("Unterminated string", pos);
|
||||
}
|
||||
|
||||
private Boolean bool(boolean expected) {
|
||||
String s = expected ? "true" : "false";
|
||||
if (json.startsWith(s, pos)) {
|
||||
pos += s.length();
|
||||
return expected;
|
||||
}
|
||||
throw new ParseException("Expected " + s, pos);
|
||||
}
|
||||
|
||||
private Object nil() {
|
||||
if (json.startsWith("null", pos)) {
|
||||
pos += 4;
|
||||
return null;
|
||||
}
|
||||
throw new ParseException("Expected null", pos);
|
||||
}
|
||||
|
||||
private Number number() {
|
||||
int start = pos;
|
||||
if (pos < json.length() && json.charAt(pos) == '-') pos++;
|
||||
|
||||
if (pos >= json.length()) throw new ParseException("Unexpected end of number", pos);
|
||||
|
||||
char c = json.charAt(pos);
|
||||
if (c == '0') {
|
||||
pos++;
|
||||
} else if (c >= '1' && c <= '9') {
|
||||
pos++;
|
||||
while (pos < json.length() && isDigit(json.charAt(pos))) pos++;
|
||||
} else {
|
||||
throw new ParseException("Expected digit", pos);
|
||||
}
|
||||
|
||||
boolean isDecimal = false;
|
||||
if (pos < json.length() && json.charAt(pos) == '.') {
|
||||
isDecimal = true;
|
||||
pos++;
|
||||
if (pos >= json.length() || !isDigit(json.charAt(pos))) {
|
||||
throw new ParseException("Expected digit after '.'", pos);
|
||||
}
|
||||
while (pos < json.length() && isDigit(json.charAt(pos))) pos++;
|
||||
}
|
||||
|
||||
if (pos < json.length() && (json.charAt(pos) == 'e' || json.charAt(pos) == 'E')) {
|
||||
isDecimal = true;
|
||||
pos++;
|
||||
if (pos < json.length() && (json.charAt(pos) == '+' || json.charAt(pos) == '-')) pos++;
|
||||
if (pos >= json.length() || !isDigit(json.charAt(pos))) {
|
||||
throw new ParseException("Expected digit in exponent", pos);
|
||||
}
|
||||
while (pos < json.length() && isDigit(json.charAt(pos))) pos++;
|
||||
}
|
||||
|
||||
String s = json.substring(start, pos);
|
||||
if (isDecimal) return new BigDecimal(s);
|
||||
try {
|
||||
return Long.parseLong(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return new BigInteger(s);
|
||||
}
|
||||
}
|
||||
|
||||
private void seek() {
|
||||
while (pos < json.length() && Character.isWhitespace(json.charAt(pos))) pos++;
|
||||
}
|
||||
|
||||
// This is a nitpick but it turns out that Character.isDigit() accepts a bunch of possible
|
||||
// unicode digits as well as ASCII '0' through '9'. That's normally good, but not for
|
||||
// parsing JSON! (Shoutout to P.H. -- "only ever permit ASCII" wins this one time.)
|
||||
private static boolean isDigit(char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
@@ -39,6 +39,14 @@ public final class GajuFormat {
|
||||
public enum Unit { GAJU, PUCK }
|
||||
public record FormatSpec(Type type, Unit unit, char separator, int span) {}
|
||||
|
||||
public static FormatSpec standardSpec(Type type, Unit unit) {
|
||||
return switch (type) {
|
||||
case US -> new FormatSpec(type, unit, ',', 3);
|
||||
case JP -> new FormatSpec(type, unit, ' ', 4);
|
||||
case METRIC, LEGACY -> new FormatSpec(type, unit, ' ', 3);
|
||||
};
|
||||
}
|
||||
|
||||
private GajuFormat() {}
|
||||
|
||||
private static final String GAJU_MARK = "木";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Copyright (c) 2026 QPQ IaaS AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
@@ -7,10 +7,10 @@
|
||||
* 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.
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ IaaS 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.
|
||||
* purchase a commercial license from QPQ IaaS AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
// GRIDS URL handling, ported from hz_grids.erl.
|
||||
|
||||
public final class Grids {
|
||||
|
||||
private Grids() {}
|
||||
|
||||
public enum Verb {
|
||||
SPEND,
|
||||
TRANSFER,
|
||||
SIGN
|
||||
}
|
||||
|
||||
public enum Context {
|
||||
CHAIN,
|
||||
NODE,
|
||||
HTTP,
|
||||
HTTPS
|
||||
}
|
||||
|
||||
public static final class ParseResult {
|
||||
public final Verb verb;
|
||||
public final Context context;
|
||||
public final String location;
|
||||
public final String recipient;
|
||||
public final BigInteger amount;
|
||||
public final byte[] payload;
|
||||
public final String url; // For SIGN verb, the reconstructed URL
|
||||
|
||||
private ParseResult(Verb verb, Context context, String location, String recipient, BigInteger amount, byte[] payload, String url) {
|
||||
this.verb = verb;
|
||||
this.context = context;
|
||||
this.location = location;
|
||||
this.recipient = recipient;
|
||||
this.amount = amount;
|
||||
this.payload = payload;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public static ParseResult spend(Context context, String location, String recipient, BigInteger amount, byte[] payload) {
|
||||
return new ParseResult(Verb.SPEND, context, location, recipient, amount, payload, null);
|
||||
}
|
||||
|
||||
public static ParseResult transfer(Context context, String location, String recipient, BigInteger amount, byte[] payload) {
|
||||
return new ParseResult(Verb.TRANSFER, context, location, recipient, amount, payload, null);
|
||||
}
|
||||
|
||||
public static ParseResult sign(Context context, String url) {
|
||||
return new ParseResult(Verb.SIGN, context, null, null, null, null, url);
|
||||
}
|
||||
}
|
||||
|
||||
public static ParseResult parse(String gridsUrl) {
|
||||
try {
|
||||
URI uri = new URI(gridsUrl);
|
||||
String scheme = uri.getScheme();
|
||||
String host = uri.getHost();
|
||||
String path = uri.getPath();
|
||||
String query = uri.getQuery();
|
||||
|
||||
if (!"grids".equals(scheme) && !"grid".equals(scheme)) {
|
||||
throw new IllegalArgumentException("Invalid scheme: " + scheme);
|
||||
}
|
||||
|
||||
if (path.startsWith("/1/s/")) {
|
||||
String recipient = path.substring(5);
|
||||
Map<String, String> qargs = parseQuery(query);
|
||||
String a = qargs.get("a");
|
||||
BigInteger amount = (a == null || a.isEmpty()) ? BigInteger.ZERO : new BigInteger(a);
|
||||
byte[] payload = decodePayload(qargs.getOrDefault("p", ""));
|
||||
return ParseResult.spend(Context.CHAIN, host, recipient, amount, payload);
|
||||
} else if (path.startsWith("/1/t/")) {
|
||||
String recipient = path.substring(5);
|
||||
Map<String, String> qargs = parseQuery(query);
|
||||
String a = qargs.get("a");
|
||||
BigInteger amount = (a == null || a.isEmpty()) ? BigInteger.ZERO : new BigInteger(a);
|
||||
byte[] payload = decodePayload(qargs.getOrDefault("p", ""));
|
||||
String location = host + (uri.getPort() != -1 ? ":" + uri.getPort() : "");
|
||||
return ParseResult.transfer(Context.NODE, location, recipient, amount, payload);
|
||||
} else if (path.startsWith("/1/d/")) {
|
||||
String subPath = path.substring(4); // Includes the leading slash
|
||||
String httpScheme = "grids".equals(scheme) ? "https" : "http";
|
||||
String hostPart = host + (uri.getPort() != -1 ? ":" + uri.getPort() : "");
|
||||
String httpUrl = httpScheme + "://" + hostPart + subPath + (query != null ? "?" + query : "");
|
||||
return ParseResult.sign("grids".equals(scheme) ? Context.HTTPS : Context.HTTP, httpUrl);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown verb in path: " + path);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse GRIDS URL: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final String REQ_TYPE_MESSAGE = "message";
|
||||
public static final String REQ_TYPE_BINARY = "binary";
|
||||
public static final String REQ_TYPE_TX = "tx";
|
||||
|
||||
public static Map<String, Object> makeRequest(String type, Object payload, String publicId, String networkId) {
|
||||
Map<String, Object> req = new LinkedHashMap<>();
|
||||
req.put("grids", 1);
|
||||
req.put("chain", "gajumaru");
|
||||
req.put("network_id", networkId);
|
||||
req.put("type", type);
|
||||
req.put("public_id", publicId);
|
||||
req.put("payload", payload);
|
||||
return req;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseQuery(String query) {
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
if (query == null || query.isEmpty()) return params;
|
||||
String[] pairs = query.split("&");
|
||||
for (String pair : pairs) {
|
||||
int idx = pair.indexOf("=");
|
||||
if (idx > 0) {
|
||||
params.put(URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8),
|
||||
URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8));
|
||||
} else {
|
||||
params.put(URLDecoder.decode(pair, StandardCharsets.UTF_8), "");
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private static byte[] decodePayload(String p) {
|
||||
if (p == null || p.isEmpty()) return new byte[0];
|
||||
// The Erlang side uses uri_string:compose_query which percent-encodes.
|
||||
// URLDecoder.decode should have handled it in parseQuery.
|
||||
return p.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import swiss.qpq.gajumaru.core.encoding.ZJ;
|
||||
|
||||
// Minimal HTTP client for GRIDS and Node interaction.
|
||||
|
||||
public final class Http {
|
||||
|
||||
private Http() {}
|
||||
|
||||
public static Object get(String url) throws Exception {
|
||||
return request(url, "GET", null);
|
||||
}
|
||||
|
||||
public static Object post(String url, Object body) throws Exception {
|
||||
return request(url, "POST", body);
|
||||
}
|
||||
|
||||
private static Object request(String urlStr, String method, Object body) throws Exception {
|
||||
URL url = new java.net.URI(urlStr).toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod(method);
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(15000);
|
||||
conn.setInstanceFollowRedirects(true);
|
||||
|
||||
// Emulate a standard browser agent to avoid being blocked by strict servers
|
||||
conn.setRequestProperty("Accept", "application/json, text/plain, */*");
|
||||
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 10) GajuMobile/1.0");
|
||||
|
||||
if (body != null) {
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setDoOutput(true);
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
byte[] input;
|
||||
if (body instanceof String) {
|
||||
input = ((String) body).getBytes(StandardCharsets.UTF_8);
|
||||
} else {
|
||||
input = ZJ.encode(body).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
os.write(input, 0, input.length);
|
||||
}
|
||||
}
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
try (java.io.InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
response.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
String responseStr = response.toString().trim();
|
||||
|
||||
if (code < 200 || code >= 300) {
|
||||
String reason = "HTTP " + code;
|
||||
try {
|
||||
Object decoded = ZJ.decode(responseStr);
|
||||
if (decoded instanceof Map) {
|
||||
Object r = ((Map<?, ?>) decoded).get("reason");
|
||||
if (r != null) reason = r.toString();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
throw new Exception(reason + " (URL: " + urlStr + ")");
|
||||
}
|
||||
|
||||
if (responseStr.isEmpty()) return null;
|
||||
|
||||
try {
|
||||
return ZJ.decode(responseStr);
|
||||
} catch (Exception e) {
|
||||
if (responseStr.startsWith("<")) {
|
||||
throw new Exception("Server returned HTML instead of JSON (URL: " + urlStr + ")");
|
||||
}
|
||||
throw new Exception("JSON parse failed for " + urlStr + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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 swiss.qpq.gajumaru.core.crypto.Ed25519;
|
||||
|
||||
/**
|
||||
* JavaSigningProvider implements SigningProvider using pure Java Ed25519 arithmetic.
|
||||
*/
|
||||
public final class JavaSigningProvider implements SigningProvider {
|
||||
|
||||
@Override
|
||||
public byte[] cryptoSignSeedKeypair(byte[] seed) {
|
||||
byte[] pub = Ed25519.publicKey(seed);
|
||||
// Pure Java Ed25519 needs a way to get the expanded secret key.
|
||||
// In Ed25519.sign it's: hash = sha512(seed); s = hash[0..31]; s[0] &= 248; s[31] &= 127; s[31] |= 64;
|
||||
// Prefix is hash[32..63].
|
||||
// Libsodium's 64-byte SK is often [32-byte seed | 32-byte public key].
|
||||
// However, Ed25519.sign implementation expects the 64-byte expanded key to work correctly.
|
||||
// Let's look at how Ed25519.sign works. It derives 's' and 'prefix' from the seed.
|
||||
|
||||
// For parity with Native implementation which returns [32pk | 64sk],
|
||||
// we need to be careful about what 'sk' means.
|
||||
// In libsodium, sk is [32-byte seed | 32-byte public key].
|
||||
// In Ed25519.java, it uses seed directly.
|
||||
|
||||
// I will implement a bridge in Ed25519.java to handle this.
|
||||
|
||||
// FOR NOW: Assume we follow libsodium: SK = [seed(32) | pub(32)]
|
||||
byte[] sk = new byte[64];
|
||||
System.arraycopy(seed, 0, sk, 0, 32);
|
||||
System.arraycopy(pub, 0, sk, 32, 32);
|
||||
|
||||
byte[] res = new byte[96];
|
||||
System.arraycopy(pub, 0, res, 0, 32);
|
||||
System.arraycopy(sk, 0, res, 32, 64);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] cryptoSignDetached(byte[] message, byte[] secretKey) {
|
||||
// Assume secretKey is [seed(32) | pub(32)]
|
||||
byte[] seed = new byte[32];
|
||||
System.arraycopy(secretKey, 0, seed, 0, 32);
|
||||
return Ed25519.sign(seed, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* NodeClient provides a simple interface to query Gajumaru nodes.
|
||||
* Ported from hz.erl
|
||||
*/
|
||||
public final class NodeClient {
|
||||
|
||||
public static final class Endpoint {
|
||||
public final String host;
|
||||
public final int port;
|
||||
public final boolean useTls;
|
||||
|
||||
public Endpoint(String host, int port) {
|
||||
this(host, port, false);
|
||||
}
|
||||
|
||||
public Endpoint(String host, int port, boolean useTls) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.useTls = useTls;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Endpoint> endpoints;
|
||||
|
||||
public NodeClient(List<Endpoint> endpoints) {
|
||||
if (endpoints == null || endpoints.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one endpoint is required");
|
||||
}
|
||||
this.endpoints = endpoints;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> status() throws Exception {
|
||||
return (Map<String, Object>) request("/v3/status", "GET", null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public long topHeight() throws Exception {
|
||||
Map<String, Object> res = (Map<String, Object>) request("/v3/headers/top", "GET", null);
|
||||
return ((Number) res.get("height")).longValue();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> account(String accountId) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/accounts/" + accountId, "GET", null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public long nextNonce(String accountId) throws Exception {
|
||||
try {
|
||||
Map<String, Object> res = (Map<String, Object>) request("/v3/accounts/" + accountId + "/next-nonce", "GET", null);
|
||||
return ((Number) res.get("next_nonce")).longValue();
|
||||
} catch (Exception e) {
|
||||
if (e.getMessage() != null && e.getMessage().contains("Account not found")) {
|
||||
return 1;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> postTx(String signedTx) throws Exception {
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("tx", signedTx);
|
||||
return (Map<String, Object>) request("/v3/transactions", "POST", body);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> tx(String txHash) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/transactions/" + txHash, "GET", null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> txInfo(String txHash) throws Exception {
|
||||
return (Map<String, Object>) request("/v3/transactions/" + txHash + "/info", "GET", null);
|
||||
}
|
||||
|
||||
private Object request(String path, String method, Object body) throws Exception {
|
||||
Exception lastException = null;
|
||||
for (Endpoint endpoint : endpoints) {
|
||||
try {
|
||||
String protocol = endpoint.useTls ? "https://" : "http://";
|
||||
String url = protocol + endpoint.host + ":" + endpoint.port + path;
|
||||
|
||||
if (endpoint.useTls) {
|
||||
try {
|
||||
return "GET".equals(method) ? Http.get(url) : Http.post(url, body);
|
||||
} catch (Exception e) {
|
||||
// Fallback to HTTP
|
||||
String httpUrl = "http://" + endpoint.host + ":" + endpoint.port + path;
|
||||
return "GET".equals(method) ? Http.get(httpUrl) : Http.post(httpUrl, body);
|
||||
}
|
||||
} else {
|
||||
return "GET".equals(method) ? Http.get(url) : Http.post(url, body);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
throw new Exception("All endpoints failed. Last error: " + (lastException != null ? lastException.getMessage() : "unknown"), lastException);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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;
|
||||
|
||||
/**
|
||||
* SigningProvider abstracts Ed25519 operations to allow different implementations
|
||||
* (e.g., pure Java vs. Native libsodium) to be used at runtime.
|
||||
*/
|
||||
public interface SigningProvider {
|
||||
|
||||
/**
|
||||
* Expands a 32-byte seed into a 64-byte secret key and 32-byte public key.
|
||||
* @param seed 32-byte seed.
|
||||
* @return 96-byte array [public_key(32) | secret_key(64)].
|
||||
*/
|
||||
byte[] cryptoSignSeedKeypair(byte[] seed);
|
||||
|
||||
/**
|
||||
* Signs a message using a 64-byte expanded secret key.
|
||||
* @param message The data to sign.
|
||||
* @param secretKey 64-byte secret key.
|
||||
* @return 64-byte detached signature.
|
||||
*/
|
||||
byte[] cryptoSignDetached(byte[] message, byte[] secretKey);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
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.data.SignedTx;
|
||||
import swiss.qpq.gajumaru.core.data.SpendTx;
|
||||
import swiss.qpq.gajumaru.core.encoding.ApiEncoder;
|
||||
|
||||
/**
|
||||
* TransactionService provides high-level tools to build and sign transactions
|
||||
* and other chain data.
|
||||
*/
|
||||
public final class TransactionService {
|
||||
|
||||
private static SigningProvider provider = new JavaSigningProvider();
|
||||
|
||||
private TransactionService() {}
|
||||
|
||||
/**
|
||||
* Sets the signing provider to use for all operations.
|
||||
*/
|
||||
public static void setProvider(SigningProvider newProvider) {
|
||||
if (newProvider != null) {
|
||||
provider = newProvider;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and signs a Spend transaction using a 32-byte seed.
|
||||
*/
|
||||
public static String buildSpendTx(
|
||||
String networkId,
|
||||
String senderId,
|
||||
String recipientId,
|
||||
BigInteger amount,
|
||||
BigInteger gasPrice,
|
||||
BigInteger gas,
|
||||
long ttl,
|
||||
long nonce,
|
||||
String payload,
|
||||
byte[] seed
|
||||
) {
|
||||
// 1. Decode IDs
|
||||
Id sender = new Id(Id.Tag.ACCOUNT, ApiEncoder.decode(senderId).payload());
|
||||
|
||||
ApiEncoder.DecodeResult recipientResult = ApiEncoder.decode(recipientId);
|
||||
Id.Tag recipientTag = recipientResult.type() == ApiEncoder.Type.CONTRACT_PUBKEY ? Id.Tag.CONTRACT : Id.Tag.ACCOUNT;
|
||||
Id recipient = new Id(recipientTag, recipientResult.payload());
|
||||
|
||||
// 2. Build SpendTx
|
||||
byte[] payloadBytes = payload != null ? payload.getBytes(StandardCharsets.UTF_8) : new byte[0];
|
||||
SpendTx tx = new SpendTx(sender, recipient, amount, gasPrice, gas, ttl, nonce, payloadBytes);
|
||||
byte[] serializedTx = tx.serialize();
|
||||
|
||||
// 3. Prepare NetworkHash
|
||||
byte[] networkIdBytes = networkId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] txHash = Blake2b.hash(serializedTx, 32);
|
||||
byte[] networkHash = new byte[networkIdBytes.length + txHash.length];
|
||||
System.arraycopy(networkIdBytes, 0, networkHash, 0, networkIdBytes.length);
|
||||
System.arraycopy(txHash, 0, networkHash, networkIdBytes.length, txHash.length);
|
||||
|
||||
// 4. Expand Seed and Sign
|
||||
byte[] keypair = provider.cryptoSignSeedKeypair(seed);
|
||||
if (keypair == null) throw new RuntimeException("Signing provider keypair generation failed");
|
||||
|
||||
byte[] secretKey = new byte[64];
|
||||
System.arraycopy(keypair, 32, secretKey, 0, 64);
|
||||
|
||||
byte[] signature = provider.cryptoSignDetached(networkHash, secretKey);
|
||||
|
||||
CryptoUtils.wipe(keypair);
|
||||
CryptoUtils.wipe(secretKey);
|
||||
|
||||
if (signature == null) throw new RuntimeException("Native signing failed");
|
||||
|
||||
// 5. Wrap in SignedTx
|
||||
SignedTx signedTx = new SignedTx(List.of(signature), serializedTx);
|
||||
|
||||
return ApiEncoder.encode(ApiEncoder.Type.TRANSACTION, signedTx.serialize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a message using the Gajumaru standard (prefixed and hashed).
|
||||
* @param secretKey 64-byte expanded secret key.
|
||||
*/
|
||||
public static byte[] signMessage(String message, byte[] secretKey) {
|
||||
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] prefix = "Gajumaru Signed Message:\n".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] pSize = VarInt.encode(prefix.length);
|
||||
byte[] mSize = VarInt.encode(msgBytes.length);
|
||||
|
||||
byte[] smashed = new byte[pSize.length + prefix.length + mSize.length + msgBytes.length];
|
||||
int pos = 0;
|
||||
System.arraycopy(pSize, 0, smashed, pos, pSize.length); pos += pSize.length;
|
||||
System.arraycopy(prefix, 0, smashed, pos, prefix.length); pos += prefix.length;
|
||||
System.arraycopy(mSize, 0, smashed, pos, mSize.length); pos += mSize.length;
|
||||
System.arraycopy(msgBytes, 0, smashed, pos, msgBytes.length);
|
||||
|
||||
byte[] hashed = Blake2b.hash(smashed, 32);
|
||||
return provider.cryptoSignDetached(hashed, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs binary data using the Gajumaru standard (prefixed and hashed).
|
||||
* @param secretKey 64-byte expanded secret key.
|
||||
*/
|
||||
public static byte[] signBinary(byte[] data, byte[] secretKey) {
|
||||
byte[] prefix = "Gajumaru Signed Binary:".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] target = new byte[prefix.length + data.length];
|
||||
System.arraycopy(prefix, 0, target, 0, prefix.length);
|
||||
System.arraycopy(data, 0, target, prefix.length, data.length);
|
||||
|
||||
byte[] hashed = Blake2b.hash(target, 32);
|
||||
return provider.cryptoSignDetached(hashed, secretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a binary signature.
|
||||
*/
|
||||
public static boolean verifyBinary(byte[] data, byte[] signature, byte[] publicKey) {
|
||||
byte[] prefix = "Gajumaru Signed Binary:".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] target = new byte[prefix.length + data.length];
|
||||
System.arraycopy(prefix, 0, target, 0, prefix.length);
|
||||
System.arraycopy(data, 0, target, prefix.length, data.length);
|
||||
|
||||
byte[] hashed = Blake2b.hash(target, 32);
|
||||
return Ed25519.verify(publicKey, hashed, signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a transaction.
|
||||
* @param secretKey 64-byte expanded secret key.
|
||||
*/
|
||||
public static String signTx(byte[] txData, String networkId, byte[] secretKey) {
|
||||
byte[] nidBytes = networkId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] hash = Blake2b.hash(txData, 32);
|
||||
|
||||
byte[] networkHash = new byte[nidBytes.length + hash.length];
|
||||
System.arraycopy(nidBytes, 0, networkHash, 0, nidBytes.length);
|
||||
System.arraycopy(hash, 0, networkHash, nidBytes.length, hash.length);
|
||||
|
||||
byte[] signature = provider.cryptoSignDetached(networkHash, secretKey);
|
||||
if (signature == null) throw new RuntimeException("Signing provider signing failed");
|
||||
|
||||
SignedTx signedTx = new SignedTx(List.of(signature), txData);
|
||||
return ApiEncoder.encode(ApiEncoder.Type.TRANSACTION, signedTx.serialize());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ IaaS 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 IaaS 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 IaaS 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.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
// Bitcoin-style Variable-length integer encoding.
|
||||
// Port of vencode in hz.erl.
|
||||
|
||||
public final class VarInt {
|
||||
|
||||
private VarInt() {}
|
||||
|
||||
public static byte[] encode(long n) {
|
||||
if (n < 0) {
|
||||
throw new IllegalArgumentException("Negative values not supported: " + n);
|
||||
}
|
||||
if (n < 0xfd) {
|
||||
return new byte[]{(byte) n};
|
||||
} else if (n <= 0xffff) {
|
||||
ByteBuffer bb = ByteBuffer.allocate(3);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.put((byte) 0xfd);
|
||||
bb.putShort((short) n);
|
||||
return bb.array();
|
||||
} else if (n <= 0xffffffffL) {
|
||||
ByteBuffer bb = ByteBuffer.allocate(5);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.put((byte) 0xfe);
|
||||
bb.putInt((int) n);
|
||||
return bb.array();
|
||||
} else {
|
||||
ByteBuffer bb = ByteBuffer.allocate(9);
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.put((byte) 0xff);
|
||||
bb.putLong(n);
|
||||
return bb.array();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,14 @@ import java.util.Base64;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
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.encoding.ZJ;
|
||||
import swiss.qpq.gajumaru.core.formatting.GajuFormat;
|
||||
import swiss.qpq.gajumaru.core.crypto.Keccak256;
|
||||
import swiss.qpq.gajumaru.core.crypto.Blake2b;
|
||||
@@ -19,6 +22,7 @@ import swiss.qpq.gajumaru.core.data.SpendTx;
|
||||
import swiss.qpq.gajumaru.core.data.SignedTx;
|
||||
import swiss.qpq.gajumaru.core.encoding.Mnemonic;
|
||||
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
||||
import swiss.qpq.gajumaru.core.tools.Grids;
|
||||
|
||||
public class Testinator {
|
||||
public static void main(String[] args) {
|
||||
@@ -40,11 +44,15 @@ public class Testinator {
|
||||
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 "ed25519_malleability" -> { System.out.print(ed25519_malleability()); }
|
||||
case "api_encode" -> { System.out.print(api_encode(args[1])); }
|
||||
case "id_serialization" -> { System.out.print(id_serialization(args[1])); }
|
||||
case "spend_tx" -> { System.out.print(spend_tx(args[1])); }
|
||||
case "signed_tx" -> { System.out.print(signed_tx(args[1])); }
|
||||
case "mnemonic" -> { System.out.print(mnemonic(args[1])); }
|
||||
case "zj_encode" -> { System.out.print(zj_encode(args[1])); }
|
||||
case "zj_decode" -> { System.out.print(zj_decode(args[1])); }
|
||||
case "grids_parse" -> { System.out.print(grids_parse(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])); }
|
||||
@@ -384,6 +392,20 @@ public class Testinator {
|
||||
return CryptoUtils.binToHex(pub) + "|" + CryptoUtils.binToHex(sig) + "|" + verify;
|
||||
}
|
||||
|
||||
private static String ed25519_malleability() {
|
||||
// L in little-endian
|
||||
byte[] L_bytes = {
|
||||
(byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c, (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
|
||||
(byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2, (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10
|
||||
};
|
||||
byte[] sig = new byte[64];
|
||||
System.arraycopy(L_bytes, 0, sig, 32, 32); // S = L
|
||||
boolean result = Ed25519.verify(new byte[32], new byte[0], sig);
|
||||
return Boolean.toString(result);
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -493,4 +515,56 @@ public class Testinator {
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
|
||||
private static String zj_encode(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "zj_encode.test");
|
||||
Path resPath = Path.of(workingPath, "zj_encode.java.txt");
|
||||
List<String> lines = Files.readAllLines(testPath);
|
||||
List<String> results = new ArrayList<>();
|
||||
for (String line : lines) {
|
||||
if (line.trim().isEmpty()) continue;
|
||||
// For testing, we decode then re-encode to check consistency
|
||||
Object val = ZJ.decode(line);
|
||||
results.add(ZJ.encode(val));
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
|
||||
private static String zj_decode(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "zj_decode.test");
|
||||
Path resPath = Path.of(workingPath, "zj_decode.java.txt");
|
||||
List<String> lines = Files.readAllLines(testPath);
|
||||
List<String> results = new ArrayList<>();
|
||||
for (String line : lines) {
|
||||
if (line.trim().isEmpty()) continue;
|
||||
Object val = ZJ.decode(line);
|
||||
// We encode it back to string for comparison or output format
|
||||
results.add(ZJ.encode(val));
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
|
||||
private static String grids_parse(String workingPath) throws IOException {
|
||||
Path testPath = Path.of(workingPath, "grids_parse.test");
|
||||
Path resPath = Path.of(workingPath, "grids_parse.java.txt");
|
||||
List<String> lines = Files.readAllLines(testPath);
|
||||
List<String> results = new ArrayList<>();
|
||||
for (String url : lines) {
|
||||
if (url.trim().isEmpty()) continue;
|
||||
Grids.ParseResult res = Grids.parse(url);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("verb", res.verb.name());
|
||||
map.put("context", res.context.name());
|
||||
map.put("location", res.location);
|
||||
map.put("recipient", res.recipient);
|
||||
map.put("amount", res.amount != null ? res.amount.toString() : null);
|
||||
map.put("payload", res.payload != null ? CryptoUtils.binToHex(res.payload) : null);
|
||||
map.put("url", res.url);
|
||||
results.add(ZJ.encode(map));
|
||||
}
|
||||
Files.write(resPath, results);
|
||||
return resPath.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user