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 +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +29 -0
- data/README.md +51 -2
- data/Steepfile +2 -0
- data/lib/nostr/crypto.rb +143 -0
- data/lib/nostr/event.rb +157 -11
- data/lib/nostr/event_kind.rb +8 -0
- data/lib/nostr/events/encrypted_direct_message.rb +53 -0
- data/lib/nostr/keygen.rb +1 -1
- data/lib/nostr/user.rb +2 -30
- data/lib/nostr/version.rb +1 -1
- data/lib/nostr.rb +2 -1
- data/sig/nostr/crypto.rbs +16 -0
- data/sig/nostr/event.rbs +24 -9
- data/sig/nostr/event_kind.rbs +1 -0
- data/sig/nostr/events/encrypted_direct_message.rbs +12 -0
- data/sig/nostr/user.rbs +0 -2
- metadata +7 -5
- data/lib/nostr/event_fragment.rb +0 -111
- data/sig/nostr/event_fragment.rbs +0 -12
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,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.
|
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
|
-
|
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
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
@@ -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
|
-
|
61
|
-
|
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
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
|
3
|
-
attr_reader
|
4
|
-
attr_reader
|
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: (
|
7
|
-
|
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
|
data/sig/nostr/event_kind.rbs
CHANGED
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.
|
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-
|
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.
|
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.
|
data/lib/nostr/event_fragment.rb
DELETED
@@ -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
|