nostr 0.3.0 → 0.5.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/.editorconfig +1 -1
- data/.rubocop.yml +26 -0
- data/.tool-versions +2 -1
- data/CHANGELOG.md +65 -1
- data/README.md +96 -183
- data/Steepfile +2 -0
- data/docs/.gitignore +4 -0
- data/docs/.vitepress/config.mjs +112 -0
- data/docs/README.md +44 -0
- data/docs/api-examples.md +49 -0
- data/docs/bun.lockb +0 -0
- data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
- data/docs/core/client.md +108 -0
- data/docs/core/keys.md +136 -0
- data/docs/core/user.md +43 -0
- data/docs/events/contact-list.md +29 -0
- data/docs/events/encrypted-direct-message.md +28 -0
- data/docs/events/recommend-server.md +32 -0
- data/docs/events/set-metadata.md +20 -0
- data/docs/events/text-note.md +15 -0
- data/docs/events.md +11 -0
- data/docs/getting-started/installation.md +21 -0
- data/docs/getting-started/overview.md +170 -0
- data/docs/implemented-nips.md +9 -0
- data/docs/index.md +44 -0
- data/docs/markdown-examples.md +85 -0
- data/docs/package.json +12 -0
- data/docs/relays/connecting-to-a-relay.md +21 -0
- data/docs/relays/publishing-events.md +29 -0
- data/docs/relays/receiving-events.md +6 -0
- data/docs/subscriptions/creating-a-subscription.md +49 -0
- data/docs/subscriptions/deleting-a-subscription.md +10 -0
- data/docs/subscriptions/filtering-subscription-events.md +115 -0
- data/docs/subscriptions/updating-a-subscription.md +4 -0
- data/lib/nostr/bech32.rb +203 -0
- data/lib/nostr/client.rb +2 -1
- data/lib/nostr/crypto.rb +147 -0
- data/lib/nostr/errors/error.rb +7 -0
- data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
- data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
- data/lib/nostr/errors/key_validation_error.rb +6 -0
- data/lib/nostr/errors.rb +8 -0
- data/lib/nostr/event.rb +157 -12
- data/lib/nostr/event_kind.rb +8 -0
- data/lib/nostr/events/encrypted_direct_message.rb +54 -0
- data/lib/nostr/filter.rb +4 -4
- data/lib/nostr/key.rb +100 -0
- data/lib/nostr/key_pair.rb +30 -6
- data/lib/nostr/keygen.rb +43 -4
- data/lib/nostr/private_key.rb +36 -0
- data/lib/nostr/public_key.rb +36 -0
- data/lib/nostr/relay_message_type.rb +18 -0
- data/lib/nostr/subscription.rb +2 -2
- data/lib/nostr/user.rb +17 -36
- data/lib/nostr/version.rb +1 -1
- data/lib/nostr.rb +8 -1
- data/nostr.gemspec +9 -9
- data/sig/nostr/bech32.rbs +14 -0
- data/sig/nostr/client.rbs +5 -5
- data/sig/nostr/crypto.rbs +16 -0
- data/sig/nostr/errors/error.rbs +4 -0
- data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
- data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
- data/sig/nostr/errors/key_validation_error.rbs +4 -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/filter.rbs +3 -12
- data/sig/nostr/key.rbs +16 -0
- data/sig/nostr/key_pair.rbs +7 -3
- data/sig/nostr/keygen.rbs +5 -2
- data/sig/nostr/private_key.rbs +4 -0
- data/sig/nostr/public_key.rbs +4 -0
- data/sig/nostr/relay_message_type.rbs +8 -0
- data/sig/nostr/user.rbs +4 -10
- data/sig/vendor/bech32/nostr/entity.rbs +41 -0
- data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
- data/sig/vendor/bech32/segwit_addr.rbs +21 -0
- data/sig/vendor/bech32.rbs +25 -0
- data/sig/vendor/event_emitter.rbs +10 -3
- data/sig/vendor/event_machine/channel.rbs +1 -1
- data/sig/vendor/faye/websocket/api.rbs +45 -0
- data/sig/vendor/faye/websocket/client.rbs +43 -0
- data/sig/vendor/faye/websocket.rbs +30 -0
- metadata +83 -23
- data/lib/nostr/event_fragment.rb +0 -111
- data/sig/nostr/event_fragment.rbs +0 -12
data/lib/nostr/bech32.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bech32'
|
4
|
+
require 'bech32/nostr'
|
5
|
+
require 'bech32/nostr/entity'
|
6
|
+
|
7
|
+
module Nostr
|
8
|
+
# Bech32 encoding and decoding
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
#
|
12
|
+
module Bech32
|
13
|
+
# Decodes a bech32-encoded string
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# bech32_value = 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
19
|
+
# Nostr::Bech32.decode(bech32_value) # => ['npub', '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d8...']
|
20
|
+
#
|
21
|
+
# @param [String] bech32_value The bech32-encoded string to decode
|
22
|
+
#
|
23
|
+
# @return [Array<String, String>] The human readable part and the data
|
24
|
+
#
|
25
|
+
def self.decode(bech32_value)
|
26
|
+
entity = ::Bech32::Nostr::NIP19.decode(bech32_value)
|
27
|
+
|
28
|
+
case entity
|
29
|
+
in ::Bech32::Nostr::BareEntity
|
30
|
+
[entity.hrp, entity.data]
|
31
|
+
in ::Bech32::Nostr::TLVEntity
|
32
|
+
[entity.hrp, entity.entries]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Encodes data into a bech32 string
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# Nostr::Bech32.encode(hrp: 'npub', data: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
42
|
+
# # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
43
|
+
#
|
44
|
+
# @param [String] hrp The human readable part (npub, nsec, nprofile, nrelay, nevent, naddr, etc)
|
45
|
+
# @param [String] data The data to encode
|
46
|
+
#
|
47
|
+
# @return [String] The bech32-encoded string
|
48
|
+
#
|
49
|
+
def self.encode(hrp:, data:)
|
50
|
+
::Bech32::Nostr::BareEntity.new(hrp, data).encode
|
51
|
+
end
|
52
|
+
|
53
|
+
# Encodes a hex-encoded public key into a bech32 string
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# Nostr::Bech32.npub_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
59
|
+
# # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
60
|
+
#
|
61
|
+
# @param [String] npub The public key to encode
|
62
|
+
#
|
63
|
+
# @see Nostr::Bech32#encode
|
64
|
+
# @see Nostr::PublicKey#to_bech32
|
65
|
+
# @see Nostr::PrivateKey#to_bech32
|
66
|
+
#
|
67
|
+
# @return [String] The bech32-encoded string
|
68
|
+
#
|
69
|
+
def self.npub_encode(npub)
|
70
|
+
encode(hrp: 'npub', data: npub)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Encodes a hex-encoded private key into a bech32 string
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# Nostr::Bech32.nsec_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
79
|
+
# # => 'nsec10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
80
|
+
#
|
81
|
+
# @param [String] nsec The private key to encode
|
82
|
+
#
|
83
|
+
# @see Nostr::Bech32#encode
|
84
|
+
# @see Nostr::PrivateKey#to_bech32
|
85
|
+
# @see Nostr::PublicKey#to_bech32
|
86
|
+
#
|
87
|
+
# @return [String] The bech32-encoded string
|
88
|
+
#
|
89
|
+
def self.nsec_encode(nsec)
|
90
|
+
encode(hrp: 'nsec', data: nsec)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Encodes an address into a bech32 string
|
94
|
+
#
|
95
|
+
# @api public
|
96
|
+
#
|
97
|
+
# @example
|
98
|
+
# naddr = Nostr::Bech32.naddr_encode(
|
99
|
+
# pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
|
100
|
+
# relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
101
|
+
# kind: Nostr::EventKind::TEXT_NOTE,
|
102
|
+
# identifier: 'damus'
|
103
|
+
# )
|
104
|
+
# naddr # => 'naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7ns...'
|
105
|
+
#
|
106
|
+
# @param [PublicKey] pubkey The public key to encode
|
107
|
+
# @param [Array<String>] relays The relays to encode
|
108
|
+
# @param [String] kind The kind of address to encode
|
109
|
+
# @param [String] identifier The identifier of the address to encode
|
110
|
+
#
|
111
|
+
# @return [String] The bech32-encoded string
|
112
|
+
#
|
113
|
+
def self.naddr_encode(pubkey:, relays: [], kind: nil, identifier: nil)
|
114
|
+
entry_relays = relays.map do |relay_url|
|
115
|
+
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
116
|
+
end
|
117
|
+
|
118
|
+
pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, pubkey)
|
119
|
+
kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
|
120
|
+
identifier_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, identifier)
|
121
|
+
|
122
|
+
entries = [pubkey_entry, *entry_relays, kind_entry, identifier_entry].compact
|
123
|
+
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT_COORDINATE, entries)
|
124
|
+
entity.encode
|
125
|
+
end
|
126
|
+
|
127
|
+
# Encodes an event into a bech32 string
|
128
|
+
#
|
129
|
+
# @api public
|
130
|
+
#
|
131
|
+
# @example
|
132
|
+
# nevent = Nostr::Bech32.nevent_encode(
|
133
|
+
# id: '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3',
|
134
|
+
# relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
135
|
+
# kind: Nostr::EventKind::TEXT_NOTE,
|
136
|
+
# )
|
137
|
+
# nevent # => 'nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0pra...'
|
138
|
+
#
|
139
|
+
# @param [PublicKey] id The id the event to encode
|
140
|
+
# @param [Array<String>] relays The relays to encode
|
141
|
+
# @param [String] kind The kind of event to encode
|
142
|
+
#
|
143
|
+
# @return [String] The bech32-encoded string
|
144
|
+
#
|
145
|
+
def self.nevent_encode(id:, relays: [], kind: nil)
|
146
|
+
entry_relays = relays.map do |relay_url|
|
147
|
+
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
148
|
+
end
|
149
|
+
|
150
|
+
id_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_AUTHOR, id)
|
151
|
+
kind_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_KIND, kind)
|
152
|
+
|
153
|
+
entries = [id_entry, *entry_relays, kind_entry].compact
|
154
|
+
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_EVENT, entries)
|
155
|
+
entity.encode
|
156
|
+
end
|
157
|
+
|
158
|
+
# Encodes a profile into a bech32 string
|
159
|
+
#
|
160
|
+
# @api public
|
161
|
+
#
|
162
|
+
# @example
|
163
|
+
# nprofile = Nostr::Bech32.nprofile_encode(
|
164
|
+
# pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e',
|
165
|
+
# relays: ['wss://relay.damus.io', 'wss://nos.lol']
|
166
|
+
# )
|
167
|
+
#
|
168
|
+
# @param [PublicKey] pubkey The public key to encode
|
169
|
+
# @param [Array<String>] relays The relays to encode
|
170
|
+
#
|
171
|
+
# @return [String] The bech32-encoded string
|
172
|
+
#
|
173
|
+
def self.nprofile_encode(pubkey:, relays: [])
|
174
|
+
entry_relays = relays.map do |relay_url|
|
175
|
+
::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_RELAY, relay_url)
|
176
|
+
end
|
177
|
+
|
178
|
+
pubkey_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, pubkey)
|
179
|
+
entries = [pubkey_entry, *entry_relays].compact
|
180
|
+
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_PROFILE, entries)
|
181
|
+
entity.encode
|
182
|
+
end
|
183
|
+
|
184
|
+
# Encodes a relay URL into a bech32 string
|
185
|
+
#
|
186
|
+
# @api public
|
187
|
+
#
|
188
|
+
# @example
|
189
|
+
# nrelay = Nostr::Bech32.nrelay_encode('wss://relay.damus.io')
|
190
|
+
# nrelay # => 'nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x'
|
191
|
+
#
|
192
|
+
# @param [String] relay_url The relay url to encode
|
193
|
+
#
|
194
|
+
# @return [String] The bech32-encoded string
|
195
|
+
#
|
196
|
+
def self.nrelay_encode(relay_url)
|
197
|
+
relay_entry = ::Bech32::Nostr::TLVEntry.new(::Bech32::Nostr::TLVEntity::TYPE_SPECIAL, relay_url)
|
198
|
+
|
199
|
+
entity = ::Bech32::Nostr::TLVEntity.new(::Bech32::Nostr::NIP19::HRP_RELAY, [relay_entry])
|
200
|
+
entity.encode
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
data/lib/nostr/client.rb
CHANGED
@@ -74,7 +74,8 @@ module Nostr
|
|
74
74
|
# @example Subscribing to all events created after a certain time
|
75
75
|
# subscription = client.subscribe(filter: Nostr::Filter.new(since: 1230981305))
|
76
76
|
#
|
77
|
-
# @param subscription_id [String] The subscription id.
|
77
|
+
# @param subscription_id [String] The subscription id. An arbitrary, non-empty string of max length 64
|
78
|
+
# chars used to represent a subscription.
|
78
79
|
# @param filter [Filter] A set of attributes that represent the events that the client is interested in.
|
79
80
|
#
|
80
81
|
# @return [Subscription] The subscription object
|
data/lib/nostr/crypto.rb
ADDED
@@ -0,0 +1,147 @@
|
|
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 [PrivateKey] 32-bytes hex-encoded private key of the creator.
|
34
|
+
# @param recipient_public_key [PublicKey] 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 [PublicKey] 32-bytes hex-encoded public key of the message creator.
|
58
|
+
# @param recipient_private_key [PrivateKey] 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
|
+
|
66
|
+
# Ensure iv and base64_encoded_text are not nil
|
67
|
+
return '' unless iv && base64_encoded_text
|
68
|
+
|
69
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
|
70
|
+
cipher.iv = Base64.decode64(iv)
|
71
|
+
cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
|
72
|
+
plain_text = cipher.update(Base64.decode64(base64_encoded_text)) + cipher.final
|
73
|
+
plain_text.force_encoding('UTF-8')
|
74
|
+
end
|
75
|
+
|
76
|
+
# Uses the private key to generate an event id and sign the event
|
77
|
+
#
|
78
|
+
# @api public
|
79
|
+
#
|
80
|
+
# @example Signing an event
|
81
|
+
# crypto = Nostr::Crypto.new
|
82
|
+
# crypto.sign(event, private_key)
|
83
|
+
# event.id # => an id
|
84
|
+
# event.sig # => a signature
|
85
|
+
#
|
86
|
+
# @param event [Event] The event to be signed
|
87
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
88
|
+
#
|
89
|
+
# @return [Event] An unsigned event.
|
90
|
+
#
|
91
|
+
def sign_event(event, private_key)
|
92
|
+
event_digest = hash_event(event)
|
93
|
+
|
94
|
+
hex_private_key = Array(private_key).pack('H*')
|
95
|
+
hex_message = Array(event_digest).pack('H*')
|
96
|
+
event_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
|
97
|
+
|
98
|
+
event.id = event_digest
|
99
|
+
event.sig = event_signature
|
100
|
+
|
101
|
+
event
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# Finds a shared key between two keys
|
107
|
+
#
|
108
|
+
# @api private
|
109
|
+
#
|
110
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
111
|
+
# @param public_key [PublicKey] 32-bytes hex-encoded public key.
|
112
|
+
#
|
113
|
+
# @return [String] A shared key used in the event's content encryption and decryption.
|
114
|
+
#
|
115
|
+
def compute_shared_key(private_key, public_key)
|
116
|
+
group = OpenSSL::PKey::EC::Group.new(CIPHER_CURVE)
|
117
|
+
|
118
|
+
private_key_bn = OpenSSL::BN.new(private_key, BN_BASE)
|
119
|
+
public_key_bn = OpenSSL::BN.new("02#{public_key}", BN_BASE)
|
120
|
+
public_key_point = OpenSSL::PKey::EC::Point.new(group, public_key_bn)
|
121
|
+
|
122
|
+
asn1 = OpenSSL::ASN1::Sequence(
|
123
|
+
[
|
124
|
+
OpenSSL::ASN1::Integer.new(1),
|
125
|
+
OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)),
|
126
|
+
OpenSSL::ASN1::ObjectId(CIPHER_CURVE, 0, :EXPLICIT),
|
127
|
+
OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT)
|
128
|
+
]
|
129
|
+
)
|
130
|
+
|
131
|
+
pkey = OpenSSL::PKey::EC.new(asn1.to_der)
|
132
|
+
pkey.dh_compute_key(public_key_point)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Generates a SHA256 hash of a +Nostr::Event+
|
136
|
+
#
|
137
|
+
# @api private
|
138
|
+
#
|
139
|
+
# @param event [Event] The event to be hashed
|
140
|
+
#
|
141
|
+
# @return [String] A SHA256 digest of the event
|
142
|
+
#
|
143
|
+
def hash_event(event)
|
144
|
+
Digest::SHA256.hexdigest(JSON.dump(event.serialize))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Raised when the human readable part of a Bech32 string is invalid
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
class InvalidHRPError < KeyValidationError
|
9
|
+
# Initializes the error
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# InvalidHRPError.new('example wrong hrp', 'nsec')
|
13
|
+
#
|
14
|
+
# @param given_hrp [String] The given human readable part of the Bech32 string
|
15
|
+
# @param allowed_hrp [String] The allowed human readable part of the Bech32 string
|
16
|
+
#
|
17
|
+
def initialize(given_hrp, allowed_hrp)
|
18
|
+
super("Invalid hrp: #{given_hrp}. The allowed hrp value for this kind of entity is '#{allowed_hrp}'.")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Raised when the private key is in an invalid format
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
class InvalidKeyFormatError < KeyValidationError
|
9
|
+
# Initializes the error
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# InvalidKeyFormatError.new('private'')
|
13
|
+
#
|
14
|
+
# @param [String] key_kind The kind of key that is invalid (public or private)
|
15
|
+
#
|
16
|
+
def initialize(key_kind)
|
17
|
+
super("Only lowercase hexadecimal characters are allowed in #{key_kind} keys.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Raised when the private key's length is not 64 characters
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
class InvalidKeyLengthError < KeyValidationError
|
9
|
+
# Initializes the error
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# InvalidKeyLengthError.new('private'')
|
13
|
+
#
|
14
|
+
# @param [String] key_kind The kind of key that is invalid (public or private)
|
15
|
+
#
|
16
|
+
def initialize(key_kind)
|
17
|
+
super("Invalid #{key_kind} key length. It should have 64 characters.")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nostr
|
4
|
+
# Raised when the private key is not a string
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
class InvalidKeyTypeError < KeyValidationError
|
9
|
+
# Initializes the error
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# InvalidKeyTypeError.new('private'')
|
13
|
+
#
|
14
|
+
# @param [String] key_kind The kind of key that is invalid (public or private)
|
15
|
+
#
|
16
|
+
def initialize(key_kind) = super("Invalid #{key_kind} key type")
|
17
|
+
end
|
18
|
+
end
|
data/lib/nostr/errors.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'errors/error'
|
4
|
+
require_relative 'errors/key_validation_error'
|
5
|
+
require_relative 'errors/invalid_hrp_error'
|
6
|
+
require_relative 'errors/invalid_key_type_error'
|
7
|
+
require_relative 'errors/invalid_key_length_error'
|
8
|
+
require_relative 'errors/invalid_key_format_error'
|
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,95 @@ 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
|
-
|
54
|
-
|
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
|
+
)
|
55
131
|
@id = id
|
56
132
|
@sig = sig
|
133
|
+
@pubkey = pubkey
|
134
|
+
@created_at = created_at
|
135
|
+
@kind = kind
|
136
|
+
@tags = tags
|
137
|
+
@content = content
|
138
|
+
end
|
139
|
+
|
140
|
+
# Adds a reference to an event id as an 'e' tag
|
141
|
+
#
|
142
|
+
# @api public
|
143
|
+
#
|
144
|
+
# @example Adding a reference to a pubkey
|
145
|
+
# event_id = '189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408'
|
146
|
+
# event.add_event_reference(event_id)
|
147
|
+
#
|
148
|
+
# @param event_id [String] 32-bytes hex-encoded event id.
|
149
|
+
#
|
150
|
+
# @return [Array<String>] The event's updated list of tags
|
151
|
+
#
|
152
|
+
def add_event_reference(event_id) = tags.push(['e', event_id])
|
153
|
+
|
154
|
+
# Adds a reference to a pubkey as a 'p' tag
|
155
|
+
#
|
156
|
+
# @api public
|
157
|
+
#
|
158
|
+
# @example Adding a reference to a pubkey
|
159
|
+
# pubkey = '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
|
160
|
+
# event.add_pubkey_reference(pubkey)
|
161
|
+
#
|
162
|
+
# @param pubkey [PublicKey] 32-bytes hex-encoded public key.
|
163
|
+
#
|
164
|
+
# @return [Array<String>] The event's updated list of tags
|
165
|
+
#
|
166
|
+
def add_pubkey_reference(pubkey) = tags.push(['p', pubkey.to_s])
|
167
|
+
|
168
|
+
# Signs an event with the user's private key
|
169
|
+
#
|
170
|
+
# @api public
|
171
|
+
#
|
172
|
+
# @example Signing an event
|
173
|
+
# event.sign(private_key)
|
174
|
+
#
|
175
|
+
# @param private_key [PrivateKey] 32-bytes hex-encoded private key.
|
176
|
+
#
|
177
|
+
# @return [Event] A signed event.
|
178
|
+
#
|
179
|
+
def sign(private_key)
|
180
|
+
crypto = Crypto.new
|
181
|
+
crypto.sign_event(self, private_key)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Serializes the event, to obtain a SHA256 digest of it
|
185
|
+
#
|
186
|
+
# @api public
|
187
|
+
#
|
188
|
+
# @example Converting the event to a digest
|
189
|
+
# event.serialize
|
190
|
+
#
|
191
|
+
# @return [Array] The event as an array.
|
192
|
+
#
|
193
|
+
def serialize
|
194
|
+
[
|
195
|
+
0,
|
196
|
+
pubkey,
|
197
|
+
created_at,
|
198
|
+
kind,
|
199
|
+
tags,
|
200
|
+
content
|
201
|
+
]
|
57
202
|
end
|
58
203
|
|
59
204
|
# 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
|