WIP: Alternative route. Still need better conformance to libsodium's Ed25519 API, but almost there.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
* 1) Under the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version (AGPL-3.0-or-later).
|
||||
*
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
* If you wish to use this software outside the strict constraints of the
|
||||
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
|
||||
* purchase a commercial license from QPQ AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
*/
|
||||
|
||||
package swiss.qpq.gajumaru.core.tools;
|
||||
|
||||
import 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);
|
||||
}
|
||||
}
|
||||
@@ -55,19 +55,23 @@ public final class NodeClient {
|
||||
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);
|
||||
@@ -80,16 +84,19 @@ public final class NodeClient {
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
||||
* Project: Gajumaru Core Java Libraries <gajumaru.io>
|
||||
*
|
||||
* This program is dual-licensed:
|
||||
* 1) Under the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option)
|
||||
* any later version (AGPL-3.0-or-later).
|
||||
*
|
||||
* 2) Under a commercial/proprietary license available directly from QPQ AG.
|
||||
* If you wish to use this software outside the strict constraints of the
|
||||
* AGPLv3 (e.g. within a closed-source or proprietary product), you must
|
||||
* purchase a commercial license from QPQ AG.
|
||||
*
|
||||
* Authors:
|
||||
* - Craig Everett <craigeverett@qpq.swiss>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-QPQ-Commercial
|
||||
*/
|
||||
|
||||
package swiss.qpq.gajumaru.core.tools;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user