From 3520cf82195d928231af7af50e6ad8e3fbe46954 Mon Sep 17 00:00:00 2001 From: Wilson Silva Date: Mon, 20 Nov 2023 09:59:27 +0700 Subject: [PATCH] Implement NIP-19 bech32-encoded private and public keys https://github.com/nostr-protocol/nips/blob/master/19.md --- .rubocop.yml | 14 +++ CHANGELOG.md | 20 +++- README.md | 12 ++- docs/core/keys.md | 93 ++++++++++++---- docs/getting-started/overview.md | 12 ++- lib/nostr.rb | 4 + lib/nostr/crypto.rb | 14 +-- lib/nostr/errors.rb | 8 ++ lib/nostr/errors/error.rb | 7 ++ lib/nostr/errors/invalid_hrp_error.rb | 21 ++++ lib/nostr/errors/invalid_key_format_error.rb | 20 ++++ lib/nostr/errors/invalid_key_length_error.rb | 20 ++++ lib/nostr/errors/invalid_key_type_error.rb | 18 ++++ lib/nostr/errors/key_validation_error.rb | 6 ++ lib/nostr/event.rb | 6 +- lib/nostr/events/encrypted_direct_message.rb | 5 +- lib/nostr/key.rb | 102 ++++++++++++++++++ lib/nostr/key_pair.rb | 36 +++++-- lib/nostr/keygen.rb | 47 +++++++- lib/nostr/private_key.rb | 36 +++++++ lib/nostr/public_key.rb | 36 +++++++ sig/nostr/client.rbs | 10 +- sig/nostr/crypto.rbs | 8 +- sig/nostr/errors/error.rbs | 4 + sig/nostr/errors/invalid_hrb_error.rbs | 6 ++ sig/nostr/errors/invalid_key_format_error.rbs | 5 + sig/nostr/errors/invalid_key_length_error.rbs | 5 + sig/nostr/errors/invalid_key_type_error.rbs | 5 + sig/nostr/errors/key_validation_error.rbs | 4 + sig/nostr/event.rbs | 8 +- sig/nostr/events/encrypted_direct_message.rbs | 4 +- sig/nostr/key.rbs | 16 +++ sig/nostr/key_pair.rbs | 10 +- sig/nostr/keygen.rbs | 7 +- sig/nostr/private_key.rbs | 4 + sig/nostr/public_key.rbs | 4 + sig/vendor/bech32.rbs | 25 +++++ sig/vendor/bech32/nostr/entity.rbs | 41 +++++++ sig/vendor/bech32/nostr/nip19.rbs | 20 ++++ sig/vendor/bech32/segwit_addr.rbs | 21 ++++ sig/vendor/event_emitter.rbs | 13 ++- sig/vendor/event_machine/channel.rbs | 2 +- sig/vendor/faye/websocket.rbs | 30 ++++++ sig/vendor/faye/websocket/api.rbs | 45 ++++++++ sig/vendor/faye/websocket/client.rbs | 43 ++++++++ spec/nostr/crypto_spec.rb | 20 ++-- spec/nostr/errors/invalid_hrp_error_spec.rb | 15 +++ .../errors/invalid_key_format_error_spec.rb | 14 +++ .../errors/invalid_key_length_error_spec.rb | 14 +++ .../errors/invalid_key_type_error_spec.rb | 14 +++ spec/nostr/event_spec.rb | 4 +- .../events/encrypted_direct_message_spec.rb | 8 +- spec/nostr/key_pair_spec.rb | 40 +++++-- spec/nostr/key_spec.rb | 73 +++++++++++++ spec/nostr/keygen_spec.rb | 46 +++++++- spec/nostr/private_key_spec.rb | 82 ++++++++++++++ spec/nostr/public_key_spec.rb | 82 ++++++++++++++ spec/nostr/user_spec.rb | 4 +- 58 files changed, 1189 insertions(+), 104 deletions(-) create mode 100644 lib/nostr/errors.rb create mode 100644 lib/nostr/errors/error.rb create mode 100644 lib/nostr/errors/invalid_hrp_error.rb create mode 100644 lib/nostr/errors/invalid_key_format_error.rb create mode 100644 lib/nostr/errors/invalid_key_length_error.rb create mode 100644 lib/nostr/errors/invalid_key_type_error.rb create mode 100644 lib/nostr/errors/key_validation_error.rb create mode 100644 lib/nostr/key.rb create mode 100644 lib/nostr/private_key.rb create mode 100644 lib/nostr/public_key.rb create mode 100644 sig/nostr/errors/error.rbs create mode 100644 sig/nostr/errors/invalid_hrb_error.rbs create mode 100644 sig/nostr/errors/invalid_key_format_error.rbs create mode 100644 sig/nostr/errors/invalid_key_length_error.rbs create mode 100644 sig/nostr/errors/invalid_key_type_error.rbs create mode 100644 sig/nostr/errors/key_validation_error.rbs create mode 100644 sig/nostr/key.rbs create mode 100644 sig/nostr/private_key.rbs create mode 100644 sig/nostr/public_key.rbs create mode 100644 sig/vendor/bech32.rbs create mode 100644 sig/vendor/bech32/nostr/entity.rbs create mode 100644 sig/vendor/bech32/nostr/nip19.rbs create mode 100644 sig/vendor/bech32/segwit_addr.rbs create mode 100644 sig/vendor/faye/websocket.rbs create mode 100644 sig/vendor/faye/websocket/api.rbs create mode 100644 sig/vendor/faye/websocket/client.rbs create mode 100644 spec/nostr/errors/invalid_hrp_error_spec.rb create mode 100644 spec/nostr/errors/invalid_key_format_error_spec.rb create mode 100644 spec/nostr/errors/invalid_key_length_error_spec.rb create mode 100644 spec/nostr/errors/invalid_key_type_error_spec.rb create mode 100644 spec/nostr/key_spec.rb create mode 100644 spec/nostr/private_key_spec.rb create mode 100644 spec/nostr/public_key_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 1b5abbc..e4b800b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,10 @@ AllCops: # ----------------------- Style ----------------------- +Style/RaiseArgs: + Exclude: + - 'lib/nostr/key.rb' + Style/StringLiterals: Enabled: true EnforcedStyle: single_quotes @@ -38,3 +42,13 @@ Metrics/ParameterLists: RSpec/ExampleLength: Enabled: false + +RSpec/FilePath: + Exclude: + - spec/nostr/errors/invalid_* + +# ----------------------- Naming ----------------------- + +Naming/MemoizedInstanceVariableName: + Exclude: + - 'spec/nostr/key.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c165b9..bab9784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,17 +4,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.1/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.5.0] 2023-11-20 ### Added - Added relay message type enums `Nostr::RelayMessageType` +- Initial compliance with [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) - bech32-formatted private +keys and public keys +- `Nostr::PrivateKey` and `Nostr::PublicKey` to represent private and public keys, respectively +- Added a validation of private and public keys +- Added an ability to convert keys to and from Bech32 format +- Added RBS types for `faye-websocket` and `bech32` ### Changed - Set the gem's homepage to [`https://nostr-ruby.com/`](https://nostr-ruby.com/) - Updated the filter's documentation to reflect the removal of prefix matching - Updated the subscription's id documentation to reflect the changes in the protocol definition +- Updated `Nostr::PrivateKey` and `Nostr::PublicKey` internally, instead of Strings +- Updated the gem `rbs` to version `3.3` (was `2.8`) +- Updated the gem `steep` to version `1.6` (was `1.4`) + +## Fixed + +- Fixed the RBS type of the constant `Nostr::Crypto::BN_BASE` +- Fixed the return type of `Nostr::Crypto#decrypt_text` when given an invalid ciphertext +- Fixed the RBS type of `Nostr::Filter#to_h`, `Nostr::Filter#e` and `Nostr::Filter#p` +- Fixed the RBS types of `EventEmitter` and `EventMachine::Channel` ## [0.4.0] - 2023-02-25 @@ -61,7 +77,7 @@ principles of immutability and was a major source of internal complexity as I ne - Initial release -[unreleased]: https://github.com/wilsonsilva/nostr/compare/v0.4.0...HEAD +[0.5.0]: https://github.com/wilsonsilva/nostr/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/wilsonsilva/nostr/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/wilsonsilva/nostr/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/wilsonsilva/nostr/compare/v0.1.0...v0.2.0 diff --git a/README.md b/README.md index 70d948d..f9ea29f 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,17 @@ client = Nostr::Client.new # a) Use an existing keypair keypair = Nostr::KeyPair.new( - private_key: 'add-your-private-key-here', - public_key: 'add-your-public-key-here', + private_key: Nostr::PrivateKey.new('add-your-hex-private-key-here'), + public_key: Nostr::PublicKey.new('add-your-hex-public-key-here'), ) -# b) Or create a new keypair +# b) Or build a keypair from a private key +keygen = Nostr::Keygen.new +keypair = keygen.get_key_pair_from_private_key( + Nostr::PrivateKey.new('add-your-hex-private-key-here') +) + +# c) Or create a new keypair keygen = Nostr::Keygen.new keypair = keygen.generate_keypair diff --git a/docs/core/keys.md b/docs/core/keys.md index cd53929..83bb29b 100644 --- a/docs/core/keys.md +++ b/docs/core/keys.md @@ -3,18 +3,22 @@ To [sign events](#signing-an-event), you need a **private key**. To verify signatures, you need a **public key**. The combination of a private and a public key is called a **keypair**. +Both public and private keys are 64-character hexadecimal strings. They can be represented in bech32 format, +which is a human-readable format that starts with `nsec` for private keys and `npub` for public keys. + There are a few ways to generate a keypair. ## a) Generating a keypair -If you don't have any keys, you can generate a keypair using the [`Nostr::Keygen`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen) class: +If you don't have any keys, you can generate a keypair using the +[`Nostr::Keygen`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen) class: ```ruby keygen = Nostr::Keygen.new keypair = keygen.generate_key_pair -keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900' -keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' +keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa' +keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e' ``` ## b) Generating a private key and a public key @@ -28,19 +32,69 @@ keygen = Nostr::Keygen.new private_key = keygen.generate_private_key public_key = keygen.extract_public_key(private_key) -private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900' -public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' +private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa' +public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e' ``` -## c) Using existing keys +## c) Using existing hexadecimal keys -If you already have a private key and a public key, you can create a keypair using the `Nostr::KeyPair` class: +If you already have a private key and a public key in hexadecimal format, you can create a keypair using the +`Nostr::KeyPair` class: ```ruby keypair = Nostr::KeyPair.new( - private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', - public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558', + private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'), + public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'), ) + +keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa' +keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e' +``` + +### d) Use existing bech32 keys + +If you already have a private key and a public key in bech32 format, you can create a keypair using the +`Nostr::KeyPair` class: + +```ruby +keypair = Nostr::KeyPair.new( + private_key: Nostr::PrivateKey.from_bech32('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'), + public_key: Nostr::PublicKey.from_bech32('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'), +) + +keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa' +keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e' +``` + +## e) Using an existing hexadecimal private key + +If you already have a private key in hexadecimal format, you can create a keypair using the method +[`Nostr::Keygen#get_key_pair_from_private_key`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen#get_key_pair_from_private_key-instance_method): + +```ruby +private_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa') + +keygen= Nostr::Keygen.new +keypair = keygen.get_key_pair_from_private_key(private_key) + +keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa' +keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e' +``` + +## f) Using an existing bech32 private key + +If you already have a private key in bech32 format, you can create a keypair using the methods +[`Nostr::PrivateKey.from_bech32`](https://www.rubydoc.info/gems/nostr/Nostr/PrivateKey.from_bech32-class_method) and +[`Nostr::Keygen#get_key_pair_from_private_key`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen#get_key_pair_from_private_key-instance_method): + +```ruby +private_key = Nostr::PrivateKey.from_bech32('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5') + +keygen= Nostr::Keygen.new +keypair = keygen.get_key_pair_from_private_key(private_key) + +keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa' +keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e' ``` ## Signing an event @@ -48,13 +102,12 @@ keypair = Nostr::KeyPair.new( KeyPairs are used to sign [events](../events). To create a signed event, you need to instantiate a [`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) with a keypair: -```ruby{9,12-15} -# a) Use an existing keypair -keypair = Nostr::KeyPair.new(private_key: 'your-key', public_key: 'your-key') - -# b) Or generate a new keypair -keygen = Nostr::Keygen.new -keypair = keygen.generate_key_pair +```ruby{8,11-14} +# Use an existing keypair +keypair = Nostr::KeyPair.new( + private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'), + public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'), +) # Add the keypair to a user user = Nostr::User.new(keypair: keypair) @@ -71,13 +124,13 @@ text_note = user.create_event( ```ruby # text_note.to_h { - id: '5feb10973dbcf5f210cfc1f0aa338fee62bed6a29696a67957713599b9baf0eb', - pubkey: 'b9b9821074d1b60b8fb4a3983632af3ef9669f55b20d515bf982cda5c439ad61', # from keypair - created_at: 1699847447, + id: '030fbc71151379e5b58e7428ed6e7f2884e5dfc9087fd64d1dc4cc677f5097c8', + pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e', # from the keypair + created_at: 1700119819, kind: 1, # Nostr::EventKind::TEXT_NOTE, tags: [], content: 'Your feedback is appreciated, now pay $8', - sig: 'e30f2f08331f224e41a4099d16aefc780bf9f2d1191b71777e1e1789e6b51fdf7bb956f25d4ea9a152d1c66717a9d68c081ce6c89c298c3c5e794914013381ab' + sig: '586877896ef6f7d54fa4dd2ade04e3fdc4dfcd6166dd0df696b3c3c768868c0b690338f5baed6ab4fc717785333cb487363384de9fb0f740ac4775522cb4acb3' # signed with the private key from the keypair } ``` ::: diff --git a/docs/getting-started/overview.md b/docs/getting-started/overview.md index 801fb4c..ab9e2f3 100644 --- a/docs/getting-started/overview.md +++ b/docs/getting-started/overview.md @@ -98,11 +98,17 @@ client = Nostr::Client.new # a) Use an existing keypair keypair = Nostr::KeyPair.new( - private_key: 'your-private-key', - public_key: 'your-public-key', + private_key: Nostr::PrivateKey.new('your-hex-private-key'), + public_key: Nostr::PublicKey.new('your-hex-public-key'), ) -# b) Or create a new keypair +# b) Or build a keypair from a private key +keygen = Nostr::Keygen.new +keypair = keygen.get_key_pair_from_private_key( + Nostr::PrivateKey.new('your-hex-private-key') +) + +# c) Or create a new keypair keygen = Nostr::Keygen.new keypair = keygen.generate_keypair diff --git a/lib/nostr.rb b/lib/nostr.rb index 55f91bb..6dfe5ae 100644 --- a/lib/nostr.rb +++ b/lib/nostr.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative 'nostr/errors' require_relative 'nostr/crypto' require_relative 'nostr/version' require_relative 'nostr/keygen' @@ -14,6 +15,9 @@ require_relative 'nostr/event' require_relative 'nostr/events/encrypted_direct_message' require_relative 'nostr/client' require_relative 'nostr/user' +require_relative 'nostr/key' +require_relative 'nostr/private_key' +require_relative 'nostr/public_key' # Encapsulates all the gem's logic module Nostr diff --git a/lib/nostr/crypto.rb b/lib/nostr/crypto.rb index 1fc62d1..afd20ca 100644 --- a/lib/nostr/crypto.rb +++ b/lib/nostr/crypto.rb @@ -30,8 +30,8 @@ module Nostr # encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8') # encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg==" # - # @param sender_private_key [String] 32-bytes hex-encoded private key of the creator. - # @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient. + # @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the creator. + # @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient. # @param plain_text [String] The text to be encrypted # # @return [String] Encrypted text. @@ -54,8 +54,8 @@ module Nostr # encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg==" # decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted) # - # @param sender_public_key [String] 32-bytes hex-encoded public key of the message creator. - # @param recipient_private_key [String] 32-bytes hex-encoded public key of the recipient. + # @param sender_public_key [PublicKey] 32-bytes hex-encoded public key of the message creator. + # @param recipient_private_key [PrivateKey] 32-bytes hex-encoded public key of the recipient. # @param encrypted_text [String] The text to be decrypted # # @return [String] Decrypted text. @@ -84,7 +84,7 @@ module Nostr # event.sig # => a signature # # @param event [Event] The event to be signed - # @param private_key [String] 32-bytes hex-encoded private key. + # @param private_key [PrivateKey] 32-bytes hex-encoded private key. # # @return [Event] An unsigned event. # @@ -107,8 +107,8 @@ module Nostr # # @api private # - # @param private_key [String] 32-bytes hex-encoded private key. - # @param public_key [String] 32-bytes hex-encoded public key. + # @param private_key [PrivateKey] 32-bytes hex-encoded private key. + # @param public_key [PublicKey] 32-bytes hex-encoded public key. # # @return [String] A shared key used in the event's content encryption and decryption. # diff --git a/lib/nostr/errors.rb b/lib/nostr/errors.rb new file mode 100644 index 0000000..32f48ad --- /dev/null +++ b/lib/nostr/errors.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require_relative 'errors/error' +require_relative 'errors/key_validation_error' +require_relative 'errors/invalid_hrp_error' +require_relative 'errors/invalid_key_type_error' +require_relative 'errors/invalid_key_length_error' +require_relative 'errors/invalid_key_format_error' diff --git a/lib/nostr/errors/error.rb b/lib/nostr/errors/error.rb new file mode 100644 index 0000000..3509051 --- /dev/null +++ b/lib/nostr/errors/error.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Nostr + # Base error class + class Error < StandardError + end +end diff --git a/lib/nostr/errors/invalid_hrp_error.rb b/lib/nostr/errors/invalid_hrp_error.rb new file mode 100644 index 0000000..8594232 --- /dev/null +++ b/lib/nostr/errors/invalid_hrp_error.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Nostr + # Raised when the human readable part of a Bech32 string is invalid + # + # @api public + # + class InvalidHRPError < KeyValidationError + # Initializes the error + # + # @example + # InvalidHRPError.new('example wrong hrp', 'nsec') + # + # @param given_hrp [String] The given human readable part of the Bech32 string + # @param allowed_hrp [String] The allowed human readable part of the Bech32 string + # + def initialize(given_hrp, allowed_hrp) + super("Invalid hrp: #{given_hrp}. The allowed hrp value for this kind of entity is '#{allowed_hrp}'.") + end + end +end diff --git a/lib/nostr/errors/invalid_key_format_error.rb b/lib/nostr/errors/invalid_key_format_error.rb new file mode 100644 index 0000000..ab2d541 --- /dev/null +++ b/lib/nostr/errors/invalid_key_format_error.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Nostr + # Raised when the private key is in an invalid format + # + # @api public + # + class InvalidKeyFormatError < KeyValidationError + # Initializes the error + # + # @example + # InvalidKeyFormatError.new('private'') + # + # @param [String] key_kind The kind of key that is invalid (public or private) + # + def initialize(key_kind) + super("Only lowercase hexadecimal characters are allowed in #{key_kind} keys.") + end + end +end diff --git a/lib/nostr/errors/invalid_key_length_error.rb b/lib/nostr/errors/invalid_key_length_error.rb new file mode 100644 index 0000000..5508896 --- /dev/null +++ b/lib/nostr/errors/invalid_key_length_error.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Nostr + # Raised when the private key's length is not 64 characters + # + # @api public + # + class InvalidKeyLengthError < KeyValidationError + # Initializes the error + # + # @example + # InvalidKeyLengthError.new('private'') + # + # @param [String] key_kind The kind of key that is invalid (public or private) + # + def initialize(key_kind) + super("Invalid #{key_kind} key length. It should have 64 characters.") + end + end +end diff --git a/lib/nostr/errors/invalid_key_type_error.rb b/lib/nostr/errors/invalid_key_type_error.rb new file mode 100644 index 0000000..ce1fe7e --- /dev/null +++ b/lib/nostr/errors/invalid_key_type_error.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Nostr + # Raised when the private key is not a string + # + # @api public + # + class InvalidKeyTypeError < KeyValidationError + # Initializes the error + # + # @example + # InvalidKeyTypeError.new('private'') + # + # @param [String] key_kind The kind of key that is invalid (public or private) + # + def initialize(key_kind) = super("Invalid #{key_kind} key type") + end +end diff --git a/lib/nostr/errors/key_validation_error.rb b/lib/nostr/errors/key_validation_error.rb new file mode 100644 index 0000000..6628684 --- /dev/null +++ b/lib/nostr/errors/key_validation_error.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Nostr + # Base class for all key validation errors + class KeyValidationError < Error; end +end diff --git a/lib/nostr/event.rb b/lib/nostr/event.rb index a08f0dd..4e148d2 100644 --- a/lib/nostr/event.rb +++ b/lib/nostr/event.rb @@ -159,11 +159,11 @@ module Nostr # pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e' # event.add_pubkey_reference(pubkey) # - # @param pubkey [String] 32-bytes hex-encoded public key. + # @param pubkey [PublicKey] 32-bytes hex-encoded public key. # # @return [Array] The event's updated list of tags # - def add_pubkey_reference(pubkey) = tags.push(['p', pubkey]) + def add_pubkey_reference(pubkey) = tags.push(['p', pubkey.to_s]) # Signs an event with the user's private key # @@ -172,7 +172,7 @@ module Nostr # @example Signing an event # event.sign(private_key) # - # @param private_key [String] 32-bytes hex-encoded private key. + # @param private_key [PrivateKey] 32-bytes hex-encoded private key. # # @return [Event] A signed event. # diff --git a/lib/nostr/events/encrypted_direct_message.rb b/lib/nostr/events/encrypted_direct_message.rb index 247b069..ba8d628 100644 --- a/lib/nostr/events/encrypted_direct_message.rb +++ b/lib/nostr/events/encrypted_direct_message.rb @@ -26,8 +26,9 @@ module Nostr # ) # # @param plain_text [String] The +content+ of the encrypted message. - # @param sender_private_key [String] 32-bytes hex-encoded private key of the message's author. - # @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient of the encrypted message. + # @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the message's author. + # @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient of the encrypted + # message. # @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a # conversation or a message we are explicitly replying to (such that contextual, more organized conversations # may happen diff --git a/lib/nostr/key.rb b/lib/nostr/key.rb new file mode 100644 index 0000000..dc875d0 --- /dev/null +++ b/lib/nostr/key.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'bech32' + +module Nostr + # Abstract class for all keys + # + # @api private + # + class Key < String + # The regular expression for hexadecimal lowercase characters + # + # @return [Regexp] The regular expression for hexadecimal lowercase characters + # + FORMAT = /^[a-f0-9]+$/ + + # The length of the key in hex format + # + # @return [Integer] The length of the key in hex format + # + LENGTH = 64 + + # Instantiates a new key. Can't be used directly because this is an abstract class. Raises a +ValidationError+ + # + # @see Nostr::PrivateKey + # @see Nostr::PublicKey + # + # @param [String] hex_value Hex-encoded value of the key + # + # @raise [ValidationError] + # + def initialize(hex_value) + validate_hex_value(hex_value) + + super(hex_value) + end + + # Instantiates a key from a bech32 string + # + # @api public + # + # @example + # bech32_key = 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5' + # bech32_key.to_key # => # + # + # @raise [Nostr::InvalidHRPError] if the bech32 string is invalid. + # + # @param [String] bech32_value The bech32 string representation of the key. + # + # @return [Key] the key. + # + def self.from_bech32(bech32_value) + entity = Bech32::Nostr::NIP19.decode(bech32_value) + + raise InvalidHRPError.new(entity.hrp, hrp) unless entity.hrp == hrp + + new(entity.data) + end + + # Abstract method to be implemented by subclasses to provide the HRP (npub, nsec) + # + # @api private + # + # @return [String] The HRP + # + def self.hrp + raise 'Subclasses must implement this method' + end + + # Converts the key to a bech32 string representation + # + # @api public + # + # @example Converting a private key to a bech32 string + # public_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa') + # public_key.to_bech32 # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5' + # + # @example Converting a public key to a bech32 string + # public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e') + # public_key.to_bech32 # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg' + # + # @return [String] The bech32 string representation of the key + # + def to_bech32 = Bech32::Nostr::BareEntity.new(self.class.hrp, self).encode + + protected + + # Validates the hex value during initialization + # + # @api private + # + # @param [String] _hex_value The hex value of the key + # + # @raise [KeyValidationError] When the hex value is invalid + # + # @return [void] + # + def validate_hex_value(_hex_value) + raise 'Subclasses must implement this method' + end + end +end diff --git a/lib/nostr/key_pair.rb b/lib/nostr/key_pair.rb index 169ea48..766a05d 100644 --- a/lib/nostr/key_pair.rb +++ b/lib/nostr/key_pair.rb @@ -10,7 +10,7 @@ module Nostr # @example # keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900' # - # @return [String] + # @return [PrivateKey] # attr_reader :private_key @@ -21,7 +21,7 @@ module Nostr # @example # keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' # - # @return [String] + # @return [PublicKey] # attr_reader :public_key @@ -31,16 +31,40 @@ module Nostr # # @example # keypair = Nostr::KeyPair.new( - # private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', - # public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558', + # private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'), + # public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'), # ) # - # @param private_key [String] 32-bytes hex-encoded private key. - # @param public_key [String] 32-bytes hex-encoded public key. + # @param private_key [PrivateKey] 32-bytes hex-encoded private key. + # @param public_key [PublicKey] 32-bytes hex-encoded public key. + # + # @raise ArgumentError when the private key is not a +PrivateKey+ + # @raise ArgumentError when the public key is not a +PublicKey+ # def initialize(private_key:, public_key:) + validate_keys(private_key, public_key) + @private_key = private_key @public_key = public_key end + + private + + # Validates the keys + # + # @api private + # + # @param private_key [PrivateKey] 32-bytes hex-encoded private key. + # @param public_key [PublicKey] 32-bytes hex-encoded public key. + # + # @raise ArgumentError when the private key is not a +PrivateKey+ + # @raise ArgumentError when the public key is not a +PublicKey+ + # + # @return [void] + # + def validate_keys(private_key, public_key) + raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey) + raise ArgumentError, 'public_key is not an instance of PublicKey' unless public_key.is_a?(Nostr::PublicKey) + end end end diff --git a/lib/nostr/keygen.rb b/lib/nostr/keygen.rb index da09800..d8bd226 100644 --- a/lib/nostr/keygen.rb +++ b/lib/nostr/keygen.rb @@ -44,10 +44,11 @@ module Nostr # private_key = keygen.generate_private_key # private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900' # - # @return [String] A 32-bytes hex-encoded private key. + # @return [PrivateKey] A 32-bytes hex-encoded private key. # def generate_private_key - (SecureRandom.random_number(group.order - 1) + 1).to_s(16) + hex_value = (SecureRandom.random_number(group.order - 1) + 1).to_s(16) + PrivateKey.new(hex_value) end # Extracts a public key from a private key @@ -59,10 +60,36 @@ module Nostr # public_key = keygen.extract_public_key(private_key) # public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' # - # @return [String] A 32-bytes hex-encoded public key. + # @param [PrivateKey] private_key A 32-bytes hex-encoded private key. + # + # @raise [ArgumentError] if the private key is not an instance of +PrivateKey+ + # + # @return [PublicKey] A 32-bytes hex-encoded public key. # def extract_public_key(private_key) - group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0') + validate_private_key(private_key) + hex_value = group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0') + PublicKey.new(hex_value) + end + + # Builds a key pair from an existing private key + # + # @api public + # + # @example + # private_key = Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900') + # keygen.get_key_pair_from_private_key(private_key) + # + # @param private_key [PrivateKey] 32-bytes hex-encoded private key. + # + # @raise [ArgumentError] if the private key is not an instance of +PrivateKey+ + # + # @return [Nostr::KeyPair] + # + def get_key_pair_from_private_key(private_key) + validate_private_key(private_key) + public_key = extract_public_key(private_key) + KeyPair.new(private_key:, public_key:) end private @@ -74,5 +101,17 @@ module Nostr # @return [ECDSA::Group] # attr_reader :group + + # Validates that the private key is an instance of +PrivateKey+ + # + # @api private + # + # @raise [ArgumentError] if the private key is not an instance of +PrivateKey+ + # + # @return [void] + # + def validate_private_key(private_key) + raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey) + end end end diff --git a/lib/nostr/private_key.rb b/lib/nostr/private_key.rb new file mode 100644 index 0000000..b11298f --- /dev/null +++ b/lib/nostr/private_key.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Nostr + # 32-bytes lowercase hex-encoded private key + class PrivateKey < Key + # Human-readable part of the Bech32 encoded address + # + # @api private + # + # @return [String] The human-readable part of the Bech32 encoded address + # + def self.hrp + 'nsec' + end + + private + + # Validates the hex value of the private key + # + # @api private + # + # @param [String] hex_value The private key in hex format + # + # @raise InvalidKeyTypeError when the private key is not a string + # @raise InvalidKeyLengthError when the private key's length is not 64 characters + # @raise InvalidKeyFormatError when the private key is in an invalid format + # + # @return [void] + # + def validate_hex_value(hex_value) + raise InvalidKeyTypeError, 'private' unless hex_value.is_a?(String) + raise InvalidKeyLengthError, 'private' unless hex_value.size == Key::LENGTH + raise InvalidKeyFormatError, 'private' unless hex_value.match(Key::FORMAT) + end + end +end diff --git a/lib/nostr/public_key.rb b/lib/nostr/public_key.rb new file mode 100644 index 0000000..92aacff --- /dev/null +++ b/lib/nostr/public_key.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Nostr + # 32-bytes lowercase hex-encoded public key + class PublicKey < Key + # Human-readable part of the Bech32 encoded address + # + # @api private + # + # @return [String] The human-readable part of the Bech32 encoded address + # + def self.hrp + 'npub' + end + + private + + # Validates the hex value of the public key + # + # @api private + # + # @param [String] hex_value The public key in hex format + # + # @raise InvalidKeyTypeError when the public key is not a string + # @raise InvalidKeyLengthError when the public key's length is not 64 characters + # @raise InvalidKeyFormatError when the public key is in an invalid format + # + # @return [void] + # + def validate_hex_value(hex_value) + raise InvalidKeyTypeError, 'public' unless hex_value.is_a?(String) + raise InvalidKeyLengthError, 'public' unless hex_value.size == Key::LENGTH + raise InvalidKeyFormatError, 'public' unless hex_value.match(Key::FORMAT) + end + end +end diff --git a/sig/nostr/client.rbs b/sig/nostr/client.rbs index 1b3e9c4..fdf1bb2 100644 --- a/sig/nostr/client.rbs +++ b/sig/nostr/client.rbs @@ -5,16 +5,16 @@ module Nostr def initialize: -> void def connect: (Relay relay) -> Thread def subscribe: (?subscription_id: String, ?filter: Filter) -> Subscription - def unsubscribe: (String subscription_id) -> untyped + def unsubscribe: (String subscription_id) -> void def publish: (Event event) -> untyped private attr_reader subscriptions: Hash[String, Subscription] - attr_reader parent_to_child_channel: untyped - attr_reader child_to_parent_channel: untyped + attr_reader parent_to_child_channel: EventMachine::Channel + attr_reader child_to_parent_channel: EventMachine::Channel - def execute_within_an_em_thread: { -> untyped } -> Thread - def initialize_channels: -> untyped + def execute_within_an_em_thread: { -> void } -> Thread + def initialize_channels: -> void end end diff --git a/sig/nostr/crypto.rbs b/sig/nostr/crypto.rbs index 9924f1c..f582040 100644 --- a/sig/nostr/crypto.rbs +++ b/sig/nostr/crypto.rbs @@ -4,13 +4,13 @@ module Nostr CIPHER_CURVE: String CIPHER_ALGORITHM: String - def encrypt_text: (String, String, String) -> String - def decrypt_text: (String, String, String) -> String - def sign_event: (Event, String) -> Event + def encrypt_text: (PrivateKey, PublicKey, String) -> String + def decrypt_text: (PrivateKey, PublicKey, String) -> String + def sign_event: (Event, PrivateKey) -> Event private - def compute_shared_key: (String, String) -> String + def compute_shared_key: (PrivateKey, PublicKey) -> String def hash_event:(Event) -> String end end diff --git a/sig/nostr/errors/error.rbs b/sig/nostr/errors/error.rbs new file mode 100644 index 0000000..f17b12e --- /dev/null +++ b/sig/nostr/errors/error.rbs @@ -0,0 +1,4 @@ +module Nostr + class Error < StandardError + end +end diff --git a/sig/nostr/errors/invalid_hrb_error.rbs b/sig/nostr/errors/invalid_hrb_error.rbs new file mode 100644 index 0000000..fed2738 --- /dev/null +++ b/sig/nostr/errors/invalid_hrb_error.rbs @@ -0,0 +1,6 @@ +module Nostr + class InvalidHRPError < KeyValidationError + def initialize: (String, String) -> void + end +end + diff --git a/sig/nostr/errors/invalid_key_format_error.rbs b/sig/nostr/errors/invalid_key_format_error.rbs new file mode 100644 index 0000000..77da2bf --- /dev/null +++ b/sig/nostr/errors/invalid_key_format_error.rbs @@ -0,0 +1,5 @@ +module Nostr + class InvalidKeyFormatError < KeyValidationError + def initialize: (String) -> void + end +end diff --git a/sig/nostr/errors/invalid_key_length_error.rbs b/sig/nostr/errors/invalid_key_length_error.rbs new file mode 100644 index 0000000..d7572fe --- /dev/null +++ b/sig/nostr/errors/invalid_key_length_error.rbs @@ -0,0 +1,5 @@ +module Nostr + class InvalidKeyLengthError < KeyValidationError + def initialize: (String) -> void + end +end diff --git a/sig/nostr/errors/invalid_key_type_error.rbs b/sig/nostr/errors/invalid_key_type_error.rbs new file mode 100644 index 0000000..1056354 --- /dev/null +++ b/sig/nostr/errors/invalid_key_type_error.rbs @@ -0,0 +1,5 @@ +module Nostr + class InvalidKeyTypeError < KeyValidationError + def initialize: (String) -> void + end +end diff --git a/sig/nostr/errors/key_validation_error.rbs b/sig/nostr/errors/key_validation_error.rbs new file mode 100644 index 0000000..4dc9200 --- /dev/null +++ b/sig/nostr/errors/key_validation_error.rbs @@ -0,0 +1,4 @@ +module Nostr + class KeyValidationError < Error + end +end diff --git a/sig/nostr/event.rbs b/sig/nostr/event.rbs index 39e7348..fe4798b 100644 --- a/sig/nostr/event.rbs +++ b/sig/nostr/event.rbs @@ -1,6 +1,6 @@ module Nostr class Event - attr_reader pubkey: String + attr_reader pubkey: PublicKey attr_reader created_at: Integer attr_reader kind: Integer attr_reader tags: Array[Array[String]] @@ -9,7 +9,7 @@ module Nostr attr_accessor sig: String?|nil def initialize: ( - pubkey: String, + pubkey: PublicKey, kind: Integer, content: String, ?created_at: Integer, @@ -31,9 +31,9 @@ module Nostr } def ==: (Event other) -> bool - def sign:(String) -> Event + def sign:(PrivateKey) -> Event def add_event_reference: (String) -> Array[Array[String]] - def add_pubkey_reference: (String) -> Array[Array[String]] + def add_pubkey_reference: (PublicKey) -> Array[Array[String]] end end diff --git a/sig/nostr/events/encrypted_direct_message.rbs b/sig/nostr/events/encrypted_direct_message.rbs index 051bbaa..ee354d4 100644 --- a/sig/nostr/events/encrypted_direct_message.rbs +++ b/sig/nostr/events/encrypted_direct_message.rbs @@ -3,8 +3,8 @@ module Nostr class EncryptedDirectMessage < Event def initialize: ( plain_text: String, - sender_private_key: String, - recipient_public_key: String, + sender_private_key: PrivateKey, + recipient_public_key: PublicKey, ?previous_direct_message: String|nil ) -> void end diff --git a/sig/nostr/key.rbs b/sig/nostr/key.rbs new file mode 100644 index 0000000..f8c6214 --- /dev/null +++ b/sig/nostr/key.rbs @@ -0,0 +1,16 @@ +module Nostr + class Key < String + FORMAT: Regexp + LENGTH: int + + def self.from_bech32: (String) -> Key + def self.hrp: -> String + + def initialize: (String) -> void + def to_bech32: -> String + + private + + def validate_hex_value: (String) -> nil + end +end diff --git a/sig/nostr/key_pair.rbs b/sig/nostr/key_pair.rbs index 5ad86cf..cd9583e 100644 --- a/sig/nostr/key_pair.rbs +++ b/sig/nostr/key_pair.rbs @@ -1,9 +1,13 @@ # Classes module Nostr class KeyPair - attr_reader private_key: String - attr_reader public_key: String + attr_reader private_key: PrivateKey + attr_reader public_key: PublicKey - def initialize: (private_key: String, public_key: String) -> void + def initialize: (private_key: PrivateKey, public_key: PublicKey) -> void + + private + + def validate_keys: (PrivateKey, PublicKey) -> void end end diff --git a/sig/nostr/keygen.rbs b/sig/nostr/keygen.rbs index 237567c..80bcd61 100644 --- a/sig/nostr/keygen.rbs +++ b/sig/nostr/keygen.rbs @@ -3,11 +3,14 @@ module Nostr class Keygen def initialize: -> void def generate_key_pair: -> KeyPair - def generate_private_key: -> String - def extract_public_key: (String private_key) -> String + def generate_private_key: -> PrivateKey + def extract_public_key: (PrivateKey private_key) -> PublicKey + def get_key_pair_from_private_key: (PrivateKey private_key) -> KeyPair private attr_reader group: untyped + + def validate_private_key: (PrivateKey private_key) -> void end end diff --git a/sig/nostr/private_key.rbs b/sig/nostr/private_key.rbs new file mode 100644 index 0000000..ea3997f --- /dev/null +++ b/sig/nostr/private_key.rbs @@ -0,0 +1,4 @@ +module Nostr + class PrivateKey < Key + end +end diff --git a/sig/nostr/public_key.rbs b/sig/nostr/public_key.rbs new file mode 100644 index 0000000..3803ae0 --- /dev/null +++ b/sig/nostr/public_key.rbs @@ -0,0 +1,4 @@ +module Nostr + class PublicKey < Key + end +end diff --git a/sig/vendor/bech32.rbs b/sig/vendor/bech32.rbs new file mode 100644 index 0000000..6976198 --- /dev/null +++ b/sig/vendor/bech32.rbs @@ -0,0 +1,25 @@ +# Added only to satisfy the Steep requirements. Not 100% reliable. +module Bech32 + SEPARATOR: String + BECH32M_CONST: Integer + + def encode: (untyped hrp, untyped data, untyped spec) -> untyped + def self.encode: (untyped hrp, untyped data, untyped spec) -> untyped + def decode: (untyped bech, ?Integer max_length) -> [untyped, untyped, Integer]? + def self.decode: (untyped bech, ?Integer max_length) -> [untyped, untyped, Integer]? + def create_checksum: (untyped hrp, untyped data, untyped spec) -> Array[Integer] + def self.create_checksum: (untyped hrp, untyped data, untyped spec) -> Array[Integer] + def verify_checksum: (untyped hrp, untyped data) -> Integer? + def self.verify_checksum: (untyped hrp, untyped data) -> Integer? + def expand_hrp: (untyped hrp) -> untyped + def self.expand_hrp: (untyped hrp) -> untyped + def convert_bits: (untyped data, untyped from, untyped to, ?true padding) -> Array[Integer]? + def self.convert_bits: (untyped data, untyped from, untyped to, ?true padding) -> Array[Integer]? + def polymod: (untyped values) -> Integer + def self.polymod: (untyped values) -> Integer + + module Encoding + BECH32: Integer + BECH32M: Integer + end +end diff --git a/sig/vendor/bech32/nostr/entity.rbs b/sig/vendor/bech32/nostr/entity.rbs new file mode 100644 index 0000000..bd3ca0b --- /dev/null +++ b/sig/vendor/bech32/nostr/entity.rbs @@ -0,0 +1,41 @@ +# Added only to satisfy the Steep requirements. Not 100% reliable. +module Bech32 + module Nostr + class BareEntity + attr_reader hrp: untyped + attr_reader data: untyped + + def initialize: (untyped hrp, untyped data) -> void + def encode: -> untyped + end + + class TLVEntry + attr_reader type: (Float | Integer | String)? + attr_reader label: String? + attr_reader value: (Float | Integer | String)? + + def initialize: ((Float | Integer | String)? `type`, (Float | Integer | String)? value, ?String? label) -> void + def to_payload: -> String + def to_s: -> String + + private + + def hex_string?: ((Float | Integer | String)? str) -> bool + end + + class TLVEntity + TYPE_SPECIAL: Integer + TYPE_RELAY: Integer + TYPE_AUTHOR: Integer + TYPE_KIND: Integer + TYPES: [Integer, Integer, Integer, Integer] + + attr_reader hrp: untyped + attr_reader entries: Array[TLVEntry] + + def initialize: (untyped hrp, Array[TLVEntry] entries) -> void + def self.parse: (untyped hrp, untyped data) -> TLVEntity + def encode: -> untyped + end + end +end diff --git a/sig/vendor/bech32/nostr/nip19.rbs b/sig/vendor/bech32/nostr/nip19.rbs new file mode 100644 index 0000000..8a95886 --- /dev/null +++ b/sig/vendor/bech32/nostr/nip19.rbs @@ -0,0 +1,20 @@ +# Added only to satisfy the Steep requirements. Not 100% reliable. +module Bech32 + module Nostr + module NIP19 + HRP_PUBKEY: String + HRP_PRIVATE_KEY: String + HRP_NOTE_ID: String + HRP_PROFILE: String + HRP_EVENT: String + HRP_RELAY: String + HRP_EVENT_COORDINATE: String + BARE_PREFIXES: [String, String, String] + TLV_PREFIXES: [String, String, String, String] + ALL_PREFIXES: Array[String] + + def decode: (untyped string) -> untyped + def self.decode: (untyped string) -> untyped + end + end +end diff --git a/sig/vendor/bech32/segwit_addr.rbs b/sig/vendor/bech32/segwit_addr.rbs new file mode 100644 index 0000000..023a665 --- /dev/null +++ b/sig/vendor/bech32/segwit_addr.rbs @@ -0,0 +1,21 @@ +# Added only to satisfy the Steep requirements. Not 100% reliable. +module Bech32 + class SegwitAddr + HRP_MAINNET: String + HRP_TESTNET: String + HRP_REGTEST: String + + attr_accessor hrp: String + attr_accessor ver: (Float | Integer | String)? + attr_accessor prog: Array[(Float | Integer | String)?] + + def initialize: (?nil addr) -> void + def to_script_pubkey: -> ((Float | Integer | String)?) + def script_pubkey=: (untyped script_pubkey) -> (Array[(Float | Integer | String)?]) + def addr: -> untyped + + private + + def parse_addr: (untyped addr) -> nil + end +end diff --git a/sig/vendor/event_emitter.rbs b/sig/vendor/event_emitter.rbs index a608a33..a7ca4b9 100644 --- a/sig/vendor/event_emitter.rbs +++ b/sig/vendor/event_emitter.rbs @@ -1,9 +1,16 @@ # Added only to satisfy the Steep requirements. Not 100% reliable. module EventEmitter - def add_listener: (untyped `type`, ?{once: true} params) -> Integer + interface _Event + def data: -> String + def message: -> String + def code: -> Integer + def reason: -> String + end + + def add_listener: (Symbol event_name) { (_Event event) -> void } -> void alias on add_listener def remove_listener: (untyped id_or_type) -> Array[untyped]? - def emit: (untyped `type`, *untyped data) -> Array[untyped] - def once: (untyped `type`) -> Integer + def emit: (Symbol `type`, *untyped data) -> Array[untyped] + def once: (Symbol `type`) -> Integer end diff --git a/sig/vendor/event_machine/channel.rbs b/sig/vendor/event_machine/channel.rbs index 0265325..84e535f 100644 --- a/sig/vendor/event_machine/channel.rbs +++ b/sig/vendor/event_machine/channel.rbs @@ -6,7 +6,7 @@ module EventMachine def initialize: -> void def num_subscribers: -> Integer - def subscribe: (*untyped a) ?{ -> untyped } -> Integer + def subscribe: (*untyped a) ?{ (untyped) -> untyped } -> Integer def unsubscribe: (untyped name) -> untyped def push: (*untyped items) -> untyped alias << push diff --git a/sig/vendor/faye/websocket.rbs b/sig/vendor/faye/websocket.rbs new file mode 100644 index 0000000..e067283 --- /dev/null +++ b/sig/vendor/faye/websocket.rbs @@ -0,0 +1,30 @@ +# Added only to satisfy the Steep requirements. Not 100% reliable. +module Faye + class WebSocket + ADAPTERS: Hash[String, :Goliath | :Rainbows | :Thin] + + @url: String + @driver_started: false + @stream: Stream + @driver: bot + + def self.determine_url: (untyped env, ?[String, String] schemes) -> String + def self.ensure_reactor_running: -> nil + def self.load_adapter: (untyped backend) -> bool? + def self.secure_request?: (untyped env) -> bool + def self.websocket?: (untyped env) -> untyped + + attr_reader env: untyped + + def initialize: (untyped env, ?nil protocols, ?Hash[untyped, untyped] options) -> void + def start_driver: -> nil + def rack_response: -> [Integer, Hash[untyped, untyped], Array[untyped]] + + class Stream + @socket_object: bot + + def fail: -> untyped + def receive: (untyped data) -> untyped + end + end +end diff --git a/sig/vendor/faye/websocket/api.rbs b/sig/vendor/faye/websocket/api.rbs new file mode 100644 index 0000000..a8dfc42 --- /dev/null +++ b/sig/vendor/faye/websocket/api.rbs @@ -0,0 +1,45 @@ +# Added only to satisfy the Steep requirements. Not 100% reliable. +module Faye + class WebSocket + module API + CONNECTING: Integer + OPEN: Integer + CLOSING: Integer + CLOSED: Integer + CLOSE_TIMEOUT: Integer + + @driver: untyped + @ping: nil + @ping_id: Integer + @stream: nil + @proxy: nil + @ping_timer: nil + @close_timer: nil + @close_params: [String, Integer]? + @onerror: nil + @onclose: nil + @onmessage: nil + @onopen: nil + + attr_reader url: untyped + attr_reader ready_state: Integer + attr_reader buffered_amount: Integer + + def initialize: (?Hash[untyped, untyped] options) -> void + def write: (untyped data) -> untyped + def send: (untyped message) -> false + def ping: (?String message) -> false + def close: (?nil code, ?nil reason) -> untyped + def protocol: -> String + + private + + def open: -> nil + def receive_message: (untyped data) -> nil + def emit_error: (untyped message) -> nil + def begin_close: (String reason, Integer code, ?Hash[untyped, untyped] options) -> nil + def finalize_close: -> nil + def parse: (untyped data) -> untyped + end + end +end diff --git a/sig/vendor/faye/websocket/client.rbs b/sig/vendor/faye/websocket/client.rbs new file mode 100644 index 0000000..e2c03f1 --- /dev/null +++ b/sig/vendor/faye/websocket/client.rbs @@ -0,0 +1,43 @@ +# Added only to satisfy the Steep requirements. Not 100% reliable. +module Faye + class WebSocket + class Client + DEFAULT_PORTS: Hash[String, Integer] + SECURE_PROTOCOLS: [String, String] + + include EventEmitter + include API + + @url: untyped + @endpoint: untyped + @origin_tls: Hash[untyped, untyped] + @socket_tls: Hash[untyped, untyped] + @driver: bot + @proxy: nil + @ssl_verifier: untyped + @stream: untyped + + def initialize: (untyped url, ?Array[String] protocols, ?Hash[untyped, untyped] options) -> void + + private + + def configure_proxy: (Hash[untyped, untyped] proxy) -> nil + def start_tls: (untyped uri, Hash[untyped, untyped] options) -> nil + def on_connect: (untyped stream) -> untyped + def on_network_error: (nil error) -> untyped + def ssl_verify_peer: (untyped cert) -> untyped + def ssl_handshake_completed: -> untyped + + module Connection + attr_accessor parent: bot + + def connection_completed: -> untyped + def ssl_verify_peer: (untyped cert) -> untyped + def ssl_handshake_completed: -> untyped + def receive_data: (untyped data) -> untyped + def unbind: (?nil error) -> untyped + def write: (untyped data) -> nil + end + end + end +end diff --git a/spec/nostr/crypto_spec.rb b/spec/nostr/crypto_spec.rb index de106ab..c51887e 100644 --- a/spec/nostr/crypto_spec.rb +++ b/spec/nostr/crypto_spec.rb @@ -8,8 +8,8 @@ RSpec.describe Nostr::Crypto do describe '#sign_event' do let(:keypair) do Nostr::KeyPair.new( - public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', - private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' + public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'), + private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757') ) end @@ -34,15 +34,15 @@ RSpec.describe Nostr::Crypto do describe '#encrypt_text' do let(:sender_keypair) do Nostr::KeyPair.new( - public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', - private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' + public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'), + private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757') ) end let(:recipient_keypair) do Nostr::KeyPair.new( - public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0', - private_key: '22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf' + public_key: Nostr::PublicKey.new('6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0'), + private_key: Nostr::PrivateKey.new('22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf') ) end @@ -57,15 +57,15 @@ RSpec.describe Nostr::Crypto do describe '#descrypt_text' do let(:sender_keypair) do Nostr::KeyPair.new( - public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', - private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' + public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'), + private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757') ) end let(:recipient_keypair) do Nostr::KeyPair.new( - public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0', - private_key: '22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf' + public_key: Nostr::PublicKey.new('6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0'), + private_key: Nostr::PrivateKey.new('22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf') ) end diff --git a/spec/nostr/errors/invalid_hrp_error_spec.rb b/spec/nostr/errors/invalid_hrp_error_spec.rb new file mode 100644 index 0000000..578f997 --- /dev/null +++ b/spec/nostr/errors/invalid_hrp_error_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::InvalidHRPError do + describe '#initialize' do + let(:given_hrp) { 'nwrong' } + let(:allowed_hrp) { 'nsec' } + let(:error) { described_class.new(given_hrp, allowed_hrp) } + + it 'builds a useful error message' do + expect(error.message).to eq("Invalid hrp: nwrong. The allowed hrp value for this kind of entity is 'nsec'.") + end + end +end diff --git a/spec/nostr/errors/invalid_key_format_error_spec.rb b/spec/nostr/errors/invalid_key_format_error_spec.rb new file mode 100644 index 0000000..c1450bb --- /dev/null +++ b/spec/nostr/errors/invalid_key_format_error_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::InvalidKeyFormatError do + describe '#initialize' do + let(:key_kind) { 'private' } + let(:error) { described_class.new(key_kind) } + + it 'builds a useful error message' do + expect(error.message).to eq('Only lowercase hexadecimal characters are allowed in private keys.') + end + end +end diff --git a/spec/nostr/errors/invalid_key_length_error_spec.rb b/spec/nostr/errors/invalid_key_length_error_spec.rb new file mode 100644 index 0000000..8b1d219 --- /dev/null +++ b/spec/nostr/errors/invalid_key_length_error_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::InvalidKeyLengthError do + describe '#initialize' do + let(:key_kind) { 'private' } + let(:error) { described_class.new(key_kind) } + + it 'builds a useful error message' do + expect(error.message).to eq('Invalid private key length. It should have 64 characters.') + end + end +end diff --git a/spec/nostr/errors/invalid_key_type_error_spec.rb b/spec/nostr/errors/invalid_key_type_error_spec.rb new file mode 100644 index 0000000..b1c50c9 --- /dev/null +++ b/spec/nostr/errors/invalid_key_type_error_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::InvalidKeyTypeError do + describe '#initialize' do + let(:key_kind) { 'private' } + let(:error) { described_class.new(key_kind) } + + it 'builds a useful error message' do + expect(error.message).to eq('Invalid private key type') + end + end +end diff --git a/spec/nostr/event_spec.rb b/spec/nostr/event_spec.rb index ef6a22d..c0aabce 100644 --- a/spec/nostr/event_spec.rb +++ b/spec/nostr/event_spec.rb @@ -233,8 +233,8 @@ RSpec.describe Nostr::Event do end let(:keypair) do Nostr::KeyPair.new( - public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', - private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' + public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'), + private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757') ) end diff --git a/spec/nostr/events/encrypted_direct_message_spec.rb b/spec/nostr/events/encrypted_direct_message_spec.rb index 385324c..0eaebc9 100644 --- a/spec/nostr/events/encrypted_direct_message_spec.rb +++ b/spec/nostr/events/encrypted_direct_message_spec.rb @@ -6,15 +6,15 @@ RSpec.describe Nostr::Events::EncryptedDirectMessage do describe '.new' do let(:sender_keypair) do Nostr::KeyPair.new( - public_key: '8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca', - private_key: '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757' + public_key: Nostr::PublicKey.new('8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca'), + private_key: Nostr::PrivateKey.new('3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757') ) end let(:recipient_keypair) do Nostr::KeyPair.new( - public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0', - private_key: '22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf' + public_key: Nostr::PublicKey.new('6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0'), + private_key: Nostr::PrivateKey.new('22cea01c33eccf30fdd54cb6728f814f6de00c778aafd721e017f4582545f9cf') ) end diff --git a/spec/nostr/key_pair_spec.rb b/spec/nostr/key_pair_spec.rb index 3c4cd91..d2c2190 100644 --- a/spec/nostr/key_pair_spec.rb +++ b/spec/nostr/key_pair_spec.rb @@ -5,19 +5,43 @@ require 'spec_helper' RSpec.describe Nostr::KeyPair do let(:keypair) do described_class.new( - private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', - public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' + private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'), + public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558') ) end describe '.new' do - it 'creates an instance of a key pair' do - keypair = described_class.new( - private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', - public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' - ) + context 'when private_key is not an instance of PrivateKey' do + it 'raises an error' do + expect do + described_class.new( + private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', + public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558') + ) + end.to raise_error(ArgumentError, 'private_key is not an instance of PrivateKey') + end + end - expect(keypair).to be_an_instance_of(described_class) + context 'when public_key is not an instance of PublicKey' do + it 'raises an error' do + expect do + described_class.new( + private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'), + public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' + ) + end.to raise_error(ArgumentError, 'public_key is not an instance of PublicKey') + end + end + + context 'when private_key is an instance of PrivateKey and public_key is an instance of PublicKey' do + it 'creates an instance of a key pair' do + keypair = described_class.new( + private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'), + public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558') + ) + + expect(keypair).to be_an_instance_of(described_class) + end end end diff --git a/spec/nostr/key_spec.rb b/spec/nostr/key_spec.rb new file mode 100644 index 0000000..957da65 --- /dev/null +++ b/spec/nostr/key_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::Key do + let(:subclass) do + Class.new(Nostr::Key) do + def self.hrp + 'npub' + end + + protected + + def validate_hex_value(_hex_value) = nil + end + end + + let(:valid_hex) { 'a' * 64 } + + describe '.new' do + it 'raises an error because this is an abstract class' do + expect { described_class.new(valid_hex) }.to raise_error(/Subclasses must implement this method/) + end + end + + describe '.from_bech32' do + context 'when given a valid Bech32 value' do + let(:valid_bech32) { 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg' } + + it 'creates a new key' do + expect { subclass.from_bech32(valid_bech32) }.not_to raise_error + end + end + + context 'when given an invalid Bech32 value' do + let(:invalid_bech32) { 'this is obviously not valid' } + + it 'raises an error' do + expect { subclass.from_bech32(invalid_bech32) }.to raise_error(ArgumentError, /Invalid nip19 string\./) + end + end + end + + describe '.hrp' do + context 'when called on the abstract class' do + it 'raises an error because this is an abstract method' do + expect { described_class.hrp }.to raise_error(/Subclasses must implement this method/) + end + end + + context 'when called on a subclass' do + it 'returns the human readable part of a Bech32 string' do + expect(subclass.hrp).to eq('npub') + end + end + end + + describe '#to_bech32' do + let(:key) { subclass.new(valid_hex) } + + it 'returns a bech32 string representation of the key' do + expect(key.to_bech32).to match(/^npub[0-9a-z]+$/) + end + end + + describe '#validate_hex_value' do + let(:invalid_hex) { 'g' * 64 } + + it 'raises an error because this is an abstract method' do + expect { described_class.new(invalid_hex) }.to raise_error(/Subclasses must implement this method/) + end + end +end diff --git a/spec/nostr/keygen_spec.rb b/spec/nostr/keygen_spec.rb index 0bc01c8..35e1cf0 100644 --- a/spec/nostr/keygen_spec.rb +++ b/spec/nostr/keygen_spec.rb @@ -13,6 +13,31 @@ RSpec.describe Nostr::Keygen do end end + describe '.get_key_pair_from_private_key' do + context 'when given a private key' do + let(:private_key) { Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900') } + + it 'generates a key pair' do + keypair = keygen.get_key_pair_from_private_key(private_key) + + aggregate_failures do + expect(keypair.private_key).to be_an_instance_of(Nostr::PrivateKey) + expect(keypair.public_key).to be_an_instance_of(Nostr::PublicKey) + end + end + end + + context 'when given another kind of value' do + let(:not_a_private_key) { 'something else' } + + it 'raises an error' do + expect { keygen.get_key_pair_from_private_key(not_a_private_key) }.to raise_error( + ArgumentError, 'private_key is not an instance of PrivateKey' + ) + end + end + end + describe '#generate_key_pair' do it 'generates a private/public key pair' do keypair = keygen.generate_key_pair @@ -33,11 +58,24 @@ RSpec.describe Nostr::Keygen do end describe '#extract_public_key' do - it 'extracts a public key from a private key' do - private_key = '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900' - public_key = keygen.extract_public_key(private_key) + context 'when the given value is not a private key' do + let(:not_a_private_key) { 'something else' } - expect(public_key).to eq('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558') + it 'raises an error' do + expect { keygen.extract_public_key(not_a_private_key) }.to raise_error( + ArgumentError, 'private_key is not an instance of PrivateKey' + ) + end + end + + context 'when the given value is a private key' do + let(:private_key) { Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900') } + + it 'extracts a public key from a private key' do + public_key = keygen.extract_public_key(private_key) + + expect(public_key).to eq('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558') + end end end end diff --git a/spec/nostr/private_key_spec.rb b/spec/nostr/private_key_spec.rb new file mode 100644 index 0000000..b4f849c --- /dev/null +++ b/spec/nostr/private_key_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::PrivateKey do + let(:valid_hex) { '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa' } + let(:private_key) { described_class.new(valid_hex) } + + describe '.new' do + context 'when the private key is not a string' do + it 'raises an InvalidKeyTypeError' do + expect { described_class.new(1234) }.to raise_error( + Nostr::InvalidKeyTypeError, + 'Invalid private key type' + ) + end + end + + context "when the private key's length is not 64 characters" do + it 'raises an InvalidKeyLengthError' do + expect { described_class.new('a' * 65) }.to raise_error( + Nostr::InvalidKeyLengthError, + 'Invalid private key length. It should have 64 characters.' + ) + end + end + + context 'when the private key contains non-hexadecimal characters' do + it 'raises an InvalidKeyFormatError' do + expect { described_class.new('g' * 64) }.to raise_error( + Nostr::InvalidKeyFormatError, + 'Only lowercase hexadecimal characters are allowed in private keys.' + ) + end + end + + context 'when the private key contains uppercase characters' do + it 'raises an InvalidKeyFormatError' do + expect { described_class.new('A' * 64) }.to raise_error( + Nostr::InvalidKeyFormatError, + 'Only lowercase hexadecimal characters are allowed in private keys.' + ) + end + end + + context 'when the private key is valid' do + it 'does not raise any error' do + expect { described_class.new('a' * 64) }.not_to raise_error + end + end + end + + describe '.from_bech32' do + context 'when given a valid Bech32 value' do + let(:valid_bech32) { 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5' } + + it 'instantiates a private key from a Bech32 encoded string' do + expect(described_class.from_bech32(valid_bech32)).to eq(valid_hex) + end + end + + context 'when given an invalid Bech32 value' do + let(:invalid_bech32) { 'this is obviously not valid' } + + it 'raises an error' do + expect { described_class.from_bech32(invalid_bech32) }.to raise_error(ArgumentError, /Invalid nip19 string\./) + end + end + end + + describe '.hrp' do + it 'returns the human readable part of a Bech32 string' do + expect(described_class.hrp).to eq('nsec') + end + end + + describe '#to_bech32' do + it 'converts the hex key to bech32' do + expect(private_key.to_bech32).to eq('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5') + end + end +end diff --git a/spec/nostr/public_key_spec.rb b/spec/nostr/public_key_spec.rb new file mode 100644 index 0000000..b7689b3 --- /dev/null +++ b/spec/nostr/public_key_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Nostr::PublicKey do + let(:valid_hex) { '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e' } + let(:public_key) { described_class.new(valid_hex) } + + describe '.new' do + context 'when the public key is not a string' do + it 'raises an InvalidKeyTypeError' do + expect { described_class.new(1234) }.to raise_error( + Nostr::InvalidKeyTypeError, + 'Invalid public key type' + ) + end + end + + context "when the public key's length is not 64 characters" do + it 'raises an InvalidKeyLengthError' do + expect { described_class.new('a' * 65) }.to raise_error( + Nostr::InvalidKeyLengthError, + 'Invalid public key length. It should have 64 characters.' + ) + end + end + + context 'when the public key contains non-hexadecimal characters' do + it 'raises an InvalidKeyFormatError' do + expect { described_class.new('g' * 64) }.to raise_error( + Nostr::InvalidKeyFormatError, + 'Only lowercase hexadecimal characters are allowed in public keys.' + ) + end + end + + context 'when the public key contains uppercase characters' do + it 'raises an InvalidKeyFormatError' do + expect { described_class.new('A' * 64) }.to raise_error( + Nostr::InvalidKeyFormatError, + 'Only lowercase hexadecimal characters are allowed in public keys.' + ) + end + end + + context 'when the public key is valid' do + it 'does not raise any error' do + expect { described_class.new('a' * 64) }.not_to raise_error + end + end + end + + describe '.from_bech32' do + context 'when given a valid Bech32 value' do + let(:valid_bech32) { 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg' } + + it 'instantiates a public key from a Bech32 encoded string' do + expect(described_class.from_bech32(valid_bech32)).to eq(valid_hex) + end + end + + context 'when given an invalid Bech32 value' do + let(:invalid_bech32) { 'this is obviously not valid' } + + it 'raises an error' do + expect { described_class.from_bech32(invalid_bech32) }.to raise_error(ArgumentError, /Invalid nip19 string\./) + end + end + end + + describe '.hrp' do + it 'returns the human readable part of a Bech32 string' do + expect(described_class.hrp).to eq('npub') + end + end + + describe '#to_bech32' do + it 'converts the hex key to bech32' do + expect(public_key.to_bech32).to eq('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg') + end + end +end diff --git a/spec/nostr/user_spec.rb b/spec/nostr/user_spec.rb index 0d8413f..bc313e2 100644 --- a/spec/nostr/user_spec.rb +++ b/spec/nostr/user_spec.rb @@ -5,8 +5,8 @@ require 'spec_helper' RSpec.describe Nostr::User do let(:keypair) do Nostr::KeyPair.new( - private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900', - public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558' + private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'), + public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558') ) end