nostr 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.adr-dir +1 -0
  3. data/.editorconfig +1 -1
  4. data/.rubocop.yml +24 -1
  5. data/.tool-versions +2 -1
  6. data/CHANGELOG.md +70 -1
  7. data/README.md +93 -228
  8. data/adr/0001-record-architecture-decisions.md +19 -0
  9. data/adr/0002-introduction-of-signature-class.md +27 -0
  10. data/docs/.gitignore +4 -0
  11. data/docs/.vitepress/config.mjs +114 -0
  12. data/docs/README.md +44 -0
  13. data/docs/api-examples.md +49 -0
  14. data/docs/bun.lockb +0 -0
  15. data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
  16. data/docs/common-use-cases/signing-and-verifying-events.md +50 -0
  17. data/docs/common-use-cases/signing-and-verifying-messages.md +43 -0
  18. data/docs/core/client.md +108 -0
  19. data/docs/core/keys.md +136 -0
  20. data/docs/core/user.md +43 -0
  21. data/docs/events/contact-list.md +29 -0
  22. data/docs/events/encrypted-direct-message.md +28 -0
  23. data/docs/events/recommend-server.md +32 -0
  24. data/docs/events/set-metadata.md +20 -0
  25. data/docs/events/text-note.md +15 -0
  26. data/docs/events.md +11 -0
  27. data/docs/getting-started/installation.md +21 -0
  28. data/docs/getting-started/overview.md +171 -0
  29. data/docs/implemented-nips.md +9 -0
  30. data/docs/index.md +42 -0
  31. data/docs/markdown-examples.md +85 -0
  32. data/docs/package.json +12 -0
  33. data/docs/relays/connecting-to-a-relay.md +21 -0
  34. data/docs/relays/publishing-events.md +29 -0
  35. data/docs/relays/receiving-events.md +6 -0
  36. data/docs/subscriptions/creating-a-subscription.md +49 -0
  37. data/docs/subscriptions/deleting-a-subscription.md +10 -0
  38. data/docs/subscriptions/filtering-subscription-events.md +115 -0
  39. data/docs/subscriptions/updating-a-subscription.md +4 -0
  40. data/lib/nostr/bech32.rb +203 -0
  41. data/lib/nostr/client.rb +2 -1
  42. data/lib/nostr/crypto.rb +93 -13
  43. data/lib/nostr/errors/error.rb +7 -0
  44. data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
  45. data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
  46. data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
  47. data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
  48. data/lib/nostr/errors/invalid_signature_format_error.rb +18 -0
  49. data/lib/nostr/errors/invalid_signature_length_error.rb +18 -0
  50. data/lib/nostr/errors/invalid_signature_type_error.rb +16 -0
  51. data/lib/nostr/errors/key_validation_error.rb +6 -0
  52. data/lib/nostr/errors/signature_validation_error.rb +6 -0
  53. data/lib/nostr/errors.rb +12 -0
  54. data/lib/nostr/event.rb +40 -13
  55. data/lib/nostr/event_kind.rb +1 -0
  56. data/lib/nostr/events/encrypted_direct_message.rb +8 -7
  57. data/lib/nostr/filter.rb +14 -11
  58. data/lib/nostr/key.rb +100 -0
  59. data/lib/nostr/key_pair.rb +54 -6
  60. data/lib/nostr/keygen.rb +44 -5
  61. data/lib/nostr/private_key.rb +36 -0
  62. data/lib/nostr/public_key.rb +36 -0
  63. data/lib/nostr/relay_message_type.rb +18 -0
  64. data/lib/nostr/signature.rb +67 -0
  65. data/lib/nostr/subscription.rb +2 -2
  66. data/lib/nostr/user.rb +17 -8
  67. data/lib/nostr/version.rb +1 -1
  68. data/lib/nostr.rb +7 -0
  69. data/nostr.gemspec +13 -13
  70. data/sig/nostr/bech32.rbs +14 -0
  71. data/sig/nostr/client.rbs +5 -5
  72. data/sig/nostr/crypto.rbs +8 -5
  73. data/sig/nostr/errors/error.rbs +4 -0
  74. data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
  75. data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
  76. data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
  77. data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
  78. data/sig/nostr/errors/invalid_signature_format_error.rbs +5 -0
  79. data/sig/nostr/errors/invalid_signature_length_error.rbs +5 -0
  80. data/sig/nostr/errors/invalid_signature_type_error.rbs +5 -0
  81. data/sig/nostr/errors/key_validation_error.rbs +4 -0
  82. data/sig/nostr/errors/signature_validation_error.rbs +4 -0
  83. data/sig/nostr/event.rbs +11 -10
  84. data/sig/nostr/events/encrypted_direct_message.rbs +2 -2
  85. data/sig/nostr/filter.rbs +3 -12
  86. data/sig/nostr/key.rbs +16 -0
  87. data/sig/nostr/key_pair.rbs +8 -3
  88. data/sig/nostr/keygen.rbs +5 -2
  89. data/sig/nostr/private_key.rbs +4 -0
  90. data/sig/nostr/public_key.rbs +4 -0
  91. data/sig/nostr/relay_message_type.rbs +8 -0
  92. data/sig/nostr/signature.rbs +14 -0
  93. data/sig/nostr/user.rbs +4 -8
  94. data/sig/vendor/bech32/nostr/entity.rbs +41 -0
  95. data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
  96. data/sig/vendor/bech32/segwit_addr.rbs +21 -0
  97. data/sig/vendor/bech32.rbs +25 -0
  98. data/sig/vendor/event_emitter.rbs +10 -3
  99. data/sig/vendor/event_machine/channel.rbs +1 -1
  100. data/sig/vendor/faye/websocket/api.rbs +45 -0
  101. data/sig/vendor/faye/websocket/client.rbs +43 -0
  102. data/sig/vendor/faye/websocket.rbs +30 -0
  103. data/sig/vendor/schnorr/signature.rbs +16 -0
  104. data/sig/vendor/schnorr.rbs +3 -1
  105. metadata +102 -28
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Raised when the signature is in an invalid format
5
+ #
6
+ # @api public
7
+ #
8
+ class InvalidSignatureFormatError < SignatureValidationError
9
+ # Initializes the error
10
+ #
11
+ # @example
12
+ # InvalidSignatureFormatError.new
13
+ #
14
+ def initialize
15
+ super('Only lowercase hexadecimal characters are allowed in signatures.')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Raised when the signature's length is not 128 characters
5
+ #
6
+ # @api public
7
+ #
8
+ class InvalidSignatureLengthError < SignatureValidationError
9
+ # Initializes the error
10
+ #
11
+ # @example
12
+ # InvalidSignatureLengthError.new
13
+ #
14
+ def initialize
15
+ super('Invalid signature length. It should have 128 characters.')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Raised when the signature is not a string
5
+ #
6
+ # @api public
7
+ #
8
+ class InvalidSignatureTypeError < SignatureValidationError
9
+ # Initializes the error
10
+ #
11
+ # @example
12
+ # InvalidSignatureTypeError.new
13
+ #
14
+ def initialize = super('Invalid signature type')
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Base class for all key validation errors
5
+ class KeyValidationError < Error; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Base class for all signature validation errors
5
+ class SignatureValidationError < Error; end
6
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/error'
4
+ require_relative 'errors/key_validation_error'
5
+ require_relative 'errors/invalid_hrp_error'
6
+ require_relative 'errors/invalid_key_type_error'
7
+ require_relative 'errors/invalid_key_length_error'
8
+ require_relative 'errors/invalid_key_format_error'
9
+ require_relative 'errors/signature_validation_error'
10
+ require_relative 'errors/invalid_signature_type_error'
11
+ require_relative 'errors/invalid_signature_length_error'
12
+ require_relative 'errors/invalid_signature_format_error'
data/lib/nostr/event.rb CHANGED
@@ -100,15 +100,15 @@ module Nostr
100
100
  #
101
101
  # @example Instantiating a new event
102
102
  # Nostr::Event.new(
103
- # id: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
104
- # pubkey: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
105
- # created_at: 1230981305,
106
- # kind: 1,
107
- # tags: [],
108
- # content: 'Your feedback is appreciated, now pay $8',
109
- # sig: '123ac2923b792ce730b3da34f16155470ab13c8f97f9c53eaeb334f1fb3a5dc9a7f643
110
- # 937c6d6e9855477638f5655c5d89c9aa5501ea9b578a66aced4f1cd7b3'
111
- # )
103
+ # id: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
104
+ # pubkey: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
105
+ # created_at: 1230981305,
106
+ # kind: 1,
107
+ # tags: [],
108
+ # content: 'Your feedback is appreciated, now pay $8',
109
+ # sig: '123ac2923b792ce730b3da34f16155470ab13c8f97f9c53eaeb334f1fb3a5dc9a7f643
110
+ # 937c6d6e9855477638f5655c5d89c9aa5501ea9b578a66aced4f1cd7b3'
111
+ # )
112
112
  #
113
113
  # @param id [String|nil] 32-bytes sha256 of the the serialized event data.
114
114
  # @param sig [String|nil] 64-bytes signature of the sha256 hash of the serialized event data, which is
@@ -128,7 +128,6 @@ module Nostr
128
128
  id: nil,
129
129
  sig: nil
130
130
  )
131
-
132
131
  @id = id
133
132
  @sig = sig
134
133
  @pubkey = pubkey
@@ -160,11 +159,11 @@ module Nostr
160
159
  # pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
161
160
  # event.add_pubkey_reference(pubkey)
162
161
  #
163
- # @param pubkey [String] 32-bytes hex-encoded public key.
162
+ # @param pubkey [PublicKey] 32-bytes hex-encoded public key.
164
163
  #
165
164
  # @return [Array<String>] The event's updated list of tags
166
165
  #
167
- def add_pubkey_reference(pubkey) = tags.push(['p', pubkey])
166
+ def add_pubkey_reference(pubkey) = tags.push(['p', pubkey.to_s])
168
167
 
169
168
  # Signs an event with the user's private key
170
169
  #
@@ -173,7 +172,7 @@ module Nostr
173
172
  # @example Signing an event
174
173
  # event.sign(private_key)
175
174
  #
176
- # @param private_key [String] 32-bytes hex-encoded private key.
175
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
177
176
  #
178
177
  # @return [Event] A signed event.
179
178
  #
@@ -182,6 +181,34 @@ module Nostr
182
181
  crypto.sign_event(self, private_key)
183
182
  end
184
183
 
184
+ # Verifies if the signature of the event is valid. A valid signature means that the event was signed by the owner
185
+ #
186
+ # @api public
187
+ #
188
+ # @example Verifying the signature of an event
189
+ # event = Nostr::Event.new(
190
+ # id: '90b75b78daf883ae57fbcc414d43faa028560b3211ee58e4ea82bf395bb82042',
191
+ # pubkey: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
192
+ # created_at: 1667422587,
193
+ # kind: Nostr::EventKind::TEXT_NOTE,
194
+ # content: 'Your feedback is appreciated, now pay $8',
195
+ # sig: '32f18adebe942e19b171c1c7d2fb27ce794dfea4155e289dca7952b43ed1ec39' \
196
+ # '1d3dc198ba2761bc6d40c737a6eaf4edcc8963acabd3bfcebd04f16637025bdc'
197
+ # )
198
+ #
199
+ # event.verify_signature # => true
200
+ #
201
+ # @return [Boolean] Whether the signature is valid or not.
202
+ #
203
+ def verify_signature
204
+ crypto = Crypto.new
205
+
206
+ return false if id.nil? || pubkey.nil?
207
+ return false if sig.nil? # FIXME: See https://github.com/soutaro/steep/issues/1079
208
+
209
+ crypto.valid_sig?(id, pubkey, sig)
210
+ end
211
+
185
212
  # Serializes the event, to obtain a SHA256 digest of it
186
213
  #
187
214
  # @api public
@@ -21,6 +21,7 @@ module Nostr
21
21
  # The content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to
22
22
  # recommend to its followers.
23
23
  #
24
+ # @deprecated This event kind was removed in https://github.com/nostr-protocol/nips/pull/703/files#diff-39307f1617417657ee9874be314f13aabdc74401b124d0afe8217f2919c9c7d8L105
24
25
  # @return [Integer]
25
26
  #
26
27
  RECOMMEND_SERVER = 2
@@ -11,23 +11,24 @@ module Nostr
11
11
  # @api public
12
12
  #
13
13
  # @example Instantiating a new encrypted direct message
14
- # Nostr::Events::EncryptedDirectMessage.new(
14
+ # Nostr::Events::EncryptedDirectMessage.new(
15
15
  # sender_private_key: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
16
16
  # recipient_public_key: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
17
17
  # plain_text: 'Your feedback is appreciated, now pay $8',
18
- # )
18
+ # )
19
19
  #
20
20
  # @example Instantiating a new encrypted direct message that references a previous direct message
21
- # Nostr::Events::EncryptedDirectMessage.new(
21
+ # Nostr::Events::EncryptedDirectMessage.new(
22
22
  # sender_private_key: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
23
23
  # recipient_public_key: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
24
24
  # plain_text: 'Your feedback is appreciated, now pay $8',
25
25
  # previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
26
- # )
26
+ # )
27
27
  #
28
28
  # @param plain_text [String] The +content+ of the encrypted message.
29
- # @param sender_private_key [String] 32-bytes hex-encoded private key of the message's author.
30
- # @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient of the encrypted message.
29
+ # @param sender_private_key [PrivateKey] 32-bytes hex-encoded private key of the message's author.
30
+ # @param recipient_public_key [PublicKey] 32-bytes hex-encoded public key of the recipient of the encrypted
31
+ # message.
31
32
  # @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a
32
33
  # conversation or a message we are explicitly replying to (such that contextual, more organized conversations
33
34
  # may happen
@@ -43,7 +44,7 @@ module Nostr
43
44
  pubkey: sender_public_key,
44
45
  kind: Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE,
45
46
  content: encrypted_content,
46
- )
47
+ )
47
48
 
48
49
  add_pubkey_reference(recipient_public_key)
49
50
  add_event_reference(previous_direct_message) if previous_direct_message
data/lib/nostr/filter.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Nostr
4
4
  # A filter determines what events will be sent in a subscription.
5
5
  class Filter
6
- # A list of event ids or prefixes
6
+ # A list of event ids
7
7
  #
8
8
  # @api public
9
9
  #
@@ -14,7 +14,7 @@ module Nostr
14
14
  #
15
15
  attr_reader :ids
16
16
 
17
- # A list of pubkeys or prefixes, the pubkey of an event must be one of these
17
+ # A list of pubkeys, the pubkey of an event must be one of these
18
18
  #
19
19
  # @api public
20
20
  #
@@ -107,8 +107,8 @@ module Nostr
107
107
  # )
108
108
  #
109
109
  # @param kwargs [Hash]
110
- # @option kwargs [Array<String>, nil] ids A list of event ids or prefixes
111
- # @option kwargs [Array<String>, nil] authors A list of pubkeys or prefixes, the pubkey of an event must be one
110
+ # @option kwargs [Array<String>, nil] ids A list of event ids
111
+ # @option kwargs [Array<String>, nil] authors A list of pubkeys, the pubkey of an event must be one
112
112
  # of these
113
113
  # @option kwargs [Array<Integer>, nil] kinds A list of a kind numbers
114
114
  # @option kwargs [Array<String>, nil] e A list of event ids that are referenced in an "e" tag
@@ -133,13 +133,16 @@ module Nostr
133
133
  # @api public
134
134
  #
135
135
  # @example
136
- # filter.to_h # => {:ids=>["c24881c305c5cfb7c1168be7e9b0e150"],
137
- # :authors=>["000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7"],
138
- # :kinds=>[0, 1, 2],
139
- # :"#e"=>["7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2"],
140
- # :"#p"=>["000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7"],
141
- # :since=>1230981305,
142
- # :until=>1292190341}
136
+ # filter.to_h # =>
137
+ # {
138
+ # ids: ['c24881c305c5cfb7c1168be7e9b0e150'],
139
+ # authors: ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7'],
140
+ # kinds: [0, 1, 2],
141
+ # '#e': ['7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2'],
142
+ # '#p': ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7'],
143
+ # since: 1230981305,
144
+ # until: 1292190341
145
+ # }
143
146
  #
144
147
  # @return [Hash] The filter as a hash.
145
148
  #
data/lib/nostr/key.rb ADDED
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Abstract class for all keys
5
+ #
6
+ # @api private
7
+ #
8
+ class Key < String
9
+ # The regular expression for hexadecimal lowercase characters
10
+ #
11
+ # @return [Regexp] The regular expression for hexadecimal lowercase characters
12
+ #
13
+ FORMAT = /^[a-f0-9]+$/
14
+
15
+ # The length of the key in hex format
16
+ #
17
+ # @return [Integer] The length of the key in hex format
18
+ #
19
+ LENGTH = 64
20
+
21
+ # Instantiates a new key. Can't be used directly because this is an abstract class. Raises a +KeyValidationError+
22
+ #
23
+ # @see Nostr::PrivateKey
24
+ # @see Nostr::PublicKey
25
+ #
26
+ # @param [String] hex_value Hex-encoded value of the key
27
+ #
28
+ # @raise [KeyValidationError]
29
+ #
30
+ def initialize(hex_value)
31
+ validate_hex_value(hex_value)
32
+
33
+ super(hex_value)
34
+ end
35
+
36
+ # Instantiates a key from a bech32 string
37
+ #
38
+ # @api public
39
+ #
40
+ # @example
41
+ # bech32_key = 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
42
+ # bech32_key.to_key # => #<Nostr::PublicKey:0x000000010601e3c8 @hex_value="...">
43
+ #
44
+ # @raise [Nostr::InvalidHRPError] if the bech32 string is invalid.
45
+ #
46
+ # @param [String] bech32_value The bech32 string representation of the key.
47
+ #
48
+ # @return [Key] the key.
49
+ #
50
+ def self.from_bech32(bech32_value)
51
+ type, data = Bech32.decode(bech32_value)
52
+
53
+ raise InvalidHRPError.new(type, hrp) unless type == hrp
54
+
55
+ new(data)
56
+ end
57
+
58
+ # Abstract method to be implemented by subclasses to provide the HRP (npub, nsec)
59
+ #
60
+ # @api private
61
+ #
62
+ # @return [String] The HRP
63
+ #
64
+ def self.hrp
65
+ raise 'Subclasses must implement this method'
66
+ end
67
+
68
+ # Converts the key to a bech32 string representation
69
+ #
70
+ # @api public
71
+ #
72
+ # @example Converting a private key to a bech32 string
73
+ # public_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
74
+ # public_key.to_bech32 # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
75
+ #
76
+ # @example Converting a public key to a bech32 string
77
+ # public_key = Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
78
+ # public_key.to_bech32 # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
79
+ #
80
+ # @return [String] The bech32 string representation of the key
81
+ #
82
+ def to_bech32 = Bech32.encode(hrp: self.class.hrp, data: self)
83
+
84
+ protected
85
+
86
+ # Validates the hex value during initialization
87
+ #
88
+ # @api private
89
+ #
90
+ # @param [String] _hex_value The hex value of the key
91
+ #
92
+ # @raise [KeyValidationError] When the hex value is invalid
93
+ #
94
+ # @return [void]
95
+ #
96
+ def validate_hex_value(_hex_value)
97
+ raise 'Subclasses must implement this method'
98
+ end
99
+ end
100
+ end
@@ -10,7 +10,7 @@ module Nostr
10
10
  # @example
11
11
  # keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
12
12
  #
13
- # @return [String]
13
+ # @return [PrivateKey]
14
14
  #
15
15
  attr_reader :private_key
16
16
 
@@ -21,7 +21,7 @@ module Nostr
21
21
  # @example
22
22
  # keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
23
23
  #
24
- # @return [String]
24
+ # @return [PublicKey]
25
25
  #
26
26
  attr_reader :public_key
27
27
 
@@ -31,16 +31,64 @@ module Nostr
31
31
  #
32
32
  # @example
33
33
  # keypair = Nostr::KeyPair.new(
34
- # private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
35
- # public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
34
+ # private_key: Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'),
35
+ # public_key: Nostr::PublicKey.new('2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'),
36
36
  # )
37
37
  #
38
- # @param private_key [String] 32-bytes hex-encoded private key.
39
- # @param public_key [String] 32-bytes hex-encoded public key.
38
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
39
+ # @param public_key [PublicKey] 32-bytes hex-encoded public key.
40
+ #
41
+ # @raise ArgumentError when the private key is not a {PrivateKey}
42
+ # @raise ArgumentError when the public key is not a {PublicKey}
40
43
  #
41
44
  def initialize(private_key:, public_key:)
45
+ validate_keys(private_key, public_key)
46
+
42
47
  @private_key = private_key
43
48
  @public_key = public_key
44
49
  end
50
+
51
+ # Allows array destructuring of the KeyPair, enabling the extraction of +PrivateKey+ and +PublicKey+ separately
52
+ #
53
+ # @api public
54
+ #
55
+ # @example Implicit usage of `to_ary` for destructuring
56
+ # keypair = Nostr::KeyPair.new(
57
+ # private_key: Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d'),
58
+ # public_key: Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6'),
59
+ # )
60
+ # # The `to_ary` method can be implicitly used for array destructuring:
61
+ # private_key, public_key = keypair
62
+ # # Now `private_key` and `public_key` hold the respective values.
63
+ #
64
+ # @example Explicit usage of `to_ary`
65
+ # array_representation = keypair.to_ary
66
+ # # array_representation is now an array: [PrivateKey, PublicKey]
67
+ # # where PrivateKey and PublicKey are the respective objects.
68
+ #
69
+ # @return [Array<PrivateKey, PublicKey>] An array containing the {PrivateKey} and {PublicKey} in that order
70
+ #
71
+ def to_ary
72
+ [private_key, public_key]
73
+ end
74
+
75
+ private
76
+
77
+ # Validates the keys
78
+ #
79
+ # @api private
80
+ #
81
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
82
+ # @param public_key [PublicKey] 32-bytes hex-encoded public key.
83
+ #
84
+ # @raise ArgumentError when the private key is not a +PrivateKey+
85
+ # @raise ArgumentError when the public key is not a +PublicKey+
86
+ #
87
+ # @return [void]
88
+ #
89
+ def validate_keys(private_key, public_key)
90
+ raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
91
+ raise ArgumentError, 'public_key is not an instance of PublicKey' unless public_key.is_a?(Nostr::PublicKey)
92
+ end
45
93
  end
46
94
  end
data/lib/nostr/keygen.rb CHANGED
@@ -22,7 +22,7 @@ module Nostr
22
22
  # @api public
23
23
  #
24
24
  # @example
25
- # keypair = keygen.generate_keypair
25
+ # keypair = keygen.generate_key_pair
26
26
  # keypair # #<Nostr::KeyPair:0x0000000107bd3550
27
27
  # @private_key="893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900",
28
28
  # @public_key="2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558">
@@ -44,10 +44,11 @@ module Nostr
44
44
  # private_key = keygen.generate_private_key
45
45
  # private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
46
46
  #
47
- # @return [String] A 32-bytes hex-encoded private key.
47
+ # @return [PrivateKey] A 32-bytes hex-encoded private key.
48
48
  #
49
49
  def generate_private_key
50
- (SecureRandom.random_number(group.order - 1) + 1).to_s(16)
50
+ hex_value = (SecureRandom.random_number(group.order - 1) + 1).to_s(16).rjust(64, '0')
51
+ PrivateKey.new(hex_value)
51
52
  end
52
53
 
53
54
  # Extracts a public key from a private key
@@ -59,10 +60,36 @@ module Nostr
59
60
  # public_key = keygen.extract_public_key(private_key)
60
61
  # public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
61
62
  #
62
- # @return [String] A 32-bytes hex-encoded public key.
63
+ # @param [PrivateKey] private_key A 32-bytes hex-encoded private key.
64
+ #
65
+ # @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
66
+ #
67
+ # @return [PublicKey] A 32-bytes hex-encoded public key.
63
68
  #
64
69
  def extract_public_key(private_key)
65
- group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
70
+ validate_private_key(private_key)
71
+ hex_value = group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
72
+ PublicKey.new(hex_value)
73
+ end
74
+
75
+ # Builds a key pair from an existing private key
76
+ #
77
+ # @api public
78
+ #
79
+ # @example
80
+ # private_key = Nostr::PrivateKey.new('893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900')
81
+ # keygen.get_key_pair_from_private_key(private_key)
82
+ #
83
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
84
+ #
85
+ # @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
86
+ #
87
+ # @return [Nostr::KeyPair]
88
+ #
89
+ def get_key_pair_from_private_key(private_key)
90
+ validate_private_key(private_key)
91
+ public_key = extract_public_key(private_key)
92
+ KeyPair.new(private_key:, public_key:)
66
93
  end
67
94
 
68
95
  private
@@ -74,5 +101,17 @@ module Nostr
74
101
  # @return [ECDSA::Group]
75
102
  #
76
103
  attr_reader :group
104
+
105
+ # Validates that the private key is an instance of +PrivateKey+
106
+ #
107
+ # @api private
108
+ #
109
+ # @raise [ArgumentError] if the private key is not an instance of +PrivateKey+
110
+ #
111
+ # @return [void]
112
+ #
113
+ def validate_private_key(private_key)
114
+ raise ArgumentError, 'private_key is not an instance of PrivateKey' unless private_key.is_a?(Nostr::PrivateKey)
115
+ end
77
116
  end
78
117
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # 32-bytes lowercase hex-encoded private key
5
+ class PrivateKey < Key
6
+ # Human-readable part of the Bech32 encoded address
7
+ #
8
+ # @api private
9
+ #
10
+ # @return [String] The human-readable part of the Bech32 encoded address
11
+ #
12
+ def self.hrp
13
+ 'nsec'
14
+ end
15
+
16
+ private
17
+
18
+ # Validates the hex value of the private key
19
+ #
20
+ # @api private
21
+ #
22
+ # @param [String] hex_value The private key in hex format
23
+ #
24
+ # @raise InvalidKeyTypeError when the private key is not a string
25
+ # @raise InvalidKeyLengthError when the private key's length is not 64 characters
26
+ # @raise InvalidKeyFormatError when the private key is in an invalid format
27
+ #
28
+ # @return [void]
29
+ #
30
+ def validate_hex_value(hex_value)
31
+ raise InvalidKeyTypeError, 'private' unless hex_value.is_a?(String)
32
+ raise InvalidKeyLengthError, 'private' unless hex_value.size == Key::LENGTH
33
+ raise InvalidKeyFormatError, 'private' unless hex_value.match(Key::FORMAT)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # 32-bytes lowercase hex-encoded public key
5
+ class PublicKey < Key
6
+ # Human-readable part of the Bech32 encoded address
7
+ #
8
+ # @api private
9
+ #
10
+ # @return [String] The human-readable part of the Bech32 encoded address
11
+ #
12
+ def self.hrp
13
+ 'npub'
14
+ end
15
+
16
+ private
17
+
18
+ # Validates the hex value of the public key
19
+ #
20
+ # @api private
21
+ #
22
+ # @param [String] hex_value The public key in hex format
23
+ #
24
+ # @raise InvalidKeyTypeError when the public key is not a string
25
+ # @raise InvalidKeyLengthError when the public key's length is not 64 characters
26
+ # @raise InvalidKeyFormatError when the public key is in an invalid format
27
+ #
28
+ # @return [void]
29
+ #
30
+ def validate_hex_value(hex_value)
31
+ raise InvalidKeyTypeError, 'public' unless hex_value.is_a?(String)
32
+ raise InvalidKeyLengthError, 'public' unless hex_value.size == Key::LENGTH
33
+ raise InvalidKeyFormatError, 'public' unless hex_value.match(Key::FORMAT)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Clients can send 4 types of messages, which must be JSON arrays
5
+ module RelayMessageType
6
+ # @return [String] Used to notify clients all stored events have been sent
7
+ EOSE = 'EOSE'
8
+
9
+ # @return [String] Used to send events requested to clients
10
+ EVENT = 'EVENT'
11
+
12
+ # @return [String] Used to send human-readable messages to clients
13
+ NOTICE = 'NOTICE'
14
+
15
+ # @return [String] Used to notify clients if an EVENT was successful
16
+ OK = 'OK'
17
+ end
18
+ end