nostr 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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