Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix: replace generic exception handling
  • Loading branch information
tcheeric committed Oct 6, 2025
commit 7e9c1396514e467ca4cefbff17446a2ca9b25fe0
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package nostr.api;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand All @@ -20,6 +21,7 @@
import nostr.base.ISignable;
import nostr.client.springwebsocket.SpringWebSocketClient;
import nostr.crypto.schnorr.Schnorr;
import nostr.crypto.schnorr.SchnorrException;
import nostr.event.filter.Filters;
import nostr.event.impl.GenericEvent;
import nostr.event.message.ReqMessage;
Expand Down Expand Up @@ -313,8 +315,10 @@ public boolean verify(@NonNull GenericEvent event) {
try {
var message = NostrUtil.sha256(event.get_serializedEvent());
return Schnorr.verify(message, event.getPubKey().getRawData(), signature.getRawData());
} catch (Exception e) {
throw new RuntimeException(e);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 algorithm not available", e);
} catch (SchnorrException e) {
throw new IllegalStateException("Failed to verify Schnorr signature", e);
}
}

Expand Down
9 changes: 7 additions & 2 deletions nostr-java-base/src/main/java/nostr/base/BaseKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import nostr.crypto.bech32.Bech32;
import nostr.crypto.bech32.Bech32EncodingException;
import nostr.crypto.bech32.Bech32Prefix;
import nostr.util.NostrUtil;

Expand All @@ -29,9 +30,13 @@ public abstract class BaseKey implements IKey {
public String toBech32String() {
try {
return Bech32.toBech32(prefix, rawData);
} catch (Exception ex) {
} catch (IllegalArgumentException ex) {
log.error(
"Invalid key data for Bech32 conversion for {} key with prefix {}", type, prefix, ex);
throw new KeyEncodingException("Invalid key data for Bech32 conversion", ex);
} catch (Bech32EncodingException ex) {
log.error("Failed to convert {} key to Bech32 format with prefix {}", type, prefix, ex);
throw new RuntimeException("Failed to convert key to Bech32: " + ex.getMessage(), ex);
throw new KeyEncodingException("Failed to convert key to Bech32", ex);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nostr.base;

import lombok.experimental.StandardException;

/** Exception thrown when encoding a key to Bech32 fails. */
@StandardException
public class KeyEncodingException extends RuntimeException {}

15 changes: 10 additions & 5 deletions nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,18 @@ private Bech32Data(final Encoding encoding, final String hrp, final byte[] data)
}
}

public static String toBech32(Bech32Prefix hrp, byte[] hexKey) throws Exception {
byte[] data = convertBits(hexKey, 8, 5, true);

return Bech32.encode(Bech32.Encoding.BECH32, hrp.getCode(), data);
public static String toBech32(Bech32Prefix hrp, byte[] hexKey) {
try {
byte[] data = convertBits(hexKey, 8, 5, true);
return Bech32.encode(Bech32.Encoding.BECH32, hrp.getCode(), data);
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
throw new Bech32EncodingException("Failed to encode key to Bech32", e);
}
}

public static String toBech32(Bech32Prefix hrp, String hexKey) throws Exception {
public static String toBech32(Bech32Prefix hrp, String hexKey) {
byte[] data = NostrUtil.hexToBytes(hexKey);

return toBech32(hrp, data);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nostr.crypto.bech32;

import lombok.experimental.StandardException;

/** Exception thrown when Bech32 encoding or decoding fails. */
@StandardException
public class Bech32EncodingException extends RuntimeException {}

22 changes: 11 additions & 11 deletions nostr-java-crypto/src/main/java/nostr/crypto/schnorr/Schnorr.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ public class Schnorr {
* @return 64-byte signature (R || s)
* @throws Exception if inputs are invalid or signing fails
*/
public static byte[] sign(byte[] msg, byte[] secKey, byte[] auxRand) throws Exception {
public static byte[] sign(byte[] msg, byte[] secKey, byte[] auxRand) throws SchnorrException {
if (msg.length != 32) {
throw new Exception("The message must be a 32-byte array.");
throw new SchnorrException("The message must be a 32-byte array.");
}
BigInteger secKey0 = NostrUtil.bigIntFromBytes(secKey);

if (!(BigInteger.ONE.compareTo(secKey0) <= 0
&& secKey0.compareTo(Point.getn().subtract(BigInteger.ONE)) <= 0)) {
throw new Exception("The secret key must be an integer in the range 1..n-1.");
throw new SchnorrException("The secret key must be an integer in the range 1..n-1.");
}
Point P = Point.mul(Point.getG(), secKey0);
if (!P.hasEvenY()) {
Expand All @@ -56,7 +56,7 @@ public static byte[] sign(byte[] msg, byte[] secKey, byte[] auxRand) throws Exce
BigInteger k0 =
NostrUtil.bigIntFromBytes(Point.taggedHash("BIP0340/nonce", buf)).mod(Point.getn());
if (k0.compareTo(BigInteger.ZERO) == 0) {
throw new Exception("Failure. This happens only with negligible probability.");
throw new SchnorrException("Failure. This happens only with negligible probability.");
}
Point R = Point.mul(Point.getG(), k0);
BigInteger k;
Expand All @@ -83,7 +83,7 @@ public static byte[] sign(byte[] msg, byte[] secKey, byte[] auxRand) throws Exce
R.toBytes().length,
NostrUtil.bytesFromBigInteger(kes).length);
if (!verify(msg, P.toBytes(), sig)) {
throw new Exception("The signature does not pass verification.");
throw new SchnorrException("The signature does not pass verification.");
}
return sig;
}
Expand All @@ -97,17 +97,17 @@ public static byte[] sign(byte[] msg, byte[] secKey, byte[] auxRand) throws Exce
* @return true if the signature is valid; false otherwise
* @throws Exception if inputs are invalid
*/
public static boolean verify(byte[] msg, byte[] pubkey, byte[] sig) throws Exception {
public static boolean verify(byte[] msg, byte[] pubkey, byte[] sig) throws SchnorrException {

if (msg.length != 32) {
throw new Exception("The message must be a 32-byte array.");
throw new SchnorrException("The message must be a 32-byte array.");
}

if (pubkey.length != 32) {
throw new Exception("The public key must be a 32-byte array.");
throw new SchnorrException("The public key must be a 32-byte array.");
}
if (sig.length != 64) {
throw new Exception("The signature must be a 64-byte array.");
throw new SchnorrException("The signature must be a 64-byte array.");
}

Point P = Point.liftX(pubkey);
Expand Down Expand Up @@ -151,11 +151,11 @@ public static byte[] generatePrivateKey() {
}
}

public static byte[] genPubKey(byte[] secKey) throws Exception {
public static byte[] genPubKey(byte[] secKey) throws SchnorrException {
BigInteger x = NostrUtil.bigIntFromBytes(secKey);
if (!(BigInteger.ONE.compareTo(x) <= 0
&& x.compareTo(Point.getn().subtract(BigInteger.ONE)) <= 0)) {
throw new Exception("The secret key must be an integer in the range 1..n-1.");
throw new SchnorrException("The secret key must be an integer in the range 1..n-1.");
}
Point ret = Point.mul(Point.G, x);
return Point.bytesFromPoint(ret);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nostr.crypto.schnorr;

import lombok.experimental.StandardException;

/** Exception thrown when Schnorr signing or verification fails. */
@StandardException
public class SchnorrException extends Exception {}

Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import static org.junit.jupiter.api.Assertions.assertEquals;

import nostr.crypto.schnorr.Schnorr;
import nostr.crypto.schnorr.SchnorrException;
import org.junit.jupiter.api.Test;

class MessageCipherTest {

@Test
void testMessageCipher04EncryptDecrypt() throws Exception {
// Validates that MessageCipher04 encrypts and decrypts symmetrically
void testMessageCipher04EncryptDecrypt() throws SchnorrException {
byte[] alicePriv = Schnorr.generatePrivateKey();
byte[] alicePub = Schnorr.genPubKey(alicePriv);
byte[] bobPriv = Schnorr.generatePrivateKey();
Expand All @@ -23,7 +25,8 @@ void testMessageCipher04EncryptDecrypt() throws Exception {
}

@Test
void testMessageCipher44EncryptDecrypt() throws Exception {
// Validates that MessageCipher44 encrypts and decrypts symmetrically
void testMessageCipher44EncryptDecrypt() throws SchnorrException {
byte[] alicePriv = Schnorr.generatePrivateKey();
byte[] alicePub = Schnorr.genPubKey(alicePriv);
byte[] bobPriv = Schnorr.generatePrivateKey();
Expand Down
11 changes: 9 additions & 2 deletions nostr-java-id/src/main/java/nostr/id/Identity.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import nostr.base.PublicKey;
import nostr.base.Signature;
import nostr.crypto.schnorr.Schnorr;
import nostr.crypto.schnorr.SchnorrException;
import nostr.util.NostrUtil;

/**
Expand Down Expand Up @@ -75,7 +76,10 @@ public PublicKey getPublicKey() {
if (cachedPublicKey == null) {
try {
cachedPublicKey = new PublicKey(Schnorr.genPubKey(this.getPrivateKey().getRawData()));
} catch (Exception ex) {
} catch (IllegalArgumentException ex) {
log.error("Invalid private key while deriving public key", ex);
throw new IllegalStateException("Invalid private key", ex);
} catch (SchnorrException ex) {
log.error("Failed to derive public key", ex);
throw new IllegalStateException("Failed to derive public key", ex);
}
Expand Down Expand Up @@ -110,7 +114,10 @@ public Signature sign(@NonNull ISignable signable) {
} catch (NoSuchAlgorithmException ex) {
log.error("SHA-256 algorithm not available for signing", ex);
throw new IllegalStateException("SHA-256 algorithm not available", ex);
} catch (Exception ex) {
} catch (IllegalArgumentException ex) {
log.error("Invalid signing input", ex);
throw new SigningException("Failed to sign because of invalid input", ex);
} catch (SchnorrException ex) {
log.error("Signing failed", ex);
throw new SigningException("Failed to sign with provided key", ex);
}
Expand Down
50 changes: 30 additions & 20 deletions nostr-java-id/src/test/java/nostr/id/IdentityTest.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package nostr.id;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.function.Consumer;
import java.util.function.Supplier;
import nostr.base.ISignable;
import nostr.base.PublicKey;
import nostr.base.Signature;
import nostr.crypto.schnorr.Schnorr;
import nostr.crypto.schnorr.Schnorr;
import nostr.crypto.schnorr.SchnorrException;
import nostr.event.impl.GenericEvent;
import nostr.event.tag.DelegationTag;
import nostr.util.NostrUtil;
Expand All @@ -21,8 +23,9 @@ public class IdentityTest {

public IdentityTest() {}

@Test
public void testSignEvent() {
@Test
// Ensures signing a text note event attaches a signature
public void testSignEvent() {
System.out.println("testSignEvent");
Identity identity = Identity.generateRandomIdentity();
PublicKey publicKey = identity.getPublicKey();
Expand All @@ -31,8 +34,9 @@ public void testSignEvent() {
Assertions.assertNotNull(instance.getSignature());
}

@Test
public void testSignDelegationTag() {
@Test
// Ensures signing a delegation tag populates its signature
public void testSignDelegationTag() {
System.out.println("testSignDelegationTag");
Identity identity = Identity.generateRandomIdentity();
PublicKey publicKey = identity.getPublicKey();
Expand All @@ -41,24 +45,28 @@ public void testSignDelegationTag() {
Assertions.assertNotNull(delegationTag.getSignature());
}

@Test
public void testGenerateRandomIdentityProducesUniqueKeys() {
@Test
// Verifies that generating random identities yields unique private keys
public void testGenerateRandomIdentityProducesUniqueKeys() {
Identity id1 = Identity.generateRandomIdentity();
Identity id2 = Identity.generateRandomIdentity();
Assertions.assertNotEquals(id1.getPrivateKey(), id2.getPrivateKey());
}

@Test
public void testGetPublicKeyDerivation() {
@Test
// Confirms that deriving the public key from a known private key matches expectations
public void testGetPublicKeyDerivation() {
String privHex = "0000000000000000000000000000000000000000000000000000000000000001";
Identity identity = Identity.create(privHex);
PublicKey expected =
new PublicKey("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798");
Assertions.assertEquals(expected, identity.getPublicKey());
}

@Test
public void testSignProducesValidSignature() throws Exception {
@Test
// Verifies that signing produces a Schnorr signature that validates successfully
public void testSignProducesValidSignature()
throws NoSuchAlgorithmException, SchnorrException {
String privHex = "0000000000000000000000000000000000000000000000000000000000000001";
Identity identity = Identity.create(privHex);
final byte[] message = "hello".getBytes(StandardCharsets.UTF_8);
Expand Down Expand Up @@ -97,24 +105,26 @@ public Supplier<ByteBuffer> getByteArraySupplier() {
Assertions.assertTrue(verified);
}

@Test
public void testPublicKeyCaching() {
@Test
// Confirms public key derivation is cached for subsequent calls
public void testPublicKeyCaching() {
Identity identity = Identity.generateRandomIdentity();
PublicKey first = identity.getPublicKey();
PublicKey second = identity.getPublicKey();
Assertions.assertSame(first, second);
}

@Test
public void testGetPublicKeyFailure() {
@Test
// Ensures that invalid private keys trigger a derivation failure
public void testGetPublicKeyFailure() {
String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000";
Identity identity = Identity.create(invalidPriv);
Assertions.assertThrows(IllegalStateException.class, identity::getPublicKey);
}

@Test
// Ensures that signing with an invalid private key throws SigningException
public void testSignWithInvalidKeyFails() {
@Test
// Ensures that signing with an invalid private key throws SigningException
public void testSignWithInvalidKeyFails() {
String invalidPriv = "0000000000000000000000000000000000000000000000000000000000000000";
Identity identity = Identity.create(invalidPriv);

Expand Down