From 7cd299d250963ccefad8325a25bd29eb4c86c63e Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Wed, 26 Aug 2026 18:18:46 +0900 Subject: [PATCH] WIP: Alternative route. Still need better conformance to libsodium's Ed25519 API, but almost there. --- .../qpq/gajumaru/core/crypto/Ed25519.java | 14 ++++ .../qpq/gajumaru/core/encoding/Mnemonic.java | 2 +- .../swiss/qpq/gajumaru/core/tools/Grids.java | 3 - .../swiss/qpq/gajumaru/core/tools/Http.java | 8 +-- .../core/tools/JavaSigningProvider.java | 65 +++++++++++++++++++ .../qpq/gajumaru/core/tools/NodeClient.java | 7 ++ .../gajumaru/core/tools/SigningProvider.java | 43 ++++++++++++ .../core/tools/TransactionService.java | 44 ++++++++++--- 8 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 src/main/java/swiss/qpq/gajumaru/core/tools/JavaSigningProvider.java create mode 100644 src/main/java/swiss/qpq/gajumaru/core/tools/SigningProvider.java diff --git a/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java b/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java index 87359a8..a7ce088 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java +++ b/src/main/java/swiss/qpq/gajumaru/core/crypto/Ed25519.java @@ -228,6 +228,20 @@ 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. diff --git a/src/main/java/swiss/qpq/gajumaru/core/encoding/Mnemonic.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/Mnemonic.java index a2e2cb2..51ffec0 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/encoding/Mnemonic.java +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/Mnemonic.java @@ -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", diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java b/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java index 736fa78..dfe0298 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java @@ -79,7 +79,6 @@ public final class Grids { } public static ParseResult parse(String gridsUrl) { - android.util.Log.i("Grids", "Parsing URL: " + gridsUrl); try { URI uri = new URI(gridsUrl); String scheme = uri.getScheme(); @@ -87,8 +86,6 @@ public final class Grids { String path = uri.getPath(); String query = uri.getQuery(); - android.util.Log.i("Grids", "Components: scheme=" + scheme + ", host=" + host + ", path=" + path); - if (!"grids".equals(scheme) && !"grid".equals(scheme)) { throw new IllegalArgumentException("Invalid scheme: " + scheme); } diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/Http.java b/src/main/java/swiss/qpq/gajumaru/core/tools/Http.java index 2818a08..58b5352 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/tools/Http.java +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/Http.java @@ -44,8 +44,7 @@ public final class Http { } private static Object request(String urlStr, String method, Object body) throws Exception { - android.util.Log.i("Http", "Request [" + method + "]: " + urlStr); - URL url = new URL(urlStr); + URL url = new java.net.URI(urlStr).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod(method); conn.setConnectTimeout(15000); @@ -71,7 +70,6 @@ public final class Http { } int code = conn.getResponseCode(); - android.util.Log.i("Http", "Response Code for " + urlStr + ": " + code); StringBuilder response = new StringBuilder(); try (java.io.InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream(); @@ -102,9 +100,7 @@ public final class Http { return ZJ.decode(responseStr); } catch (Exception e) { if (responseStr.startsWith("<")) { - android.util.Log.e("Http", "Server returned HTML for " + urlStr + ". Body preview: " + - (responseStr.length() > 500 ? responseStr.substring(0, 500) : responseStr)); - throw new Exception("Server returned HTML instead of JSON. Full URL and body logged to Logcat."); + throw new Exception("Server returned HTML instead of JSON (URL: " + urlStr + ")"); } throw new Exception("JSON parse failed for " + urlStr + ": " + e.getMessage()); } diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/JavaSigningProvider.java b/src/main/java/swiss/qpq/gajumaru/core/tools/JavaSigningProvider.java new file mode 100644 index 0000000..e272235 --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/JavaSigningProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * This program is dual-licensed: + * 1) Under the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version (AGPL-3.0-or-later). + * + * 2) Under a commercial/proprietary license available directly from QPQ AG. + * If you wish to use this software outside the strict constraints of the + * AGPLv3 (e.g. within a closed-source or proprietary product), you must + * purchase a commercial license from QPQ AG. + * + * Authors: + * - Craig Everett + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.tools; + +import 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); + } +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/NodeClient.java b/src/main/java/swiss/qpq/gajumaru/core/tools/NodeClient.java index f6ead5d..462a9ab 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/tools/NodeClient.java +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/NodeClient.java @@ -55,19 +55,23 @@ public final class NodeClient { this.endpoints = endpoints; } + @SuppressWarnings("unchecked") public Map status() throws Exception { return (Map) request("/v3/status", "GET", null); } + @SuppressWarnings("unchecked") public long topHeight() throws Exception { Map res = (Map) request("/v3/headers/top", "GET", null); return ((Number) res.get("height")).longValue(); } + @SuppressWarnings("unchecked") public Map account(String accountId) throws Exception { return (Map) request("/v3/accounts/" + accountId, "GET", null); } + @SuppressWarnings("unchecked") public long nextNonce(String accountId) throws Exception { try { Map res = (Map) request("/v3/accounts/" + accountId + "/next-nonce", "GET", null); @@ -80,16 +84,19 @@ public final class NodeClient { } } + @SuppressWarnings("unchecked") public Map postTx(String signedTx) throws Exception { Map body = new HashMap<>(); body.put("tx", signedTx); return (Map) request("/v3/transactions", "POST", body); } + @SuppressWarnings("unchecked") public Map tx(String txHash) throws Exception { return (Map) request("/v3/transactions/" + txHash, "GET", null); } + @SuppressWarnings("unchecked") public Map txInfo(String txHash) throws Exception { return (Map) request("/v3/transactions/" + txHash + "/info", "GET", null); } diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/SigningProvider.java b/src/main/java/swiss/qpq/gajumaru/core/tools/SigningProvider.java new file mode 100644 index 0000000..95d6b8c --- /dev/null +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/SigningProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 QPQ AG . All rights reserved. + * Project: Gajumaru Core Java Libraries + * + * This program is dual-licensed: + * 1) Under the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version (AGPL-3.0-or-later). + * + * 2) Under a commercial/proprietary license available directly from QPQ AG. + * If you wish to use this software outside the strict constraints of the + * AGPLv3 (e.g. within a closed-source or proprietary product), you must + * purchase a commercial license from QPQ AG. + * + * Authors: + * - Craig Everett + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial + */ + +package swiss.qpq.gajumaru.core.tools; + +/** + * 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); +} diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/TransactionService.java b/src/main/java/swiss/qpq/gajumaru/core/tools/TransactionService.java index 6bb8afb..eaf7df1 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/tools/TransactionService.java +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/TransactionService.java @@ -24,7 +24,7 @@ import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.List; import swiss.qpq.gajumaru.core.crypto.Blake2b; -import swiss.qpq.gajumobile.security.GajuNative; +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; @@ -36,8 +36,19 @@ import swiss.qpq.gajumaru.core.encoding.ApiEncoder; */ 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. */ @@ -73,16 +84,16 @@ public final class TransactionService { System.arraycopy(txHash, 0, networkHash, networkIdBytes.length, txHash.length); // 4. Expand Seed and Sign - byte[] keypair = GajuNative.cryptoSignSeedKeypair(seed); - if (keypair == null) throw new RuntimeException("Native keypair generation failed"); + 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 = GajuNative.cryptoSignDetached(networkHash, secretKey); + byte[] signature = provider.cryptoSignDetached(networkHash, secretKey); - GajuNative.memzero(keypair); - GajuNative.memzero(secretKey); + CryptoUtils.wipe(keypair); + CryptoUtils.wipe(secretKey); if (signature == null) throw new RuntimeException("Native signing failed"); @@ -111,7 +122,7 @@ public final class TransactionService { System.arraycopy(msgBytes, 0, smashed, pos, msgBytes.length); byte[] hashed = Blake2b.hash(smashed, 32); - return GajuNative.cryptoSignDetached(hashed, secretKey); + return provider.cryptoSignDetached(hashed, secretKey); } /** @@ -125,7 +136,20 @@ public final class TransactionService { System.arraycopy(data, 0, target, prefix.length, data.length); byte[] hashed = Blake2b.hash(target, 32); - return GajuNative.cryptoSignDetached(hashed, secretKey); + 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); } /** @@ -140,8 +164,8 @@ public final class TransactionService { System.arraycopy(nidBytes, 0, networkHash, 0, nidBytes.length); System.arraycopy(hash, 0, networkHash, nidBytes.length, hash.length); - byte[] signature = GajuNative.cryptoSignDetached(networkHash, secretKey); - if (signature == null) throw new RuntimeException("Native signing failed"); + 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());