Skip to content

Commit f4e939b

Browse files
committed
test: add pure-Java Ed25519 shadow for JVM unit tests
The native Ed25519 JNI library crashes during JVM test execution due to System.loadLibrary in the static initializer. This shadow class, backed by net.i2p.crypto:eddsa, provides identical Ed25519 operations (key gen, sign, verify, onCurve) without native dependencies. Modules opt in via srcDir pointing to testing/ed25519-shadow, which compiles the shadow into test classes (highest classpath priority).
1 parent 8fb4a13 commit f4e939b

5 files changed

Lines changed: 248 additions & 0 deletions

File tree

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ mixpanel = { module = "com.mixpanel.android:mixpanel-android", version.ref = "mi
258258

259259
# Crypto
260260
sodium-bindings = { module = "com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings-android", version.ref = "sodium-bindings" }
261+
eddsa = { module = "net.i2p.crypto:eddsa", version = "0.3.0" }
261262

262263
# Misc
263264
cloudy = { module = "com.github.skydoves:cloudy", version.ref = "cloudy" }

libs/crypto/solana/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ plugins {
66

77
android {
88
namespace = "${Gradle.codeNamespace}.vendor.solana"
9+
10+
sourceSets.getByName("test") {
11+
java.srcDir(rootProject.file("testing/ed25519-shadow"))
12+
}
913
}
1014

1115
dependencies {
@@ -32,4 +36,5 @@ dependencies {
3236
ksp(libs.hilt.compiler)
3337

3438
testImplementation(kotlin("test"))
39+
testImplementation(libs.eddsa)
3540
}

libs/encryption/mnemonic/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ plugins {
44

55
android {
66
namespace = "${Gradle.codeNamespace}.encryption.mnemonic"
7+
8+
sourceSets.getByName("test") {
9+
java.srcDir(rootProject.file("testing/ed25519-shadow"))
10+
}
711
}
812

913
dependencies {
@@ -18,4 +22,5 @@ dependencies {
1822
implementation(libs.androidx.core)
1923

2024
testImplementation(kotlin("test"))
25+
testImplementation(libs.eddsa)
2126
}

services/opencode/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ plugins {
88
android {
99
namespace = "${Gradle.codeNamespace}.services.opencode"
1010

11+
sourceSets.getByName("test") {
12+
java.srcDir(rootProject.file("testing/ed25519-shadow"))
13+
}
14+
1115
defaultConfig {
1216
consumerProguardFiles("consumer-rules.pro")
1317

@@ -93,5 +97,6 @@ dependencies {
9397
implementation(libs.event.bus)
9498

9599
testImplementation(kotlin("test"))
100+
testImplementation(libs.eddsa)
96101
testImplementation(libs.bundles.unit.testing)
97102
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package com.getcode.ed25519;
2+
3+
import android.os.Parcel;
4+
import android.os.Parcelable;
5+
6+
import net.i2p.crypto.eddsa.EdDSAEngine;
7+
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
8+
import net.i2p.crypto.eddsa.EdDSAPublicKey;
9+
import net.i2p.crypto.eddsa.math.Curve;
10+
import net.i2p.crypto.eddsa.math.GroupElement;
11+
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
12+
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
13+
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
14+
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
15+
16+
import java.security.MessageDigest;
17+
import java.security.SecureRandom;
18+
import java.util.ArrayList;
19+
import java.util.Base64;
20+
import java.util.List;
21+
import java.util.Objects;
22+
23+
/**
24+
* Pure-Java shadow of the native Ed25519 class for JVM unit tests.
25+
* Uses net.i2p.crypto:eddsa for all cryptographic operations.
26+
*
27+
* Place this on the test classpath (via srcDir) so it shadows the real
28+
* Ed25519 class that calls System.loadLibrary in its static initializer.
29+
*/
30+
public class Ed25519 {
31+
32+
private static final EdDSANamedCurveSpec ED25519_SPEC =
33+
EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519);
34+
private static final Curve CURVE = ED25519_SPEC.getCurve();
35+
36+
public static ArrayList<String> GenerateKeyPair(String seed) {
37+
byte[] seedBytes = Base64.getDecoder().decode(seed.trim());
38+
EdDSAPrivateKeySpec privSpec = new EdDSAPrivateKeySpec(seedBytes, ED25519_SPEC);
39+
EdDSAPrivateKey privKey = new EdDSAPrivateKey(privSpec);
40+
EdDSAPublicKeySpec pubSpec = new EdDSAPublicKeySpec(privKey.getA(), ED25519_SPEC);
41+
EdDSAPublicKey pubKey = new EdDSAPublicKey(pubSpec);
42+
43+
byte[] rawPub = pubKey.getAbyte();
44+
// Private key format: seed(32) || publicKey(32) = 64 bytes
45+
byte[] rawPriv = new byte[64];
46+
System.arraycopy(seedBytes, 0, rawPriv, 0, 32);
47+
System.arraycopy(rawPub, 0, rawPriv, 32, 32);
48+
49+
ArrayList<String> result = new ArrayList<>();
50+
result.add(Base64.getEncoder().encodeToString(rawPriv)); // index 0 = private
51+
result.add(Base64.getEncoder().encodeToString(rawPub)); // index 1 = public
52+
return result;
53+
}
54+
55+
public static byte[] CreateSeed16() {
56+
byte[] seed = new byte[16];
57+
new SecureRandom().nextBytes(seed);
58+
return seed;
59+
}
60+
61+
public static byte[] CreateSeed32() {
62+
byte[] seed = new byte[32];
63+
new SecureRandom().nextBytes(seed);
64+
return seed;
65+
}
66+
67+
public static byte[] Signature(byte[] message, byte[] priKey, byte[] pubKey) {
68+
try {
69+
byte[] seed = new byte[32];
70+
System.arraycopy(priKey, 0, seed, 0, 32);
71+
EdDSAPrivateKeySpec privSpec = new EdDSAPrivateKeySpec(seed, ED25519_SPEC);
72+
EdDSAPrivateKey privKeyObj = new EdDSAPrivateKey(privSpec);
73+
74+
EdDSAEngine signer = new EdDSAEngine(MessageDigest.getInstance("SHA-512"));
75+
signer.initSign(privKeyObj);
76+
signer.update(message);
77+
return signer.sign();
78+
} catch (Exception e) {
79+
throw new RuntimeException("Ed25519 signing failed", e);
80+
}
81+
}
82+
83+
public static boolean Verify(byte[] sig, byte[] message, byte[] pubKey) {
84+
try {
85+
EdDSAPublicKeySpec pubSpec = new EdDSAPublicKeySpec(pubKey, ED25519_SPEC);
86+
EdDSAPublicKey pubKeyObj = new EdDSAPublicKey(pubSpec);
87+
88+
EdDSAEngine verifier = new EdDSAEngine(MessageDigest.getInstance("SHA-512"));
89+
verifier.initVerify(pubKeyObj);
90+
verifier.update(message);
91+
return verifier.verify(sig);
92+
} catch (Exception e) {
93+
return false;
94+
}
95+
}
96+
97+
public static boolean OnCurve(byte[] pubKey) {
98+
try {
99+
GroupElement ge = new GroupElement(CURVE, pubKey);
100+
ge.toCached();
101+
return true;
102+
} catch (Exception e) {
103+
return false;
104+
}
105+
}
106+
107+
// No static initializer — no System.loadLibrary calls
108+
109+
public static KeyPair createKeyPair() {
110+
byte[] bytes = createSeed32();
111+
return createKeyPair(bytes);
112+
}
113+
114+
public static KeyPair createKeyPair(String seed) {
115+
List<String> generatedKeyPair = GenerateKeyPair(seed);
116+
return new KeyPair(generatedKeyPair.get(1), generatedKeyPair.get(0), seed);
117+
}
118+
119+
public static KeyPair createKeyPair(byte[] seed) {
120+
String seed64 = Base64.getEncoder().encodeToString(seed);
121+
List<String> generatedKeyPair = GenerateKeyPair(seed64);
122+
return new KeyPair(generatedKeyPair.get(1), generatedKeyPair.get(0), seed64);
123+
}
124+
125+
public static byte[] createSeed16() {
126+
return CreateSeed16();
127+
}
128+
129+
public static byte[] createSeed32() {
130+
return CreateSeed32();
131+
}
132+
133+
public static byte[] sign(byte[] message, KeyPair keyPair) {
134+
return Signature(
135+
message,
136+
keyPair.getPrivateKeyBytes(),
137+
keyPair.getPublicKeyBytes());
138+
}
139+
140+
public static boolean verify(byte[] signature, byte[] message, byte[] publicKey) {
141+
return Verify(signature, message, publicKey);
142+
}
143+
144+
public static boolean onCurve(byte[] pubKey) {
145+
return OnCurve(pubKey);
146+
}
147+
148+
public static class KeyPair implements Parcelable {
149+
private final String publicKey;
150+
private final String privateKey;
151+
private final String seed;
152+
153+
public KeyPair(String publicKey, String privateKey, String seed) {
154+
this.publicKey = publicKey;
155+
this.privateKey = privateKey;
156+
this.seed = seed;
157+
}
158+
159+
protected KeyPair(Parcel in) {
160+
publicKey = in.readString();
161+
privateKey = in.readString();
162+
seed = in.readString();
163+
}
164+
165+
public static final Creator<KeyPair> CREATOR = new Creator<>() {
166+
@Override
167+
public KeyPair createFromParcel(Parcel in) {
168+
return new KeyPair(in);
169+
}
170+
171+
@Override
172+
public KeyPair[] newArray(int size) {
173+
return new KeyPair[size];
174+
}
175+
};
176+
177+
public String getPublicKey() {
178+
return publicKey;
179+
}
180+
181+
public String getPrivateKey() {
182+
return privateKey;
183+
}
184+
185+
public String getSeed() {
186+
return seed;
187+
}
188+
189+
public boolean verify(byte[] sig, byte[] message) {
190+
return Ed25519.Verify(sig, message, getPublicKeyBytes());
191+
}
192+
193+
public byte[] getPrivateKeyBytes() {
194+
if (privateKey == null) return null;
195+
return Base64.getDecoder().decode(privateKey.trim());
196+
}
197+
198+
public byte[] getPublicKeyBytes() {
199+
if (publicKey == null) return null;
200+
return Base64.getDecoder().decode(publicKey.trim());
201+
}
202+
203+
public byte[] sign(byte[] message) {
204+
return Ed25519.sign(message, this);
205+
}
206+
207+
@Override
208+
public boolean equals(Object o) {
209+
if (this == o) return true;
210+
if (o == null || getClass() != o.getClass()) return false;
211+
KeyPair keyPair = (KeyPair) o;
212+
return Objects.equals(publicKey, keyPair.publicKey) && Objects.equals(privateKey, keyPair.privateKey);
213+
}
214+
215+
@Override
216+
public int hashCode() {
217+
return Objects.hash(publicKey, privateKey);
218+
}
219+
220+
@Override
221+
public int describeContents() {
222+
return 0;
223+
}
224+
225+
@Override
226+
public void writeToParcel(Parcel dest, int flags) {
227+
dest.writeString(publicKey);
228+
dest.writeString(privateKey);
229+
dest.writeString(seed);
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)