nostr 0.2.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 +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +36 -0
- data/README.md +123 -16
- data/Steepfile +16 -0
- data/lib/nostr/crypto.rb +143 -0
- data/lib/nostr/event.rb +157 -11
- data/lib/nostr/event_kind.rb +15 -0
- data/lib/nostr/events/encrypted_direct_message.rb +53 -0
- data/lib/nostr/keygen.rb +1 -1
- data/lib/nostr/user.rb +3 -31
- data/lib/nostr/version.rb +1 -1
- data/lib/nostr.rb +2 -1
- data/nostr.gemspec +3 -0
- data/sig/nostr/client.rbs +20 -0
- data/sig/nostr/client_message_type.rbs +7 -0
- data/sig/nostr/crypto.rbs +16 -0
- data/sig/nostr/event.rbs +39 -0
- data/sig/nostr/event_kind.rbs +9 -0
- data/sig/nostr/events/encrypted_direct_message.rbs +12 -0
- data/sig/nostr/filter.rbs +25 -0
- data/sig/nostr/key_pair.rbs +9 -0
- data/sig/nostr/keygen.rbs +13 -0
- data/sig/nostr/relay.rbs +9 -0
- data/sig/nostr/subscription.rbs +9 -0
- data/sig/nostr/user.rbs +22 -0
- data/sig/vendor/ecsda/group/secp256k1.rbs +6 -0
- data/sig/vendor/event_emitter.rbs +9 -0
- data/sig/vendor/event_machine/channel.rbs +18 -0
- data/sig/vendor/event_machine.rbs +69 -0
- data/sig/vendor/schnorr.rbs +4 -0
- metadata +65 -4
- data/lib/nostr/event_fragment.rb +0 -111
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd56d59d68235fe8fa4bceb67df4a7e83d4b6a8295ed31cc0152002800ad7d23
|
4
|
+
data.tar.gz: 56b70ada7f9fd6cd29be1c7bc23b7a23adc49bfe2f8d8de48300f70709626a3e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be113a67cd651739cc3bac1deb4bba30e3598cfd787e8971a764e82d58c03ad1c918d3edd14630bbb1a5e2c6504c3c344859f0449a1774df7bb7bf0d3c38f537
|
7
|
+
data.tar.gz: c80bbf1f4d3caa25f48e1630b650587a2319197d7edba33e824b17e93f6355d8593c41364ad1fdb86a834d8cae63ad9f15e8feacae953b0ff8d7294f09c2c765
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,41 @@ 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
|
+
|
30
|
+
## [0.3.0] - 2023-02-15
|
31
|
+
|
32
|
+
### Added
|
33
|
+
|
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
|
41
|
+
|
7
42
|
## [0.2.0] - 2023-01-12
|
8
43
|
|
9
44
|
### Added
|
@@ -14,5 +49,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
14
49
|
|
15
50
|
- Initial release
|
16
51
|
|
52
|
+
[0.3.0]: https://github.com/wilsonsilva/nostr/compare/v0.2.0...v0.3.0
|
17
53
|
[0.2.0]: https://github.com/wilsonsilva/nostr/compare/v0.1.0...v0.2.0
|
18
54
|
[0.1.0]: https://github.com/wilsonsilva/nostr/compare/7fded5...v0.1.0
|
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:
|
@@ -27,12 +48,11 @@ All examples below assume that the gem has been required.
|
|
27
48
|
require 'nostr'
|
28
49
|
```
|
29
50
|
|
30
|
-
|
31
51
|
### Generating a keypair
|
32
52
|
|
33
53
|
```ruby
|
34
54
|
keygen = Nostr::Keygen.new
|
35
|
-
keypair = keygen.
|
55
|
+
keypair = keygen.generate_key_pair
|
36
56
|
|
37
57
|
keypair.private_key
|
38
58
|
keypair.public_key
|
@@ -55,7 +75,7 @@ You may instantiate multiple Clients and multiple Relays.
|
|
55
75
|
|
56
76
|
```ruby
|
57
77
|
client = Nostr::Client.new
|
58
|
-
relay
|
78
|
+
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
|
59
79
|
|
60
80
|
client.connect(relay)
|
61
81
|
```
|
@@ -82,14 +102,28 @@ end
|
|
82
102
|
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com'
|
83
103
|
```
|
84
104
|
|
85
|
-
The `:message` event is fired
|
105
|
+
The `:message` event is fired when data is received through a WebSocket.
|
86
106
|
|
87
107
|
```ruby
|
88
108
|
client.on :message do |message|
|
89
109
|
puts message
|
90
110
|
end
|
91
111
|
|
92
|
-
#
|
112
|
+
# [
|
113
|
+
# "EVENT",
|
114
|
+
# "d34107357089bfc9882146d3bfab0386",
|
115
|
+
# {
|
116
|
+
# "content":"",
|
117
|
+
# "created_at":1676456512,
|
118
|
+
# "id":"18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
|
119
|
+
# "kind":3,
|
120
|
+
# "pubkey":"117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
|
121
|
+
# "sig":"d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
|
122
|
+
# "tags":[
|
123
|
+
# ["p","1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
|
124
|
+
# ]
|
125
|
+
# }
|
126
|
+
# ]
|
93
127
|
```
|
94
128
|
|
95
129
|
The `:close` event is fired when a connection with a WebSocket is closed.
|
@@ -179,10 +213,66 @@ event = user.create_event(
|
|
179
213
|
client.publish(event)
|
180
214
|
```
|
181
215
|
|
182
|
-
|
216
|
+
### Creating/updating your contact list
|
217
|
+
|
218
|
+
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
# Creating a contact list event with 2 contacts
|
222
|
+
update_contacts_event = user.create_event(
|
223
|
+
kind: Nostr::EventKind::CONTACT_LIST,
|
224
|
+
tags: [
|
225
|
+
[
|
226
|
+
"p", # mandatory
|
227
|
+
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
|
228
|
+
"wss://alicerelay.com/", # can be an empty string or can be omitted
|
229
|
+
"alice" # can be an empty string or can be omitted
|
230
|
+
],
|
231
|
+
[
|
232
|
+
"p", # mandatory
|
233
|
+
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
|
234
|
+
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
|
235
|
+
"bob" # can be an empty string or can be omitted
|
236
|
+
],
|
237
|
+
],
|
238
|
+
)
|
239
|
+
|
240
|
+
# Send it to the Relay
|
241
|
+
client.publish(update_contacts_event)
|
242
|
+
```
|
243
|
+
|
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
|
183
272
|
|
184
273
|
- [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
|
185
|
-
- [
|
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)
|
186
276
|
|
187
277
|
## Development
|
188
278
|
|
@@ -200,17 +290,34 @@ The health and maintainability of the codebase is ensured through a set of
|
|
200
290
|
Rake tasks to test, lint and audit the gem for security vulnerabilities and documentation:
|
201
291
|
|
202
292
|
```
|
203
|
-
rake
|
204
|
-
rake
|
205
|
-
rake
|
206
|
-
rake
|
207
|
-
rake
|
208
|
-
rake
|
209
|
-
rake
|
210
|
-
rake
|
211
|
-
rake
|
293
|
+
rake build # Build nostr.gem into the pkg directory
|
294
|
+
rake build:checksum # Generate SHA512 checksum if nostr.gem into the checksums directory
|
295
|
+
rake bundle:audit:check # Checks the Gemfile.lock for insecure dependencies
|
296
|
+
rake bundle:audit:update # Updates the bundler-audit vulnerability database
|
297
|
+
rake clean # Remove any temporary products
|
298
|
+
rake clobber # Remove any generated files
|
299
|
+
rake coverage # Run spec with coverage
|
300
|
+
rake install # Build and install nostr.gem into system gems
|
301
|
+
rake install:local # Build and install nostr.gem into system gems without network access
|
302
|
+
rake qa # Test, lint and perform security and documentation audits
|
303
|
+
rake release[remote] # Create a tag, build and push nostr.gem to rubygems.org
|
304
|
+
rake rubocop # Run RuboCop
|
305
|
+
rake rubocop:autocorrect # Autocorrect RuboCop offenses (only when it's safe)
|
306
|
+
rake rubocop:autocorrect_all # Autocorrect RuboCop offenses (safe and unsafe)
|
307
|
+
rake spec # Run RSpec code examples
|
308
|
+
rake verify_measurements # Verify that yardstick coverage is at least 100%
|
309
|
+
rake yard # Generate YARD Documentation
|
310
|
+
rake yard:junk # Check the junk in your YARD Documentation
|
311
|
+
rake yardstick_measure # Measure docs in lib/**/*.rb with yardstick
|
212
312
|
```
|
213
313
|
|
314
|
+
### Type checking
|
315
|
+
|
316
|
+
This gem leverages [RBS](https://github.com/ruby/rbs), a language to describe the structure of Ruby programs. It is
|
317
|
+
used to provide type checking and autocompletion in your editor. Run `bundle exec typeprof FILENAME` to generate
|
318
|
+
an RBS definition for the given Ruby file. And validate all definitions using [Steep](https://github.com/soutaro/steep)
|
319
|
+
with the command `bundle exec steep check`.
|
320
|
+
|
214
321
|
## Contributing
|
215
322
|
|
216
323
|
Bug reports and pull requests are welcome on GitHub at https://github.com/wilsonsilva/nostr.
|
data/Steepfile
ADDED
data/lib/nostr/crypto.rb
ADDED
@@ -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
|
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
|
-
# @
|
73
|
+
# @example Setting the event id
|
74
|
+
# event.id = 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
75
|
+
# event.id # => 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460'
|
76
|
+
#
|
77
|
+
# @return [String|nil]
|
16
78
|
#
|
17
|
-
|
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
|
-
# @
|
89
|
+
# @example Setting the event signature
|
90
|
+
# event.sig = '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
|
91
|
+
# event.sig # => '058613f8d34c053294cc28b7f9e1f8f0e80fd1ac94fb20f2da6ca514e7360b39'
|
92
|
+
#
|
93
|
+
# @return [String|nil]
|
28
94
|
#
|
29
|
-
|
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
|
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
|
-
|
53
|
-
|
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
|
data/lib/nostr/event_kind.rb
CHANGED
@@ -24,5 +24,20 @@ module Nostr
|
|
24
24
|
# @return [Integer]
|
25
25
|
#
|
26
26
|
RECOMMEND_SERVER = 2
|
27
|
+
|
28
|
+
# A special event with kind 3, meaning "contact list" is defined as having a list of p tags, one for each of
|
29
|
+
# the followed/known profiles one is following.
|
30
|
+
#
|
31
|
+
# @return [Integer]
|
32
|
+
#
|
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
|
27
42
|
end
|
28
43
|
end
|