nostr 0.3.0 → 0.4.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: 371ed11c0474fd944cc55a054d553945623f7439f67c55f3eadc564805d2fb11
4
- data.tar.gz: f7fbe86119bd7816e7066ea3603263dee7327b06daeb4b92f9e90977f52ef8ae
3
+ metadata.gz: cd56d59d68235fe8fa4bceb67df4a7e83d4b6a8295ed31cc0152002800ad7d23
4
+ data.tar.gz: 56b70ada7f9fd6cd29be1c7bc23b7a23adc49bfe2f8d8de48300f70709626a3e
5
5
  SHA512:
6
- metadata.gz: fa602354304ce9e77377b80cab60146f51ac8943b1399070a87ce81a5e44582f0a23b50f1736fafd9d7d0f13042b8b7292b9d29684077ad878e2439ddf7970ef
7
- data.tar.gz: 83fc3d535a1e35438522779ef7dc3e101b4fea3a3f9874db9457b4f6c61d74469ded6ba973923d731789d3c50d7aa66b1c0770d62451794d45b52720c7928ebf
6
+ metadata.gz: be113a67cd651739cc3bac1deb4bba30e3598cfd787e8971a764e82d58c03ad1c918d3edd14630bbb1a5e2c6504c3c344859f0449a1774df7bb7bf0d3c38f537
7
+ data.tar.gz: c80bbf1f4d3caa25f48e1630b650587a2319197d7edba33e824b17e93f6355d8593c41364ad1fdb86a834d8cae63ad9f15e8feacae953b0ff8d7294f09c2c765
data/.rubocop.yml CHANGED
@@ -31,6 +31,9 @@ Metrics/BlockLength:
31
31
  - '**/*_spec.rb'
32
32
  - nostr.gemspec
33
33
 
34
+ Metrics/ParameterLists:
35
+ CountKeywordArgs: false
36
+
34
37
  # ----------------------- RSpec -----------------------
35
38
 
36
39
  RSpec/ExampleLength:
data/CHANGELOG.md CHANGED
@@ -4,11 +4,40 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.4.0] - 2023-02-25
8
+
9
+ ### Removed
10
+
11
+ - Removed `EventFragment` class. The `Event` class is no longer a Value Object. In other words, it is no longer
12
+ immutable and it may be invalid by not having attributes `id` or `sig`. The `EventFragment` abstraction, along with the
13
+ principles of immutability and was a major source of internal complexity as I needed to scale the codebase.
14
+
15
+ ### Added
16
+
17
+ - Client compliance with [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) (encrypted direct messages)
18
+ - Extracted the cryptographic concerns into a `Crypto` class.
19
+ - Added the setters `Event#id=` and `Event#sig=`
20
+ - Added a method on the event class to sign events (`Event#sign`)
21
+ - Added a missing test for `EventKind::CONTACT_LIST`
22
+ - Added two convenience methods to append event and pubkey references to an event's tags `add_event_reference` and
23
+ `add_pubkey_reference`
24
+
25
+ ### Fixed
26
+
27
+ - Fixed the generation of public keys
28
+ - Fixed the RBS signature of `User#create_event`
29
+
7
30
  ## [0.3.0] - 2023-02-15
8
31
 
9
32
  ### Added
10
33
 
11
34
  - Client compliance wth [NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md) (manage contact lists)
35
+ - RBS type checking using Steep and TypeProf
36
+
37
+ ## Fixed
38
+
39
+ - Fixed a documentation typo
40
+ - Fixed a documentation error regarding the receiving of messages via websockets
12
41
 
13
42
  ## [0.2.0] - 2023-01-12
14
43
 
data/README.md CHANGED
@@ -7,6 +7,27 @@
7
7
  Asynchronous Nostr client. Please note that the API is likely to change as the gem is still in development and
8
8
  has not yet reached a stable release. Use with caution.
9
9
 
10
+ ## Table of contents
11
+
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ * [Requiring the gem](#requiring-the-gem)
15
+ * [Generating a keypair](#generating-a-keypair)
16
+ * [Generating a private key and a public key](#generating-a-private-key-and-a-public-key)
17
+ * [Connecting to a Relay](#connecting-to-a-relay)
18
+ * [WebSocket events](#websocket-events)
19
+ * [Requesting for events / creating a subscription](#requesting-for-events--creating-a-subscription)
20
+ * [Stop previous subscriptions](#stop-previous-subscriptions)
21
+ * [Publishing an event](#publishing-an-event)
22
+ * [Creating/updating your contact list](#creatingupdating-your-contact-list)
23
+ * [Sending an encrypted direct message](#sending-an-encrypted-direct-message)
24
+ - [Implemented NIPs](#implemented-nips)
25
+ - [Development](#development)
26
+ * [Type checking](#type-checking)
27
+ - [Contributing](#contributing)
28
+ - [License](#license)
29
+ - [Code of Conduct](#code-of-conduct)
30
+
10
31
  ## Installation
11
32
 
12
33
  Install the gem and add to the application's Gemfile by executing:
@@ -31,7 +52,7 @@ require 'nostr'
31
52
 
32
53
  ```ruby
33
54
  keygen = Nostr::Keygen.new
34
- keypair = keygen.generate_keypair
55
+ keypair = keygen.generate_key_pair
35
56
 
36
57
  keypair.private_key
37
58
  keypair.public_key
@@ -220,10 +241,38 @@ update_contacts_event = user.create_event(
220
241
  client.publish(update_contacts_event)
221
242
  ```
222
243
 
223
- ## NIPS
244
+ ### Sending an encrypted direct message
245
+
246
+ ```ruby
247
+ sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
248
+
249
+ encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
250
+ sender_private_key: sender_private_key,
251
+ recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
252
+ plain_text: 'Your feedback is appreciated, now pay $8',
253
+ previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
254
+ )
255
+
256
+ encrypted_direct_message.sign(sender_private_key)
257
+
258
+ # #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
259
+ # @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
260
+ # @created_at=1676456512,
261
+ # @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
262
+ # @kind=4,
263
+ # @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
264
+ # @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
265
+ # @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
266
+
267
+ # Send it to the Relay
268
+ client.publish(encrypted_direct_message)
269
+ ````
270
+
271
+ ## Implemented NIPs
224
272
 
225
273
  - [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
226
274
  - [x] [NIP-02 - Client](https://github.com/nostr-protocol/nips/blob/master/02.md)
275
+ - [x] [NIP-04 - Client](https://github.com/nostr-protocol/nips/blob/master/04.md)
227
276
 
228
277
  ## Development
229
278
 
data/Steepfile CHANGED
@@ -6,7 +6,9 @@ target :lib do
6
6
  check 'lib'
7
7
 
8
8
  # Core libraries
9
+ library 'base64'
9
10
  library 'digest'
11
+ library 'openssl'
10
12
  library 'securerandom'
11
13
 
12
14
  # Gems
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Performs cryptographic operations on a +Nostr::Event+.
5
+ class Crypto
6
+ # Numeric base of the OpenSSL big number used in an event content's encryption.
7
+ #
8
+ # @return [Integer]
9
+ #
10
+ BN_BASE = 16
11
+
12
+ # Name of the cipher curve used in an event content's encryption.
13
+ #
14
+ # @return [String]
15
+ #
16
+ CIPHER_CURVE = 'secp256k1'
17
+
18
+ # Name of the cipher algorithm used in an event content's encryption.
19
+ #
20
+ # @return [String]
21
+ #
22
+ CIPHER_ALGORITHM = 'aes-256-cbc'
23
+
24
+ # Encrypts a piece of text
25
+ #
26
+ # @api public
27
+ #
28
+ # @example Encrypting an event's content
29
+ # crypto = Nostr::Crypto.new
30
+ # encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8')
31
+ # encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
32
+ #
33
+ # @param sender_private_key [String] 32-bytes hex-encoded private key of the creator.
34
+ # @param recipient_public_key [String] 32-bytes hex-encoded public key of the recipient.
35
+ # @param plain_text [String] The text to be encrypted
36
+ #
37
+ # @return [String] Encrypted text.
38
+ #
39
+ def encrypt_text(sender_private_key, recipient_public_key, plain_text)
40
+ cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).encrypt
41
+ cipher.iv = iv = cipher.random_iv
42
+ cipher.key = compute_shared_key(sender_private_key, recipient_public_key)
43
+ encrypted_text = cipher.update(plain_text) + cipher.final
44
+ encrypted_text = "#{Base64.encode64(encrypted_text)}?iv=#{Base64.encode64(iv)}"
45
+ encrypted_text.gsub("\n", '')
46
+ end
47
+
48
+ # Decrypts a piece of text
49
+ #
50
+ # @api public
51
+ #
52
+ # @example Encrypting an event's content
53
+ # crypto = Nostr::Crypto.new
54
+ # encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
55
+ # decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted)
56
+ #
57
+ # @param sender_public_key [String] 32-bytes hex-encoded public key of the message creator.
58
+ # @param recipient_private_key [String] 32-bytes hex-encoded public key of the recipient.
59
+ # @param encrypted_text [String] The text to be decrypted
60
+ #
61
+ # @return [String] Decrypted text.
62
+ #
63
+ def decrypt_text(recipient_private_key, sender_public_key, encrypted_text)
64
+ base64_encoded_text, iv = encrypted_text.split('?iv=')
65
+ cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
66
+ cipher.iv = Base64.decode64(iv)
67
+ cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
68
+ plain_text = cipher.update(Base64.decode64(base64_encoded_text)) + cipher.final
69
+ plain_text.force_encoding('UTF-8')
70
+ end
71
+
72
+ # Uses the private key to generate an event id and sign the event
73
+ #
74
+ # @api public
75
+ #
76
+ # @example Signing an event
77
+ # crypto = Nostr::Crypto.new
78
+ # crypto.sign(event, private_key)
79
+ # event.id # => an id
80
+ # event.sig # => a signature
81
+ #
82
+ # @param event [Event] The event to be signed
83
+ # @param private_key [String] 32-bytes hex-encoded private key.
84
+ #
85
+ # @return [Event] An unsigned event.
86
+ #
87
+ def sign_event(event, private_key)
88
+ event_digest = hash_event(event)
89
+
90
+ hex_private_key = Array(private_key).pack('H*')
91
+ hex_message = Array(event_digest).pack('H*')
92
+ event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
93
+
94
+ event.id = event_digest
95
+ event.sig = event_signature
96
+
97
+ event
98
+ end
99
+
100
+ private
101
+
102
+ # Finds a shared key between two keys
103
+ #
104
+ # @api private
105
+ #
106
+ # @param private_key [String] 32-bytes hex-encoded private key.
107
+ # @param public_key [String] 32-bytes hex-encoded public key.
108
+ #
109
+ # @return [String] A shared key used in the event's content encryption and decryption.
110
+ #
111
+ def compute_shared_key(private_key, public_key)
112
+ group = OpenSSL::PKey::EC::Group.new(CIPHER_CURVE)
113
+
114
+ private_key_bn = OpenSSL::BN.new(private_key, BN_BASE)
115
+ public_key_bn = OpenSSL::BN.new("02#{public_key}", BN_BASE)
116
+ public_key_point = OpenSSL::PKey::EC::Point.new(group, public_key_bn)
117
+
118
+ asn1 = OpenSSL::ASN1::Sequence(
119
+ [
120
+ OpenSSL::ASN1::Integer.new(1),
121
+ OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)),
122
+ OpenSSL::ASN1::ObjectId(CIPHER_CURVE, 0, :EXPLICIT),
123
+ OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT)
124
+ ]
125
+ )
126
+
127
+ pkey = OpenSSL::PKey::EC.new(asn1.to_der)
128
+ pkey.dh_compute_key(public_key_point)
129
+ end
130
+
131
+ # Generates a SHA256 hash of a +Nostr::Event+
132
+ #
133
+ # @api private
134
+ #
135
+ # @param event [Event] The event to be hashed
136
+ #
137
+ # @return [String] A SHA256 digest of the event
138
+ #
139
+ def hash_event(event)
140
+ Digest::SHA256.hexdigest(JSON.dump(event.serialize))
141
+ end
142
+ end
143
+ end
data/lib/nostr/event.rb CHANGED
@@ -2,7 +2,65 @@
2
2
 
3
3
  module Nostr
4
4
  # The only object type that exists in Nostr is an event. Events are immutable.
5
- class Event < EventFragment
5
+ class Event
6
+ # 32-bytes hex-encoded public key of the event creator
7
+ #
8
+ # @api public
9
+ #
10
+ # @example
11
+ # event.pubkey # => '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
12
+ #
13
+ # @return [String]
14
+ #
15
+ attr_reader :pubkey
16
+
17
+ # Date of the creation of the vent. A UNIX timestamp, in seconds
18
+ #
19
+ # @api public
20
+ #
21
+ # @example
22
+ # event.created_at # => 1230981305
23
+ #
24
+ # @return [Integer]
25
+ #
26
+ attr_reader :created_at
27
+
28
+ # The kind of the event. An integer from 0 to 3
29
+ #
30
+ # @api public
31
+ #
32
+ # @example
33
+ # event.kind # => 1
34
+ #
35
+ # @return [Integer]
36
+ #
37
+ attr_reader :kind
38
+
39
+ # An array of tags. Each tag is an array of strings
40
+ #
41
+ # @api public
42
+ #
43
+ # @example Tags referencing an event
44
+ # event.tags #=> [["e", "event_id", "relay URL"]]
45
+ #
46
+ # @example Tags referencing a key
47
+ # event.tags #=> [["p", "event_id", "relay URL"]]
48
+ #
49
+ # @return [Array<Array>]
50
+ #
51
+ attr_reader :tags
52
+
53
+ # An arbitrary string
54
+ #
55
+ # @api public
56
+ #
57
+ # @example
58
+ # event.content # => 'Your feedback is appreciated, now pay $8'
59
+ #
60
+ # @return [String]
61
+ #
62
+ attr_reader :content
63
+
6
64
  # 32-bytes sha256 of the the serialized event data.
7
65
  # To obtain the event.id, we sha256 the serialized event. The serialization is done over the UTF-8 JSON-serialized
8
66
  # string (with no white space or line breaks)
@@ -12,9 +70,13 @@ module Nostr
12
70
  # @example Getting the event id
13
71
  # event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
14
72
  #
15
- # @return [String]
73
+ # @example Setting the event id
74
+ # event.id = 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
75
+ # event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
76
+ #
77
+ # @return [String|nil]
16
78
  #
17
- attr_reader :id
79
+ attr_accessor :id
18
80
 
19
81
  # 64-bytes signature of the sha256 hash of the serialized event data, which is
20
82
  # the same as the "id" field
@@ -24,9 +86,13 @@ module Nostr
24
86
  # @example Getting the event signature
25
87
  # event.sig # => ''
26
88
  #
27
- # @return [String]
89
+ # @example Setting the event signature
90
+ # event.sig = '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
91
+ # event.sig # => '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
92
+ #
93
+ # @return [String|nil]
28
94
  #
29
- attr_reader :sig
95
+ attr_accessor :sig
30
96
 
31
97
  # Instantiates a new Event
32
98
  #
@@ -44,16 +110,96 @@ module Nostr
44
110
  # 937c6d6e9855477638f5655c5d89c9aa5501ea9b578a66aced4f1cd7b3'
45
111
  # )
46
112
  #
47
- #
48
- # @param id [String] 32-bytes sha256 of the the serialized event data.
49
- # @param sig [String] 64-bytes signature of the sha256 hash of the serialized event data, which is
113
+ # @param id [String|nil] 32-bytes sha256 of the the serialized event data.
114
+ # @param sig [String|nil] 64-bytes signature of the sha256 hash of the serialized event data, which is
50
115
  # the same as the "id" field
51
- #
52
- def initialize(id:, sig:, **kwargs)
53
- super(**kwargs)
116
+ # @param pubkey [String] 32-bytes hex-encoded public key of the event creator.
117
+ # @param created_at [Integer] Date of the creation of the vent. A UNIX timestamp, in seconds.
118
+ # @param kind [Integer] The kind of the event. An integer from 0 to 3.
119
+ # @param tags [Array<Array>] An array of tags. Each tag is an array of strings.
120
+ # @param content [String] Arbitrary string.
121
+ #
122
+ def initialize(
123
+ pubkey:,
124
+ kind:,
125
+ content:,
126
+ created_at: Time.now.to_i,
127
+ tags: [],
128
+ id: nil,
129
+ sig: nil
130
+ )
54
131
 
55
132
  @id = id
56
133
  @sig = sig
134
+ @pubkey = pubkey
135
+ @created_at = created_at
136
+ @kind = kind
137
+ @tags = tags
138
+ @content = content
139
+ end
140
+
141
+ # Adds a reference to an event id as an 'e' tag
142
+ #
143
+ # @api public
144
+ #
145
+ # @example Adding a reference to a pubkey
146
+ # event_id = '189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408'
147
+ # event.add_event_reference(event_id)
148
+ #
149
+ # @param event_id [String] 32-bytes hex-encoded event id.
150
+ #
151
+ # @return [Array<String>] The event's updated list of tags
152
+ #
153
+ def add_event_reference(event_id) = tags.push(['e', event_id])
154
+
155
+ # Adds a reference to a pubkey as a 'p' tag
156
+ #
157
+ # @api public
158
+ #
159
+ # @example Adding a reference to a pubkey
160
+ # pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
161
+ # event.add_pubkey_reference(pubkey)
162
+ #
163
+ # @param pubkey [String] 32-bytes hex-encoded public key.
164
+ #
165
+ # @return [Array<String>] The event's updated list of tags
166
+ #
167
+ def add_pubkey_reference(pubkey) = tags.push(['p', pubkey])
168
+
169
+ # Signs an event with the user's private key
170
+ #
171
+ # @api public
172
+ #
173
+ # @example Signing an event
174
+ # event.sign(private_key)
175
+ #
176
+ # @param private_key [String] 32-bytes hex-encoded private key.
177
+ #
178
+ # @return [Event] A signed event.
179
+ #
180
+ def sign(private_key)
181
+ crypto = Crypto.new
182
+ crypto.sign_event(self, private_key)
183
+ end
184
+
185
+ # Serializes the event, to obtain a SHA256 digest of it
186
+ #
187
+ # @api public
188
+ #
189
+ # @example Converting the event to a digest
190
+ # event.serialize
191
+ #
192
+ # @return [Array] The event as an array.
193
+ #
194
+ def serialize
195
+ [
196
+ 0,
197
+ pubkey,
198
+ created_at,
199
+ kind,
200
+ tags,
201
+ content
202
+ ]
57
203
  end
58
204
 
59
205
  # Converts the event to a hash
@@ -31,5 +31,13 @@ module Nostr
31
31
  # @return [Integer]
32
32
  #
33
33
  CONTACT_LIST = 3
34
+
35
+ # A special event with kind 4, meaning "encrypted direct message". An event of this kind has its +content+
36
+ # equal to the base64-encoded, aes-256-cbc encrypted string of anything a user wants to write, encrypted using a
37
+ # shared cipher generated by combining the recipient's public-key with the sender's private-key.
38
+ #
39
+ # @return [Integer]
40
+ #
41
+ ENCRYPTED_DIRECT_MESSAGE = 4
34
42
  end
35
43
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Classes of event kinds.
5
+ module Events
6
+ # An event whose +content+ is encrypted. It can only be decrypted by the owner of the private key that pairs
7
+ # the event's +pubkey+.
8
+ class EncryptedDirectMessage < Event
9
+ # Instantiates a new encrypted direct message
10
+ #
11
+ # @api public
12
+ #
13
+ # @example Instantiating a new encrypted direct message
14
+ # Nostr::Events::EncryptedDirectMessage.new(
15
+ # sender_private_key: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
16
+ # recipient_public_key: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
17
+ # plain_text: 'Your feedback is appreciated, now pay $8',
18
+ # )
19
+ #
20
+ # @example Instantiating a new encrypted direct message that references a previous direct message
21
+ # Nostr::Events::EncryptedDirectMessage.new(
22
+ # sender_private_key: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
23
+ # recipient_public_key: '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e',
24
+ # plain_text: 'Your feedback is appreciated, now pay $8',
25
+ # previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
26
+ # )
27
+ #
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.
31
+ # @param previous_direct_message [String] 32-bytes hex-encoded id identifying the previous message in a
32
+ # conversation or a message we are explicitly replying to (such that contextual, more organized conversations
33
+ # may happen
34
+ #
35
+ def initialize(plain_text:, sender_private_key:, recipient_public_key:, previous_direct_message: nil)
36
+ crypto = Crypto.new
37
+ keygen = Keygen.new
38
+
39
+ encrypted_content = crypto.encrypt_text(sender_private_key, recipient_public_key, plain_text)
40
+ sender_public_key = keygen.extract_public_key(sender_private_key)
41
+
42
+ super(
43
+ pubkey: sender_public_key,
44
+ kind: Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE,
45
+ content: encrypted_content,
46
+ )
47
+
48
+ add_pubkey_reference(recipient_public_key)
49
+ add_event_reference(previous_direct_message) if previous_direct_message
50
+ end
51
+ end
52
+ end
53
+ end
data/lib/nostr/keygen.rb CHANGED
@@ -62,7 +62,7 @@ module Nostr
62
62
  # @return [String] A 32-bytes hex-encoded public key.
63
63
  #
64
64
  def extract_public_key(private_key)
65
- group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16)
65
+ group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16).rjust(64, '0')
66
66
  end
67
67
 
68
68
  private
data/lib/nostr/user.rb CHANGED
@@ -57,36 +57,8 @@ module Nostr
57
57
  # @return [Event]
58
58
  #
59
59
  def create_event(event_attributes)
60
- event_fragment = EventFragment.new(**event_attributes.merge(pubkey: keypair.public_key))
61
- event_sha256 = Digest::SHA256.hexdigest(JSON.dump(event_fragment.serialize))
62
-
63
- signature = sign(event_sha256)
64
-
65
- Event.new(
66
- id: event_sha256,
67
- pubkey: event_fragment.pubkey,
68
- created_at: event_fragment.created_at,
69
- kind: event_fragment.kind,
70
- tags: event_fragment.tags,
71
- content: event_fragment.content,
72
- sig: signature
73
- )
74
- end
75
-
76
- private
77
-
78
- # Signs an event with the user's private key
79
- #
80
- # @api private
81
- #
82
- # @param event_sha256 [String] The SHA256 hash of the event.
83
- #
84
- # @return [String] The signature of the event.
85
- #
86
- def sign(event_sha256)
87
- hex_private_key = Array(keypair.private_key).pack('H*')
88
- hex_message = Array(event_sha256).pack('H*')
89
- Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
60
+ event = Event.new(**event_attributes.merge(pubkey: keypair.public_key))
61
+ event.sign(keypair.private_key)
90
62
  end
91
63
  end
92
64
  end
data/lib/nostr/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Nostr
4
4
  # The version of the gem
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  end
data/lib/nostr.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'nostr/crypto'
3
4
  require_relative 'nostr/version'
4
5
  require_relative 'nostr/keygen'
5
6
  require_relative 'nostr/client_message_type'
@@ -8,8 +9,8 @@ require_relative 'nostr/subscription'
8
9
  require_relative 'nostr/relay'
9
10
  require_relative 'nostr/key_pair'
10
11
  require_relative 'nostr/event_kind'
11
- require_relative 'nostr/event_fragment'
12
12
  require_relative 'nostr/event'
13
+ require_relative 'nostr/events/encrypted_direct_message'
13
14
  require_relative 'nostr/client'
14
15
  require_relative 'nostr/user'
15
16
 
@@ -0,0 +1,16 @@
1
+ module Nostr
2
+ class Crypto
3
+ BN_BASE: Integer
4
+ CIPHER_CURVE: String
5
+ CIPHER_ALGORITHM: String
6
+
7
+ def encrypt_text: (String, String, String) -> String
8
+ def decrypt_text: (String, String, String) -> String
9
+ def sign_event: (Event, String) -> Event
10
+
11
+ private
12
+
13
+ def compute_shared_key: (String, String) -> String
14
+ def hash_event:(Event) -> String
15
+ end
16
+ end
data/sig/nostr/event.rbs CHANGED
@@ -1,24 +1,39 @@
1
1
  module Nostr
2
- class Event < EventFragment
3
- attr_reader id: String
4
- attr_reader sig: String
2
+ class Event
3
+ attr_reader pubkey: String
4
+ attr_reader created_at: Integer
5
+ attr_reader kind: Integer
6
+ attr_reader tags: Array[Array[String]]
7
+ attr_reader content: String
8
+ attr_accessor id: String?|nil
9
+ attr_accessor sig: String?|nil
5
10
 
6
- def initialize: (id: String, sig: String,
7
- created_at: Integer,
11
+ def initialize: (
12
+ pubkey: String,
8
13
  kind: Integer,
9
- tags: Array[String],
10
14
  content: String,
15
+ ?created_at: Integer,
16
+ ?tags: Array[Array[String]],
17
+ ?id: String|nil,
18
+ ?sig: String|nil
11
19
  ) -> void
12
20
 
21
+ def serialize: -> [Integer, String, Integer, Integer, Array[Array[String]], String]
22
+
13
23
  def to_h: -> {
14
- id: String,
24
+ id: String?|nil,
15
25
  pubkey: String,
16
26
  created_at: Integer,
17
27
  kind: Integer,
18
- tags: Array[String],
28
+ tags: Array[Array[String]],
19
29
  content: String,
20
- sig: String
30
+ sig: String?|nil
21
31
  }
22
32
  def ==: (Event other) -> bool
33
+
34
+ def sign:(String) -> Event
35
+
36
+ def add_event_reference: (String) -> Array[Array[String]]
37
+ def add_pubkey_reference: (String) -> Array[Array[String]]
23
38
  end
24
39
  end
@@ -4,5 +4,6 @@ module Nostr
4
4
  TEXT_NOTE: Integer
5
5
  RECOMMEND_SERVER: Integer
6
6
  CONTACT_LIST: Integer
7
+ ENCRYPTED_DIRECT_MESSAGE: Integer
7
8
  end
8
9
  end
@@ -0,0 +1,12 @@
1
+ module Nostr
2
+ module Events
3
+ class EncryptedDirectMessage < Event
4
+ def initialize: (
5
+ plain_text: String,
6
+ sender_private_key: String,
7
+ recipient_public_key: String,
8
+ ?previous_direct_message: String|nil
9
+ ) -> void
10
+ end
11
+ end
12
+ end
data/sig/nostr/user.rbs CHANGED
@@ -6,14 +6,12 @@ module Nostr
6
6
  def initialize: (?keypair: KeyPair | nil, ?keygen: Keygen) -> void
7
7
  def create_event: (
8
8
  {
9
- id: String,
10
9
  pubkey: String,
11
10
  created_at: Integer,
12
11
  kind: Integer,
13
12
  tags: Array[String],
14
13
  content: String,
15
14
  created_at: Integer,
16
- sig: String
17
15
  }
18
16
  ) -> Event
19
17
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nostr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wilson Silva
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-15 00:00:00.000000000 Z
11
+ date: 2023-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bech32
@@ -455,9 +455,10 @@ files:
455
455
  - lib/nostr.rb
456
456
  - lib/nostr/client.rb
457
457
  - lib/nostr/client_message_type.rb
458
+ - lib/nostr/crypto.rb
458
459
  - lib/nostr/event.rb
459
- - lib/nostr/event_fragment.rb
460
460
  - lib/nostr/event_kind.rb
461
+ - lib/nostr/events/encrypted_direct_message.rb
461
462
  - lib/nostr/filter.rb
462
463
  - lib/nostr/key_pair.rb
463
464
  - lib/nostr/keygen.rb
@@ -469,9 +470,10 @@ files:
469
470
  - sig/nostr.rbs
470
471
  - sig/nostr/client.rbs
471
472
  - sig/nostr/client_message_type.rbs
473
+ - sig/nostr/crypto.rbs
472
474
  - sig/nostr/event.rbs
473
- - sig/nostr/event_fragment.rbs
474
475
  - sig/nostr/event_kind.rbs
476
+ - sig/nostr/events/encrypted_direct_message.rbs
475
477
  - sig/nostr/filter.rbs
476
478
  - sig/nostr/key_pair.rbs
477
479
  - sig/nostr/keygen.rbs
@@ -506,7 +508,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
506
508
  - !ruby/object:Gem::Version
507
509
  version: '0'
508
510
  requirements: []
509
- rubygems_version: 3.4.4
511
+ rubygems_version: 3.4.7
510
512
  signing_key:
511
513
  specification_version: 4
512
514
  summary: Client and relay implementation of the Nostr protocol.
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Nostr
4
- # Part of an +Event+. A complete +Event+ must have an +id+ and a +sig+.
5
- class EventFragment
6
- # 32-bytes hex-encoded public key of the event creator
7
- #
8
- # @api public
9
- #
10
- # @example
11
- # event.pubkey # => '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
12
- #
13
- # @return [String]
14
- #
15
- attr_reader :pubkey
16
-
17
- # Date of the creation of the vent. A UNIX timestamp, in seconds
18
- #
19
- # @api public
20
- #
21
- # @example
22
- # event.created_at # => 1230981305
23
- #
24
- # @return [Integer]
25
- #
26
- attr_reader :created_at
27
-
28
- # The kind of the event. An integer from 0 to 3
29
- #
30
- # @api public
31
- #
32
- # @example
33
- # event.kind # => 1
34
- #
35
- # @return [Integer]
36
- #
37
- attr_reader :kind
38
-
39
- # An array of tags. Each tag is an array of strings
40
- #
41
- # @api public
42
- #
43
- # @example Tags referencing an event
44
- # event.tags #=> [["e", "event_id", "relay URL"]]
45
- #
46
- # @example Tags referencing a key
47
- # event.tags #=> [["p", "event_id", "relay URL"]]
48
- #
49
- # @return [Array<Array>]
50
- #
51
- attr_reader :tags
52
-
53
- # An arbitrary string
54
- #
55
- # @api public
56
- #
57
- # @example
58
- # event.content # => 'Your feedback is appreciated, now pay $8'
59
- #
60
- # @return [String]
61
- #
62
- attr_reader :content
63
-
64
- # Instantiates a new EventFragment
65
- #
66
- # @api public
67
- #
68
- # @example
69
- # Nostr::EventFragment.new(
70
- # pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
71
- # created_at: 1230981305,
72
- # kind: 1,
73
- # tags: [['e', '189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408']],
74
- # content: 'Your feedback is appreciated, now pay $8'
75
- # )
76
- #
77
- # @param pubkey [String] 32-bytes hex-encoded public key of the event creator.
78
- # @param created_at [Integer] Date of the creation of the vent. A UNIX timestamp, in seconds.
79
- # @param kind [Integer] The kind of the event. An integer from 0 to 3.
80
- # @param tags [Array<Array>] An array of tags. Each tag is an array of strings.
81
- # @param content [String] Arbitrary string.
82
- #
83
- def initialize(pubkey:, kind:, content:, created_at: Time.now.to_i, tags: [])
84
- @pubkey = pubkey
85
- @created_at = created_at
86
- @kind = kind
87
- @tags = tags
88
- @content = content
89
- end
90
-
91
- # Serializes the event fragment, to obtain a SHA256 hash of it
92
- #
93
- # @api public
94
- #
95
- # @example Converting the event to a hash
96
- # event_fragment.serialize
97
- #
98
- # @return [Array] The event fragment as an array.
99
- #
100
- def serialize
101
- [
102
- 0,
103
- pubkey,
104
- created_at,
105
- kind,
106
- tags,
107
- content
108
- ]
109
- end
110
- end
111
- end
@@ -1,12 +0,0 @@
1
- module Nostr
2
- class EventFragment
3
- attr_reader pubkey: String
4
- attr_reader created_at: Integer
5
- attr_reader kind: Integer
6
- attr_reader tags: Array[String]
7
- attr_reader content: String
8
-
9
- def initialize: (pubkey: String, kind: Integer, content: String, ?created_at: Integer, ?tags: Array[String]) -> void
10
- def serialize: -> [Integer, String, Integer, Integer, Array[String], String]
11
- end
12
- end