nostr 0.3.0 → 0.4.0

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