diff --git a/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java b/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java index d09e2f2f..2ea0f0c9 100644 --- a/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java +++ b/nostr-java-api/src/main/java/nostr/api/NostrSpringWebSocketClient.java @@ -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; @@ -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; @@ -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); } } diff --git a/nostr-java-base/src/main/java/nostr/base/BaseKey.java b/nostr-java-base/src/main/java/nostr/base/BaseKey.java index 222dd4bf..8c9d2a91 100644 --- a/nostr-java-base/src/main/java/nostr/base/BaseKey.java +++ b/nostr-java-base/src/main/java/nostr/base/BaseKey.java @@ -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; @@ -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); } } diff --git a/nostr-java-base/src/main/java/nostr/base/KeyEncodingException.java b/nostr-java-base/src/main/java/nostr/base/KeyEncodingException.java new file mode 100644 index 00000000..427c2b4d --- /dev/null +++ b/nostr-java-base/src/main/java/nostr/base/KeyEncodingException.java @@ -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 {} + diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32.java b/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32.java index 5a3af52a..d64f5f51 100644 --- a/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32.java +++ b/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32.java @@ -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); diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32EncodingException.java b/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32EncodingException.java new file mode 100644 index 00000000..8de7b94c --- /dev/null +++ b/nostr-java-crypto/src/main/java/nostr/crypto/bech32/Bech32EncodingException.java @@ -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 {} + diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/Schnorr.java b/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/Schnorr.java index 9919bbc6..58046d6d 100644 --- a/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/Schnorr.java +++ b/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/Schnorr.java @@ -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()) { @@ -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; @@ -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; } @@ -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); @@ -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); diff --git a/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/SchnorrException.java b/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/SchnorrException.java new file mode 100644 index 00000000..00bd11b3 --- /dev/null +++ b/nostr-java-crypto/src/main/java/nostr/crypto/schnorr/SchnorrException.java @@ -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 {} + diff --git a/nostr-java-encryption/src/test/java/nostr/encryption/MessageCipherTest.java b/nostr-java-encryption/src/test/java/nostr/encryption/MessageCipherTest.java index 06703169..ae7f0450 100644 --- a/nostr-java-encryption/src/test/java/nostr/encryption/MessageCipherTest.java +++ b/nostr-java-encryption/src/test/java/nostr/encryption/MessageCipherTest.java @@ -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(); @@ -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(); diff --git a/nostr-java-id/src/main/java/nostr/id/Identity.java b/nostr-java-id/src/main/java/nostr/id/Identity.java index 94ad8b85..04bf9fa1 100644 --- a/nostr-java-id/src/main/java/nostr/id/Identity.java +++ b/nostr-java-id/src/main/java/nostr/id/Identity.java @@ -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; /** @@ -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); } @@ -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); } diff --git a/nostr-java-id/src/test/java/nostr/id/IdentityTest.java b/nostr-java-id/src/test/java/nostr/id/IdentityTest.java index 1ebe2db9..b3e7fc71 100644 --- a/nostr-java-id/src/test/java/nostr/id/IdentityTest.java +++ b/nostr-java-id/src/test/java/nostr/id/IdentityTest.java @@ -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; @@ -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(); @@ -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(); @@ -41,15 +45,17 @@ 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 = @@ -57,8 +63,10 @@ public void testGetPublicKeyDerivation() { 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); @@ -97,24 +105,26 @@ public Supplier 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);