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