Compare commits
8
Commits
7493b473a7
...
11516b1cca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11516b1cca | ||
|
|
c1c13213c1 | ||
|
|
5407431566 | ||
|
|
ea7a013abe | ||
|
|
ddc811b63d | ||
|
|
c21f399b69 | ||
|
|
97bb833d35 | ||
|
|
77dea4f3cf |
@@ -30,7 +30,21 @@ 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.
|
||||
//
|
||||
// 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.
|
||||
@@ -63,9 +77,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 +113,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 +188,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 +220,11 @@ public final class Ed25519 {
|
||||
return ok;
|
||||
}
|
||||
|
||||
// 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 +246,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 +268,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 +293,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 +330,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 +375,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 +410,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 +444,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 +468,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 +492,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 +507,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 +641,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);
|
||||
|
||||
@@ -25,17 +25,9 @@ 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";
|
||||
@@ -43,14 +35,36 @@ public final class Vault {
|
||||
|
||||
private Vault() {}
|
||||
|
||||
/**
|
||||
* Holds the results of an encryption operation.
|
||||
*/
|
||||
public static final class Ciphertext {
|
||||
private final byte[] iv;
|
||||
private final byte[] data;
|
||||
|
||||
public Ciphertext(byte[] iv, byte[] data) {
|
||||
this.iv = iv;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public byte[] getIv() { return iv; }
|
||||
public byte[] getData() { return 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.
|
||||
* Letting the provider generate the IV (required by Android Keystore).
|
||||
*/
|
||||
public static Ciphertext encrypt(SecretKey key, byte[] plaintext) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance(AES_GCM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
byte[] iv = cipher.getIV();
|
||||
byte[] ciphertext = cipher.doFinal(plaintext);
|
||||
return new Ciphertext(iv, ciphertext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -61,13 +75,6 @@ public final class Vault {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = "木";
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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);
|
||||
BigInteger amount = new BigInteger(qargs.getOrDefault("a", "0"));
|
||||
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);
|
||||
BigInteger amount = new BigInteger(qargs.getOrDefault("a", "0"));
|
||||
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(5);
|
||||
String httpScheme = "grids".equals(scheme) ? "https" : "http";
|
||||
String baseUrl = httpScheme + "://" + host + (uri.getPort() != -1 ? ":" + uri.getPort() : "") + subPath;
|
||||
String httpUrl = query != null ? baseUrl + "?" + query : baseUrl;
|
||||
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_TX = "tx";
|
||||
public static final String REQ_TYPE_ACK = "ack";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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