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.
- checksums.yaml +4 -4
- data/.adr-dir +1 -0
- data/.editorconfig +1 -1
- data/.rubocop.yml +24 -1
- data/.tool-versions +2 -1
- data/CHANGELOG.md +70 -1
- data/README.md +93 -228
- data/adr/0001-record-architecture-decisions.md +19 -0
- data/adr/0002-introduction-of-signature-class.md +27 -0
- data/docs/.gitignore +4 -0
- data/docs/.vitepress/config.mjs +114 -0
- data/docs/README.md +44 -0
- data/docs/api-examples.md +49 -0
- data/docs/bun.lockb +0 -0
- data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
- data/docs/common-use-cases/signing-and-verifying-events.md +50 -0
- data/docs/common-use-cases/signing-and-verifying-messages.md +43 -0
- data/docs/core/client.md +108 -0
- data/docs/core/keys.md +136 -0
- data/docs/core/user.md +43 -0
- data/docs/events/contact-list.md +29 -0
- data/docs/events/encrypted-direct-message.md +28 -0
- data/docs/events/recommend-server.md +32 -0
- data/docs/events/set-metadata.md +20 -0
- data/docs/events/text-note.md +15 -0
- data/docs/events.md +11 -0
- data/docs/getting-started/installation.md +21 -0
- data/docs/getting-started/overview.md +171 -0
- data/docs/implemented-nips.md +9 -0
- data/docs/index.md +42 -0
- data/docs/markdown-examples.md +85 -0
- data/docs/package.json +12 -0
- data/docs/relays/connecting-to-a-relay.md +21 -0
- data/docs/relays/publishing-events.md +29 -0
- data/docs/relays/receiving-events.md +6 -0
- data/docs/subscriptions/creating-a-subscription.md +49 -0
- data/docs/subscriptions/deleting-a-subscription.md +10 -0
- data/docs/subscriptions/filtering-subscription-events.md +115 -0
- data/docs/subscriptions/updating-a-subscription.md +4 -0
- data/lib/nostr/bech32.rb +203 -0
- data/lib/nostr/client.rb +2 -1
- data/lib/nostr/crypto.rb +93 -13
- data/lib/nostr/errors/error.rb +7 -0
- data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
- data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
- data/lib/nostr/errors/invalid_signature_format_error.rb +18 -0
- data/lib/nostr/errors/invalid_signature_length_error.rb +18 -0
- data/lib/nostr/errors/invalid_signature_type_error.rb +16 -0
- data/lib/nostr/errors/key_validation_error.rb +6 -0
- data/lib/nostr/errors/signature_validation_error.rb +6 -0
- data/lib/nostr/errors.rb +12 -0
- data/lib/nostr/event.rb +40 -13
- data/lib/nostr/event_kind.rb +1 -0
- data/lib/nostr/events/encrypted_direct_message.rb +8 -7
- data/lib/nostr/filter.rb +14 -11
- data/lib/nostr/key.rb +100 -0
- data/lib/nostr/key_pair.rb +54 -6
- data/lib/nostr/keygen.rb +44 -5
- data/lib/nostr/private_key.rb +36 -0
- data/lib/nostr/public_key.rb +36 -0
- data/lib/nostr/relay_message_type.rb +18 -0
- data/lib/nostr/signature.rb +67 -0
- data/lib/nostr/subscription.rb +2 -2
- data/lib/nostr/user.rb +17 -8
- data/lib/nostr/version.rb +1 -1
- data/lib/nostr.rb +7 -0
- data/nostr.gemspec +13 -13
- data/sig/nostr/bech32.rbs +14 -0
- data/sig/nostr/client.rbs +5 -5
- data/sig/nostr/crypto.rbs +8 -5
- data/sig/nostr/errors/error.rbs +4 -0
- data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
- data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
- data/sig/nostr/errors/invalid_signature_format_error.rbs +5 -0
- data/sig/nostr/errors/invalid_signature_length_error.rbs +5 -0
- data/sig/nostr/errors/invalid_signature_type_error.rbs +5 -0
- data/sig/nostr/errors/key_validation_error.rbs +4 -0
- data/sig/nostr/errors/signature_validation_error.rbs +4 -0
- data/sig/nostr/event.rbs +11 -10
- data/sig/nostr/events/encrypted_direct_message.rbs +2 -2
- data/sig/nostr/filter.rbs +3 -12
- data/sig/nostr/key.rbs +16 -0
- data/sig/nostr/key_pair.rbs +8 -3
- data/sig/nostr/keygen.rbs +5 -2
- data/sig/nostr/private_key.rbs +4 -0
- data/sig/nostr/public_key.rbs +4 -0
- data/sig/nostr/relay_message_type.rbs +8 -0
- data/sig/nostr/signature.rbs +14 -0
- data/sig/nostr/user.rbs +4 -8
- data/sig/vendor/bech32/nostr/entity.rbs +41 -0
- data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
- data/sig/vendor/bech32/segwit_addr.rbs +21 -0
- data/sig/vendor/bech32.rbs +25 -0
- data/sig/vendor/event_emitter.rbs +10 -3
- data/sig/vendor/event_machine/channel.rbs +1 -1
- data/sig/vendor/faye/websocket/api.rbs +45 -0
- data/sig/vendor/faye/websocket/client.rbs +43 -0
- data/sig/vendor/faye/websocket.rbs +30 -0
- data/sig/vendor/schnorr/signature.rbs +16 -0
- data/sig/vendor/schnorr.rbs +3 -1
- 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
|
data/lib/nostr/errors.rb
ADDED
@@ -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
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
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 [
|
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 [
|
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
|
data/lib/nostr/event_kind.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
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 [
|
30
|
-
# @param recipient_public_key [
|
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
|
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
|
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
|
111
|
-
# @option kwargs [Array<String>, nil] authors A list of pubkeys
|
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 # =>
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
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
|
data/lib/nostr/key_pair.rb
CHANGED
@@ -10,7 +10,7 @@ module Nostr
|
|
10
10
|
# @example
|
11
11
|
# keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
|
12
12
|
#
|
13
|
-
# @return [
|
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 [
|
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 [
|
39
|
-
# @param 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.
|
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 [
|
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
|
-
# @
|
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
|
-
|
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
|