nostr_ruby 0.2.0 → 0.3.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.
data/lib/nostr_ruby.rb CHANGED
@@ -1,5 +1,6 @@
1
- require_relative 'custom_addr'
1
+ require_relative 'version'
2
2
  require_relative 'crypto_tools'
3
+
3
4
  require 'ecdsa'
4
5
  require 'schnorr'
5
6
  require 'json'
@@ -8,277 +9,16 @@ require 'bech32'
8
9
  require 'unicode/emoji'
9
10
  require 'websocket-client-simple'
10
11
 
11
- # * Ruby library to interact with the Nostr protocol
12
-
13
- class Nostr
14
- include CryptoTools
15
-
16
- attr_reader :private_key, :public_key, :pow_difficulty_target, :nip26_delegation_tag
17
-
18
- def initialize(key)
19
- hex_private_key = if key[:private_key]&.include?('nsec')
20
- Nostr.to_hex(key[:private_key])
21
- else
22
- key[:private_key]
23
- end
24
-
25
- hex_public_key = if key[:public_key]&.include?('npub')
26
- Nostr.to_hex(key[:public_key])
27
- else
28
- key[:public_key]
29
- end
30
-
31
- if hex_private_key
32
- @private_key = hex_private_key
33
- group = ECDSA::Group::Secp256k1
34
- @public_key = group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
35
- elsif hex_public_key
36
- @public_key = hex_public_key
37
- else
38
- raise 'Missing private or public key'
39
- end
40
- end
41
-
42
- def keys
43
- keys = { public_key: @public_key }
44
- keys[:private_key] = @private_key if @private_key
45
- keys
46
- end
47
-
48
- def bech32_keys
49
- bech32_keys = { public_key: Nostr.to_bech32(@public_key, 'npub') }
50
- bech32_keys[:private_key] = Nostr.to_bech32(@private_key, 'nsec') if @private_key
51
- bech32_keys
52
- end
53
-
54
- def self.to_hex(bech32_key)
55
- public_addr = CustomAddr.new(bech32_key)
56
- public_addr.to_scriptpubkey
57
- end
58
-
59
- def self.to_bech32(hex_key, hrp)
60
- custom_addr = CustomAddr.new
61
- custom_addr.scriptpubkey = hex_key
62
- custom_addr.hrp = hrp
63
- custom_addr.addr
64
- end
65
-
66
- def sign_event(event)
67
- raise 'Invalid pubkey' unless event[:pubkey].is_a?(String) && event[:pubkey].size == 64
68
- raise 'Invalid created_at' unless event[:created_at].is_a?(Integer)
69
- raise 'Invalid kind' unless (0..29_999).include?(event[:kind])
70
- raise 'Invalid tags' unless event[:tags].is_a?(Array)
71
- raise 'Invalid content' unless event[:content].is_a?(String)
72
-
73
- serialized_event = [
74
- 0,
75
- event[:pubkey],
76
- event[:created_at],
77
- event[:kind],
78
- event[:tags],
79
- event[:content]
80
- ]
81
-
82
- serialized_event_sha256 = nil
83
- if @pow_difficulty_target
84
- nonce = 1
85
- loop do
86
- nonce_tag = ['nonce', nonce.to_s, @pow_difficulty_target.to_s]
87
- nonced_serialized_event = serialized_event.clone
88
- nonced_serialized_event[4] = nonced_serialized_event[4] + [nonce_tag]
89
- serialized_event_sha256 = Digest::SHA256.hexdigest(JSON.dump(nonced_serialized_event))
90
- if match_pow_difficulty?(serialized_event_sha256)
91
- event[:tags] << nonce_tag
92
- break
93
- end
94
- nonce += 1
95
- end
96
- else
97
- serialized_event_sha256 = Digest::SHA256.hexdigest(JSON.dump(serialized_event))
98
- end
99
-
100
- private_key = Array(@private_key).pack('H*')
101
- message = Array(serialized_event_sha256).pack('H*')
102
- event_signature = Schnorr.sign(message, private_key).encode.unpack('H*')[0]
103
-
104
- event['id'] = serialized_event_sha256
105
- event['sig'] = event_signature
106
- event
107
- end
108
-
109
- def build_event(payload)
110
- if @nip26_delegation_tag
111
- payload[:tags] = [] unless payload[:tags]
112
- payload[:tags] << @nip26_delegation_tag
113
- end
114
- event = sign_event(payload)
115
- ['EVENT', event]
116
- end
117
-
118
- def build_metadata_event(name, about, picture, nip05)
119
- data = {}
120
- data[:name] = name if name
121
- data[:about] = about if about
122
- data[:picture] = picture if picture
123
- data[:nip05] = nip05 if nip05
124
- event = {
125
- "pubkey": @public_key,
126
- "created_at": Time.now.utc.to_i,
127
- "kind": 0,
128
- "tags": [],
129
- "content": data.to_json
130
- }
131
-
132
- build_event(event)
133
- end
134
-
135
- def build_note_event(text, channel_key = nil)
136
- event = {
137
- "pubkey": @public_key,
138
- "created_at": Time.now.utc.to_i,
139
- "kind": channel_key ? 42 : 1,
140
- "tags": channel_key ? [['e', channel_key]] : [],
141
- "content": text
142
- }
143
-
144
- build_event(event)
145
- end
146
-
147
- def build_recommended_relay_event(relay)
148
- raise 'Invalid relay' unless relay.start_with?('wss://') || relay.start_with?('ws://')
149
-
150
- event = {
151
- "pubkey": @public_key,
152
- "created_at": Time.now.utc.to_i,
153
- "kind": 2,
154
- "tags": [],
155
- "content": relay
156
- }
157
-
158
- build_event(event)
159
- end
160
-
161
- def build_contact_list_event(contacts)
162
- event = {
163
- "pubkey": @public_key,
164
- "created_at": Time.now.utc.to_i,
165
- "kind": 3,
166
- "tags": contacts.map { |c| ['p'] + c },
167
- "content": ''
168
- }
169
-
170
- build_event(event)
171
- end
172
-
173
- def build_dm_event(text, recipient_public_key)
174
- encrypted_text = CryptoTools.aes_256_cbc_encrypt(@private_key, recipient_public_key, text)
175
-
176
- event = {
177
- "pubkey": @public_key,
178
- "created_at": Time.now.utc.to_i,
179
- "kind": 4,
180
- "tags": [['p', recipient_public_key]],
181
- "content": encrypted_text
182
- }
183
-
184
- build_event(event)
185
- end
186
-
187
- def build_deletion_event(events, reason = '')
188
- event = {
189
- "pubkey": @public_key,
190
- "created_at": Time.now.utc.to_i,
191
- "kind": 5,
192
- "tags": events.map{ |e| ['e', e] },
193
- "content": reason
194
- }
195
-
196
- build_event(event)
197
- end
198
-
199
- def build_reaction_event(reaction, event, author)
200
- raise 'Invalid reaction' unless ['+', '-'].include?(reaction) || reaction.match?(Unicode::Emoji::REGEX)
201
- raise 'Invalid author' unless event.is_a?(String) && event.size == 64
202
- raise 'Invalid event' unless author.is_a?(String) && author.size == 64
203
-
204
- event = {
205
- "pubkey": @public_key,
206
- "created_at": Time.now.utc.to_i,
207
- "kind": 7,
208
- "tags": [['e', event], ['p', author]],
209
- "content": reaction
210
- }
211
-
212
- build_event(event)
213
- end
214
-
215
- def decrypt_dm(event)
216
- data = event[1]
217
- sender_public_key = data[:pubkey] != @public_key ? data[:pubkey] : data[:tags][0][1]
218
- encrypted = data[:content].split('?iv=')[0]
219
- iv = data[:content].split('?iv=')[1]
220
- CryptoTools.aes_256_cbc_decrypt(@private_key, sender_public_key, encrypted, iv)
221
- end
222
-
223
- def get_delegation_tag(delegatee_pubkey, conditions)
224
- delegation_message_sha256 = Digest::SHA256.hexdigest("nostr:delegation:#{delegatee_pubkey}:#{conditions}")
225
- signature = Schnorr.sign(Array(delegation_message_sha256).pack('H*'), Array(@private_key).pack('H*')).encode.unpack('H*')[0]
226
- [
227
- "delegation",
228
- @public_key,
229
- conditions,
230
- signature
231
- ]
232
- end
233
-
234
- def set_delegation(tag)
235
- @nip26_delegation_tag = tag
236
- end
237
-
238
- def reset_delegation
239
- @nip26_delegation_tag = nil
240
- end
241
-
242
- def self.verify_delegation_signature(delegatee_pubkey, tag)
243
- delegation_message_sha256 = Digest::SHA256.hexdigest("nostr:delegation:#{delegatee_pubkey}:#{tag[2]}")
244
- Schnorr.valid_sig?(Array(delegation_message_sha256).pack('H*'), Array(tag[1]).pack('H*'), Array(tag[3]).pack('H*'))
245
- end
246
-
247
- def build_req_event(filters)
248
- ['REQ', SecureRandom.random_number.to_s, filters]
249
- end
250
-
251
- def build_close_event(subscription_id)
252
- ['CLOSE', subscription_id]
253
- end
254
-
255
- def build_notice_event(message)
256
- ['NOTICE', message]
257
- end
258
-
259
- def match_pow_difficulty?(event_id)
260
- @pow_difficulty_target.nil? || @pow_difficulty_target == [event_id].pack("H*").unpack("B*")[0].index('1')
261
- end
262
-
263
- def set_pow_difficulty_target(n)
264
- @pow_difficulty_target = n
265
- end
266
-
267
- def test_post_event(event, relay)
268
- response = nil
269
- ws = WebSocket::Client::Simple.connect relay
270
- ws.on :message do |msg|
271
- puts msg
272
- response = JSON.parse(msg.data)
273
- ws.close
274
- end
275
- ws.on :open do
276
- ws.send event.to_json
277
- end
278
- while response.nil? do
279
- sleep 0.1
280
- end
281
- response[0] == 'OK'
282
- end
12
+ require_relative 'bech32'
13
+ require_relative 'context'
14
+ require_relative 'kind'
15
+ require_relative 'key'
16
+ require_relative 'event'
17
+ require_relative 'filter'
18
+ require_relative 'signer'
19
+ require_relative 'client'
20
+ require_relative 'message_handler'
21
+
22
+ module Nostr
283
23
 
284
24
  end
data/lib/signer.rb ADDED
@@ -0,0 +1,89 @@
1
+ module Nostr
2
+ class Signer
3
+
4
+ attr_reader :private_key
5
+ attr_reader :public_key
6
+
7
+ def initialize(private_key:)
8
+ @private_key = private_key
9
+ unless @public_key
10
+ @public_key = Nostr::Key::get_public_key(@private_key)
11
+ end
12
+ end
13
+
14
+ def nsec
15
+ Nostr::Bech32.encode_nsec(@private_key)
16
+ end
17
+
18
+ def npub
19
+ Nostr::Bech32.encode_npub(@public_key)
20
+ end
21
+
22
+ def sign(event)
23
+
24
+ raise ArgumentError, "Event is not signable" unless event.signable?
25
+
26
+ event.pubkey = @public_key if event.pubkey.nil? || event.pubkey.empty?
27
+
28
+ raise ArgumentError, "Pubkey doesn't match the private key" unless event.pubkey == @public_key
29
+
30
+ if event.kind == Nostr::Kind::DIRECT_MESSAGE
31
+ dm_recipient = event.tags.select{|t| t[0] == "p"}.first[1]
32
+ event.content = CryptoTools.aes_256_cbc_encrypt(@private_key, dm_recipient, event.content)
33
+ end
34
+
35
+ if event.delegation
36
+ event.tags << event.delegation
37
+ end
38
+
39
+ event_sha256_digest = nil
40
+ if event.pow
41
+ nonce = 1
42
+ loop do
43
+ nonce_tag = ['nonce', nonce.to_s, event.pow.to_s]
44
+ nonced_serialized_event = event.serialize.clone
45
+ nonced_serialized_event[4] = nonced_serialized_event[4] + [nonce_tag]
46
+ event_sha256_digest = Digest::SHA256.hexdigest(JSON.dump(nonced_serialized_event))
47
+ if Nostr::Event.match_pow_difficulty?(event_sha256_digest, event.pow)
48
+ event.tags << nonce_tag
49
+ break
50
+ end
51
+ nonce += 1
52
+ end
53
+ else
54
+ event_sha256_digest = Digest::SHA256.hexdigest(JSON.dump(event.serialize))
55
+ end
56
+
57
+ event.id = event_sha256_digest
58
+ binary_private_key = Array(@private_key).pack('H*')
59
+ binary_message = Array(event.id).pack('H*')
60
+ event.sig = Schnorr.sign(binary_message, binary_private_key).encode.unpack('H*')[0]
61
+ event
62
+ end
63
+
64
+ def decrypt(event)
65
+ case event.kind
66
+ when Nostr::Kind::DIRECT_MESSAGE
67
+ data = event.content.split('?iv=')[0]
68
+ iv = event.content.split('?iv=')[1]
69
+ dm_recipient = event.tags.select{|t| t[0] == "p"}.first[1]
70
+ event.content = CryptoTools.aes_256_cbc_decrypt(@private_key, dm_recipient, data, iv)
71
+ event
72
+ else
73
+ raise "Unable to decrypt a kind #{event.kind} event"
74
+ end
75
+ end
76
+
77
+ def generate_delegation_tag(to:, conditions:)
78
+ delegation_message_sha256 = Digest::SHA256.hexdigest("nostr:delegation:#{to}:#{conditions}")
79
+ signature = Schnorr.sign(Array(delegation_message_sha256).pack('H*'), Array(@private_key).pack('H*')).encode.unpack('H*')[0]
80
+ [
81
+ "delegation",
82
+ @public_key,
83
+ conditions,
84
+ signature
85
+ ]
86
+ end
87
+
88
+ end
89
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Nostr
2
+ VERSION = '0.3.0'
3
+ end
Binary file
data/nostr_ruby.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  $:.unshift File.expand_path('../lib', __FILE__)
2
- require 'nostr_ruby/version'
2
+ require 'version'
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = 'nostr_ruby'
6
- s.version = NostrRuby::VERSION
6
+ s.version = Nostr::VERSION
7
7
  s.summary = 'A Ruby library to interact with the Nostr protocol'
8
8
  s.description = 'NostrRuby is a Ruby library to interact with the Nostr protocol. At this stage the focus is the creation of public events and private encrypted messages.'
9
9
  s.authors = ['Daniele Tonon']
@@ -14,9 +14,9 @@ Gem::Specification.new do |s|
14
14
  s.require_paths = ['lib']
15
15
 
16
16
  s.add_dependency 'base64', '~> 0.1.1'
17
- s.add_dependency 'bech32', '~> 1.3.0'
17
+ s.add_dependency 'bech32', '~> 1.4.0'
18
18
  s.add_dependency 'bip-schnorr', '~> 0.4.0'
19
19
  s.add_dependency 'json', '~> 2.6.2'
20
20
  s.add_dependency 'unicode-emoji', '~> 3.3.1'
21
- s.add_dependency 'websocket-client-simple', '~> 0.6.0'
21
+ s.add_dependency 'faye-websocket', '~> 0.11'
22
22
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nostr_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniele Tonon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-14 00:00:00.000000000 Z
11
+ date: 2025-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 1.3.0
33
+ version: 1.4.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 1.3.0
40
+ version: 1.4.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bip-schnorr
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -81,19 +81,19 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: 3.3.1
83
83
  - !ruby/object:Gem::Dependency
84
- name: websocket-client-simple
84
+ name: faye-websocket
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.6.0
89
+ version: '0.11'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 0.6.0
96
+ version: '0.11'
97
97
  description: NostrRuby is a Ruby library to interact with the Nostr protocol. At this
98
98
  stage the focus is the creation of public events and private encrypted messages.
99
99
  email:
@@ -105,10 +105,20 @@ files:
105
105
  - Gemfile.lock
106
106
  - LICENSE.md
107
107
  - README.md
108
+ - lib/bech32.rb
109
+ - lib/client.rb
110
+ - lib/context.rb
108
111
  - lib/crypto_tools.rb
109
- - lib/custom_addr.rb
112
+ - lib/event.rb
113
+ - lib/event_wizard.rb
114
+ - lib/filter.rb
115
+ - lib/key.rb
116
+ - lib/kind.rb
117
+ - lib/message_handler.rb
110
118
  - lib/nostr_ruby.rb
111
- - lib/nostr_ruby/version.rb
119
+ - lib/signer.rb
120
+ - lib/version.rb
121
+ - nostr_ruby-0.2.0.gem
112
122
  - nostr_ruby.gemspec
113
123
  homepage: https://github.com/dtonon/nostr-ruby
114
124
  licenses:
data/lib/custom_addr.rb DELETED
@@ -1,59 +0,0 @@
1
- class CustomAddr
2
-
3
- attr_accessor :hrp # human-readable part
4
- attr_accessor :prog # witness program
5
-
6
- def initialize(addr = nil)
7
- @hrp, @prog = parse_addr(addr) if addr
8
- end
9
-
10
- def to_scriptpubkey
11
- prog.map{|p|[p].pack("C")}.join.unpack('H*').first
12
- end
13
-
14
- def scriptpubkey=(script)
15
- values = [script].pack('H*').unpack("C*")
16
- @prog = values
17
- end
18
-
19
- def addr
20
- spec = Bech32::Encoding::BECH32
21
- Bech32.encode(hrp, convert_bits(prog, 8, 5), spec)
22
- end
23
-
24
- private
25
-
26
- def parse_addr(addr)
27
- hrp, data, spec = Bech32.decode(addr)
28
- raise 'Invalid address.' if hrp.nil? || data[0].nil?
29
- # raise 'Invalid witness version' if ver > 16
30
- prog = convert_bits(data, 5, 8, false)
31
- # raise 'Invalid witness program' if prog.nil? || prog.length < 2 || prog.length > 40
32
- # raise 'Invalid witness program with version 0' if ver == 0 && (prog.length != 20 && prog.length != 32)
33
- [hrp, prog]
34
- end
35
-
36
- def convert_bits(data, from, to, padding=true)
37
- acc = 0
38
- bits = 0
39
- ret = []
40
- maxv = (1 << to) - 1
41
- max_acc = (1 << (from + to - 1)) - 1
42
- data.each do |v|
43
- return nil if v < 0 || (v >> from) != 0
44
- acc = ((acc << from) | v) & max_acc
45
- bits += from
46
- while bits >= to
47
- bits -= to
48
- ret << ((acc >> bits) & maxv)
49
- end
50
- end
51
- if padding
52
- ret << ((acc << (to - bits)) & maxv) unless bits == 0
53
- elsif bits >= from || ((acc << (to - bits)) & maxv) != 0
54
- return nil
55
- end
56
- ret
57
- end
58
-
59
- end
@@ -1,3 +0,0 @@
1
- module NostrRuby
2
- VERSION = '0.2.0'
3
- end