nostr_ruby 0.1.3 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c07cd32c02398c0b91710a693bfa2307bcdaa85690690612580e923761cb61ce
4
- data.tar.gz: c875468f38ea2c1695724975f281f5b2619a2908993d267670bde22a2e83d145
3
+ metadata.gz: 19e9c9f2d37d02877b0f4cdc83faeb46d09d707ee7b54d4ec938ee448fefc5bb
4
+ data.tar.gz: c01abc356c5bab165b6534eef10163c40be8c73632c2ce8b657c3ceb3e0cd1c3
5
5
  SHA512:
6
- metadata.gz: 004524d5d93169b21931e1e0639124b9861b376894fb7f6d489018658925576de39781f699e59177a3e968061aba596e771de5d139fb40b9be97860566bc0110
7
- data.tar.gz: f82ae191c0908480d514a6c897c6dd9381adfa35df84df1dacd3118989e09ed0361950b82aa87495c35f2f3d22a9b4c9bfce12f2d1649eb295bdcdab58741530
6
+ metadata.gz: 2021a0b34822aeb9eaa071fa96a6c48b8c3eb982eabbb2646daae076ddfc39957383dad49ecbde208e129d81af7a0489031afdd91ddfb8a69a011bba88178baa
7
+ data.tar.gz: 1959f99e9d18ca5fbe7fb0f5c41943f1981a980716c4871d94d9e23eb7d5c1e39cd0d0665f1bf51d2e21bd9f7b41c7630f7b15fcab583fa336cb4d0ab7d54adc
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nostr_ruby (0.1.3)
4
+ nostr_ruby (0.2.0)
5
5
  base64 (~> 0.1.1)
6
6
  bech32 (~> 1.3.0)
7
7
  bip-schnorr (~> 0.4.0)
data/README.md CHANGED
@@ -173,3 +173,41 @@ note = n.build_note_event("Hello Nostr!")
173
173
  # "id"=>"0000fb0c4563274e742e56d7d6de08684a2a25dfb52b79cccdb49c649dccbf45",
174
174
  # "sig"=>"838a1457c75084319e4723fbd9cbcf4c3311c466daf3908ffa114682094140e3b188996a73ae9fd3d3c6dbf08beecf9081b8d2bf0e60163b07cdf36a50dea1c0"}]
175
175
  ```
176
+
177
+ ### Create events with a NIP-26 delegation
178
+ ```ruby
179
+ from = Time.now.to_i
180
+ to = (Time.now + 60*60).to_i
181
+ delegatee_pubkey = "b1d8dfd69fe8795042dbbc4d3f85938a01d4740c54d2daf11088c75c50ff19d9"
182
+ conditions = "kind=1&created_at>#{from}&created_at<#{to}"
183
+ tag = n.get_delegation_tag(delegatee_pubkey, conditions)
184
+ n.set_delegation(tag)
185
+ n.build_note_event("I delegate someone to post this!")
186
+ #=>
187
+ #["EVENT",
188
+ # {:pubkey=>"1ed41a3ce33edfc580102abfbdc01d922f8c7697beee3e395aa7dcd7115a3372",
189
+ # :created_at=>1681503318,
190
+ # :kind=>1,
191
+ # :tags=>
192
+ # [["delegation",
193
+ # "1ed41a3ce33edfc580102abfbdc01d922f8c7697beee3e395aa7dcd7115a3372",
194
+ # "kind=1&created_at>1681503305&created_at<1681506905",
195
+ # "dbbdc7074a5c2d2a53ee174c43afb0bb03106ced866e0dfa7996fe2553a54aaa9af6affe915ec2e688e26357681bfcf0445d607ed85eca2217f79fb51094d816"]],
196
+ # :content=>"I delegate someone to post this!",
197
+ # "id"=>"f6fd20535db6748e0529052310c47dc788616b03f8cef20260ca6a2dfb5dedaf",
198
+ # "sig"=>"826d7bd1f369f6ab8330746c236437610f13205809d12bc94af287b7ceec180443a3a750267d5152323329a1b2d02d0702364fa3ee8228bab57016b39975e449"}]
199
+ ```
200
+
201
+ ### Verify a NIP-26 delegation
202
+ ```ruby
203
+ delegatee_pubkey = "b1d8dfd69fe8795042dbbc4d3f85938a01d4740c54d2daf11088c75c50ff19d9"
204
+ tag = ["delegation", delegator_pubkey, conditions, signature]
205
+ Nostr.verify_delegation_signature(delegatee_pubkey, tag)
206
+ #=> true
207
+ ```
208
+
209
+ ### Reset the NIP-26 delegation
210
+ ```ruby
211
+ n.reset_delegation
212
+ #=> nil
213
+ ```
@@ -0,0 +1,31 @@
1
+ module CryptoTools
2
+
3
+ def self.calculate_shared_key(priv_key_a, pub_key_b)
4
+ ec = OpenSSL::PKey::EC.new('secp256k1')
5
+ ec.private_key = OpenSSL::BN.new(priv_key_a, 16)
6
+ pub_key_hex = "02#{pub_key_b}"
7
+ pub_key_bn = OpenSSL::BN.new(pub_key_hex, 16)
8
+ secret_point = OpenSSL::PKey::EC::Point.new(ec.group, pub_key_bn)
9
+ ec.dh_compute_key(secret_point)
10
+ end
11
+
12
+ def self.aes_256_cbc_encrypt(priv_key_a, pub_key_b, payload)
13
+ cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
14
+ cipher.encrypt
15
+ cipher.iv = iv = cipher.random_iv
16
+ cipher.key = calculate_shared_key(priv_key_a, pub_key_b)
17
+ encrypted_text = cipher.update(payload)
18
+ encrypted_text << cipher.final
19
+ encrypted_text = "#{Base64.encode64(encrypted_text)}?iv=#{Base64.encode64(iv)}"
20
+ encrypted_text.gsub("\n", '')
21
+ end
22
+
23
+ def self.aes_256_cbc_decrypt(priv_key_a, pub_key_b, payload, iv)
24
+ cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
25
+ cipher.decrypt
26
+ cipher.iv = Base64.decode64(iv)
27
+ cipher.key = calculate_shared_key(priv_key_a, pub_key_b)
28
+ (cipher.update(Base64.decode64(payload)) + cipher.final).force_encoding('UTF-8')
29
+ end
30
+
31
+ end
@@ -1,3 +1,3 @@
1
1
  module NostrRuby
2
- VERSION = '0.1.3'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/nostr_ruby.rb CHANGED
@@ -1,4 +1,5 @@
1
- require 'custom_addr'
1
+ require_relative 'custom_addr'
2
+ require_relative 'crypto_tools'
2
3
  require 'ecdsa'
3
4
  require 'schnorr'
4
5
  require 'json'
@@ -10,7 +11,9 @@ require 'websocket-client-simple'
10
11
  # * Ruby library to interact with the Nostr protocol
11
12
 
12
13
  class Nostr
13
- attr_reader :private_key, :public_key, :pow_difficulty_target
14
+ include CryptoTools
15
+
16
+ attr_reader :private_key, :public_key, :pow_difficulty_target, :nip26_delegation_tag
14
17
 
15
18
  def initialize(key)
16
19
  hex_private_key = if key[:private_key]&.include?('nsec')
@@ -60,15 +63,6 @@ class Nostr
60
63
  custom_addr.addr
61
64
  end
62
65
 
63
- def calculate_shared_key(other_public_key)
64
- ec = OpenSSL::PKey::EC.new('secp256k1')
65
- ec.private_key = OpenSSL::BN.new(@private_key, 16)
66
- recipient_key_hex = "02#{other_public_key}"
67
- recipient_pub_bn = OpenSSL::BN.new(recipient_key_hex, 16)
68
- secret_point = OpenSSL::PKey::EC::Point.new(ec.group, recipient_pub_bn)
69
- ec.dh_compute_key(secret_point)
70
- end
71
-
72
66
  def sign_event(event)
73
67
  raise 'Invalid pubkey' unless event[:pubkey].is_a?(String) && event[:pubkey].size == 64
74
68
  raise 'Invalid created_at' unless event[:created_at].is_a?(Integer)
@@ -113,6 +107,10 @@ class Nostr
113
107
  end
114
108
 
115
109
  def build_event(payload)
110
+ if @nip26_delegation_tag
111
+ payload[:tags] = [] unless payload[:tags]
112
+ payload[:tags] << @nip26_delegation_tag
113
+ end
116
114
  event = sign_event(payload)
117
115
  ['EVENT', event]
118
116
  end
@@ -131,8 +129,7 @@ class Nostr
131
129
  "content": data.to_json
132
130
  }
133
131
 
134
- event = sign_event(event)
135
- ['EVENT', event]
132
+ build_event(event)
136
133
  end
137
134
 
138
135
  def build_note_event(text, channel_key = nil)
@@ -144,8 +141,7 @@ class Nostr
144
141
  "content": text
145
142
  }
146
143
 
147
- event = sign_event(event)
148
- ['EVENT', event]
144
+ build_event(event)
149
145
  end
150
146
 
151
147
  def build_recommended_relay_event(relay)
@@ -159,8 +155,7 @@ class Nostr
159
155
  "content": relay
160
156
  }
161
157
 
162
- event = sign_event(event)
163
- ['EVENT', event]
158
+ build_event(event)
164
159
  end
165
160
 
166
161
  def build_contact_list_event(contacts)
@@ -172,19 +167,11 @@ class Nostr
172
167
  "content": ''
173
168
  }
174
169
 
175
- event = sign_event(event)
176
- ['EVENT', event]
170
+ build_event(event)
177
171
  end
178
172
 
179
173
  def build_dm_event(text, recipient_public_key)
180
- cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
181
- cipher.encrypt
182
- cipher.iv = iv = cipher.random_iv
183
- cipher.key = calculate_shared_key(recipient_public_key)
184
- encrypted_text = cipher.update(text)
185
- encrypted_text << cipher.final
186
- encrypted_text = "#{Base64.encode64(encrypted_text)}?iv=#{Base64.encode64(iv)}"
187
- encrypted_text = encrypted_text.gsub("\n", '')
174
+ encrypted_text = CryptoTools.aes_256_cbc_encrypt(@private_key, recipient_public_key, text)
188
175
 
189
176
  event = {
190
177
  "pubkey": @public_key,
@@ -194,8 +181,7 @@ class Nostr
194
181
  "content": encrypted_text
195
182
  }
196
183
 
197
- event = sign_event(event)
198
- ['EVENT', event]
184
+ build_event(event)
199
185
  end
200
186
 
201
187
  def build_deletion_event(events, reason = '')
@@ -207,8 +193,7 @@ class Nostr
207
193
  "content": reason
208
194
  }
209
195
 
210
- event = sign_event(event)
211
- ['EVENT', event]
196
+ build_event(event)
212
197
  end
213
198
 
214
199
  def build_reaction_event(reaction, event, author)
@@ -224,8 +209,7 @@ class Nostr
224
209
  "content": reaction
225
210
  }
226
211
 
227
- event = sign_event(event)
228
- ['EVENT', event]
212
+ build_event(event)
229
213
  end
230
214
 
231
215
  def decrypt_dm(event)
@@ -233,11 +217,31 @@ class Nostr
233
217
  sender_public_key = data[:pubkey] != @public_key ? data[:pubkey] : data[:tags][0][1]
234
218
  encrypted = data[:content].split('?iv=')[0]
235
219
  iv = data[:content].split('?iv=')[1]
236
- cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
237
- cipher.decrypt
238
- cipher.iv = Base64.decode64(iv)
239
- cipher.key = calculate_shared_key(sender_public_key)
240
- (cipher.update(Base64.decode64(encrypted)) + cipher.final).force_encoding('UTF-8')
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*'))
241
245
  end
242
246
 
243
247
  def build_req_event(filters)
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.1.3
4
+ version: 0.2.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-03-10 00:00:00.000000000 Z
11
+ date: 2023-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -105,6 +105,7 @@ files:
105
105
  - Gemfile.lock
106
106
  - LICENSE.md
107
107
  - README.md
108
+ - lib/crypto_tools.rb
108
109
  - lib/custom_addr.rb
109
110
  - lib/nostr_ruby.rb
110
111
  - lib/nostr_ruby/version.rb