nostr 0.4.0 → 0.6.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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.adr-dir +1 -0
  3. data/.editorconfig +1 -1
  4. data/.rubocop.yml +24 -1
  5. data/.tool-versions +2 -1
  6. data/CHANGELOG.md +70 -1
  7. data/README.md +93 -228
  8. data/adr/0001-record-architecture-decisions.md +19 -0
  9. data/adr/0002-introduction-of-signature-class.md +27 -0
  10. data/docs/.gitignore +4 -0
  11. data/docs/.vitepress/config.mjs +114 -0
  12. data/docs/README.md +44 -0
  13. data/docs/api-examples.md +49 -0
  14. data/docs/bun.lockb +0 -0
  15. data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
  16. data/docs/common-use-cases/signing-and-verifying-events.md +50 -0
  17. data/docs/common-use-cases/signing-and-verifying-messages.md +43 -0
  18. data/docs/core/client.md +108 -0
  19. data/docs/core/keys.md +136 -0
  20. data/docs/core/user.md +43 -0
  21. data/docs/events/contact-list.md +29 -0
  22. data/docs/events/encrypted-direct-message.md +28 -0
  23. data/docs/events/recommend-server.md +32 -0
  24. data/docs/events/set-metadata.md +20 -0
  25. data/docs/events/text-note.md +15 -0
  26. data/docs/events.md +11 -0
  27. data/docs/getting-started/installation.md +21 -0
  28. data/docs/getting-started/overview.md +171 -0
  29. data/docs/implemented-nips.md +9 -0
  30. data/docs/index.md +42 -0
  31. data/docs/markdown-examples.md +85 -0
  32. data/docs/package.json +12 -0
  33. data/docs/relays/connecting-to-a-relay.md +21 -0
  34. data/docs/relays/publishing-events.md +29 -0
  35. data/docs/relays/receiving-events.md +6 -0
  36. data/docs/subscriptions/creating-a-subscription.md +49 -0
  37. data/docs/subscriptions/deleting-a-subscription.md +10 -0
  38. data/docs/subscriptions/filtering-subscription-events.md +115 -0
  39. data/docs/subscriptions/updating-a-subscription.md +4 -0
  40. data/lib/nostr/bech32.rb +203 -0
  41. data/lib/nostr/client.rb +2 -1
  42. data/lib/nostr/crypto.rb +93 -13
  43. data/lib/nostr/errors/error.rb +7 -0
  44. data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
  45. data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
  46. data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
  47. data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
  48. data/lib/nostr/errors/invalid_signature_format_error.rb +18 -0
  49. data/lib/nostr/errors/invalid_signature_length_error.rb +18 -0
  50. data/lib/nostr/errors/invalid_signature_type_error.rb +16 -0
  51. data/lib/nostr/errors/key_validation_error.rb +6 -0
  52. data/lib/nostr/errors/signature_validation_error.rb +6 -0
  53. data/lib/nostr/errors.rb +12 -0
  54. data/lib/nostr/event.rb +40 -13
  55. data/lib/nostr/event_kind.rb +1 -0
  56. data/lib/nostr/events/encrypted_direct_message.rb +8 -7
  57. data/lib/nostr/filter.rb +14 -11
  58. data/lib/nostr/key.rb +100 -0
  59. data/lib/nostr/key_pair.rb +54 -6
  60. data/lib/nostr/keygen.rb +44 -5
  61. data/lib/nostr/private_key.rb +36 -0
  62. data/lib/nostr/public_key.rb +36 -0
  63. data/lib/nostr/relay_message_type.rb +18 -0
  64. data/lib/nostr/signature.rb +67 -0
  65. data/lib/nostr/subscription.rb +2 -2
  66. data/lib/nostr/user.rb +17 -8
  67. data/lib/nostr/version.rb +1 -1
  68. data/lib/nostr.rb +7 -0
  69. data/nostr.gemspec +13 -13
  70. data/sig/nostr/bech32.rbs +14 -0
  71. data/sig/nostr/client.rbs +5 -5
  72. data/sig/nostr/crypto.rbs +8 -5
  73. data/sig/nostr/errors/error.rbs +4 -0
  74. data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
  75. data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
  76. data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
  77. data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
  78. data/sig/nostr/errors/invalid_signature_format_error.rbs +5 -0
  79. data/sig/nostr/errors/invalid_signature_length_error.rbs +5 -0
  80. data/sig/nostr/errors/invalid_signature_type_error.rbs +5 -0
  81. data/sig/nostr/errors/key_validation_error.rbs +4 -0
  82. data/sig/nostr/errors/signature_validation_error.rbs +4 -0
  83. data/sig/nostr/event.rbs +11 -10
  84. data/sig/nostr/events/encrypted_direct_message.rbs +2 -2
  85. data/sig/nostr/filter.rbs +3 -12
  86. data/sig/nostr/key.rbs +16 -0
  87. data/sig/nostr/key_pair.rbs +8 -3
  88. data/sig/nostr/keygen.rbs +5 -2
  89. data/sig/nostr/private_key.rbs +4 -0
  90. data/sig/nostr/public_key.rbs +4 -0
  91. data/sig/nostr/relay_message_type.rbs +8 -0
  92. data/sig/nostr/signature.rbs +14 -0
  93. data/sig/nostr/user.rbs +4 -8
  94. data/sig/vendor/bech32/nostr/entity.rbs +41 -0
  95. data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
  96. data/sig/vendor/bech32/segwit_addr.rbs +21 -0
  97. data/sig/vendor/bech32.rbs +25 -0
  98. data/sig/vendor/event_emitter.rbs +10 -3
  99. data/sig/vendor/event_machine/channel.rbs +1 -1
  100. data/sig/vendor/faye/websocket/api.rbs +45 -0
  101. data/sig/vendor/faye/websocket/client.rbs +43 -0
  102. data/sig/vendor/faye/websocket.rbs +30 -0
  103. data/sig/vendor/schnorr/signature.rbs +16 -0
  104. data/sig/vendor/schnorr.rbs +3 -1
  105. metadata +102 -28
@@ -0,0 +1,29 @@
1
+ # Publishing events
2
+
3
+ Create a [signed event](../core/keys) and call the method
4
+ [`Nostr::Client#publish`](https://www.rubydoc.info/gems/nostr/Nostr/Client#publish-instance_method) to send the
5
+ event to the relay.
6
+
7
+ ```ruby{4-8,17}
8
+ # Create a user with the keypair
9
+ user = Nostr::User.new(keypair: keypair)
10
+
11
+ # Create a signed event
12
+ text_note_event = user.create_event(
13
+ kind: Nostr::EventKind::TEXT_NOTE,
14
+ content: 'Your feedback is appreciated, now pay $8'
15
+ )
16
+
17
+ # Connect asynchronously to a relay
18
+ relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
19
+ client.connect(relay)
20
+
21
+ # Listen asynchronously for the connect event
22
+ client.on :connect do
23
+ # Send the event to the relay
24
+ client.publish(text_note_event)
25
+ end
26
+ ```
27
+
28
+ The relay will verify the signature of the event with the public key. If the signature is valid, the relay should
29
+ broadcast the event to all subscribers.
@@ -0,0 +1,6 @@
1
+ # Receiving events
2
+
3
+ To receive events from Relays, you must create a subscription on the relay. A subscription is a filter that defines the
4
+ events you want to receive.
5
+
6
+ For more information, read the [Subscription](../subscriptions/creating-a-subscription.md) section.
@@ -0,0 +1,49 @@
1
+ # Creating a subscription
2
+
3
+ A client can request events and subscribe to new updates __after__ it has established a connection with the Relay.
4
+
5
+ You may use a [`Nostr::Filter`](https://www.rubydoc.info/gems/nostr/Nostr/Filter) instance with as many attributes as
6
+ you wish:
7
+
8
+ ```ruby
9
+ client.on :connect do
10
+ filter = Nostr::Filter.new(
11
+ ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
12
+ authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
13
+ kinds: [Nostr::EventKind::TEXT_NOTE],
14
+ e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
15
+ p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
16
+ since: 1230981305,
17
+ until: 1292190341,
18
+ limit: 420,
19
+ )
20
+
21
+ subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
22
+ end
23
+ ```
24
+
25
+ With just a few:
26
+
27
+ ```ruby
28
+ client.on :connect do
29
+ filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
30
+ subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
31
+ end
32
+ ```
33
+
34
+ Or omit the filter:
35
+
36
+ ```ruby
37
+ client.on :connect do
38
+ subscription = client.subscribe(subscription_id: 'an-id')
39
+ end
40
+ ```
41
+
42
+ Or even omit the subscription id:
43
+
44
+ ```ruby
45
+ client.on :connect do
46
+ subscription = client.subscribe(filter: filter)
47
+ subscription.id # => "13736f08dee8d7b697222ba605c6fab2" (randomly generated)
48
+ end
49
+ ```
@@ -0,0 +1,10 @@
1
+ # Stop previous subscriptions
2
+
3
+ You can stop receiving messages from a subscription by calling
4
+ [`Nostr::Client#unsubscribe`](https://www.rubydoc.info/gems/nostr/Nostr/Client#unsubscribe-instance_method) with the
5
+ ID of the subscription you want to stop receiving messages from:
6
+
7
+ ```ruby
8
+ client.unsubscribe('your-subscription-id')
9
+ client.unsubscribe(subscription.id)
10
+ ```
@@ -0,0 +1,115 @@
1
+ # Filtering events
2
+
3
+ ## Filtering by id
4
+
5
+ You can filter events by their ids:
6
+
7
+ ```ruby
8
+ filter = Nostr::Filter.new(
9
+ ids: [
10
+ # matches events with these exact IDs
11
+ '8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8',
12
+ '461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
13
+ ]
14
+ )
15
+ subscription = client.subscribe(filter: filter)
16
+ ```
17
+
18
+ ## Filtering by author
19
+
20
+ You can filter events by their author's pubkey:
21
+
22
+ ```ruby
23
+ filter = Nostr::Filter.new(
24
+ authors: [
25
+ # matches events whose (authors) pubkey match these exact IDs
26
+ 'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
27
+ '51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
28
+ ]
29
+ )
30
+ subscription = client.subscribe(filter: filter)
31
+ ```
32
+
33
+ ## Filtering by kind
34
+
35
+ You can filter events by their kind:
36
+
37
+ ```ruby
38
+ filter = Nostr::Filter.new(
39
+ kinds: [
40
+ # matches events whose kind is TEXT_NOTE
41
+ Nostr::EventKind::TEXT_NOTE,
42
+ # and matches events whose kind is CONTACT_LIST
43
+ Nostr::EventKind::CONTACT_LIST,
44
+ ]
45
+ )
46
+ subscription = client.subscribe(filter: filter)
47
+ ```
48
+
49
+ ## Filtering by referenced event
50
+
51
+ You can filter events by the events they reference (in their `e` tag):
52
+
53
+ ```ruby
54
+ filter = Nostr::Filter.new(
55
+ e: [
56
+ # matches events that reference other events whose ids match these exact IDs
57
+ 'f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52',
58
+ 'f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8',
59
+ ]
60
+ )
61
+ subscription = client.subscribe(filter: filter)
62
+ ```
63
+
64
+ ## Filtering by referenced pubkey
65
+
66
+ You can filter events by the pubkeys they reference (in their `p` tag):
67
+
68
+ ```ruby
69
+ filter = Nostr::Filter.new(
70
+ p: [
71
+ # matches events that reference other pubkeys that match these exact IDs
72
+ 'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
73
+ '51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
74
+ ]
75
+ )
76
+ subscription = client.subscribe(filter: filter)
77
+ ```
78
+
79
+ ## Filtering by timestamp
80
+
81
+ You can filter events by their timestamp:
82
+
83
+ ```ruby
84
+ filter = Nostr::Filter.new(
85
+ since: 1230981305, # matches events that are newer than this timestamp
86
+ until: 1292190341, # matches events that are older than this timestamp
87
+ )
88
+ subscription = client.subscribe(filter: filter)
89
+ ```
90
+
91
+ ## Limiting the number of events
92
+
93
+ You can limit the number of events received:
94
+
95
+ ```ruby
96
+ filter = Nostr::Filter.new(
97
+ limit: 420, # matches at most 420 events
98
+ )
99
+ subscription = client.subscribe(filter: filter)
100
+ ```
101
+
102
+ ## Combining filters
103
+
104
+ You can combine filters. For example, to match `5` text note events that are newer than `1230981305` from the author
105
+ `ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577`:
106
+
107
+ ```ruby
108
+ filter = Nostr::Filter.new(
109
+ authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
110
+ kinds: [Nostr::EventKind::TEXT_NOTE],
111
+ since: 1230981305,
112
+ limit: 5,
113
+ )
114
+ subscription = client.subscribe(filter: filter)
115
+ ```
@@ -0,0 +1,4 @@
1
+ # Updating a subscription
2
+
3
+ Updating a subscription is done by creating a new subscription with the same id as the previous one. See
4
+ [creating a subscription](./creating-a-subscription.md) for more information.
@@ -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. A random string.
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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nostr
4
- # Performs cryptographic operations on a +Nostr::Event+.
4
+ # Performs cryptographic operations.
5
5
  class Crypto
6
6
  # Numeric base of the OpenSSL big number used in an event content's encryption.
7
7
  #
@@ -30,8 +30,8 @@ module Nostr
30
30
  # encrypted = crypto.encrypt_text(sender_private_key, recipient_public_key, 'Feedback appreciated. Now pay $8')
31
31
  # encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
32
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.
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
35
  # @param plain_text [String] The text to be encrypted
36
36
  #
37
37
  # @return [String] Encrypted text.
@@ -54,14 +54,18 @@ module Nostr
54
54
  # encrypted # => "wrYQaHDfpOEvyJELSCg1vzsywmlJTz8NqH03eFW44s8iQs869jtSb26Lr4s23gmY?iv=v38vAJ3LlJAGZxbmWU4qAg=="
55
55
  # decrypted = crypto.decrypt_text(recipient_private_key, sender_public_key, encrypted)
56
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.
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
59
  # @param encrypted_text [String] The text to be decrypted
60
60
  #
61
61
  # @return [String] Decrypted text.
62
62
  #
63
63
  def decrypt_text(recipient_private_key, sender_public_key, encrypted_text)
64
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
+
65
69
  cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM).decrypt
66
70
  cipher.iv = Base64.decode64(iv)
67
71
  cipher.key = compute_shared_key(recipient_private_key, sender_public_key)
@@ -80,31 +84,107 @@ module Nostr
80
84
  # event.sig # => a signature
81
85
  #
82
86
  # @param event [Event] The event to be signed
83
- # @param private_key [String] 32-bytes hex-encoded private key.
87
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
84
88
  #
85
89
  # @return [Event] An unsigned event.
86
90
  #
87
91
  def sign_event(event, private_key)
88
92
  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
+ signature = sign_message(event_digest, private_key)
93
94
 
94
95
  event.id = event_digest
95
- event.sig = event_signature
96
+ event.sig = signature
96
97
 
97
98
  event
98
99
  end
99
100
 
101
+ # Signs a message using the Schnorr signature algorithm
102
+ #
103
+ # @api public
104
+ #
105
+ # @example Signing a message
106
+ # crypto = Nostr::Crypto.new
107
+ # message = 'Viva la libertad carajo'
108
+ # private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
109
+ # signature = crypto.sign_message(message, private_key)
110
+ # signature # => 'b2115694a576f5bdcebf8c0951a3c7adcfbdb17b11cb9e6d6b7017691138bc6' \
111
+ # '38fee642a7bd26f71b313a7057181294198900a9770d1435e43f182acf3d34c26'
112
+ #
113
+ # @param [String] message The message to be signed
114
+ # @param [PrivateKey] private_key The private key used for signing
115
+ #
116
+ # @return [Signature] A signature object containing the signature as a 64-byte hexadecimal string.
117
+ #
118
+ def sign_message(message, private_key)
119
+ hex_private_key = Array(private_key).pack('H*')
120
+ hex_message = Array(message).pack('H*')
121
+ hex_signature = Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
122
+
123
+ Signature.new(hex_signature.to_s)
124
+ end
125
+
126
+ # Verifies the given {Signature} and returns true if it is valid
127
+ #
128
+ # @api public
129
+ #
130
+ # @example Checking a signature
131
+ # public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6')
132
+ # private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
133
+ # message = 'Viva la libertad carajo'
134
+ # crypto = Nostr::Crypto.new
135
+ # signature = crypto.sign_message(message, private_key)
136
+ # valid = crypto.valid_sig?(message, public_key, signature)
137
+ # valid # => true
138
+ #
139
+ # @see #check_sig!
140
+ #
141
+ # @param [String] message A message to be signed with binary format.
142
+ # @param [PublicKey] public_key The public key with binary format.
143
+ # @param [Signature] signature The signature with binary format.
144
+ #
145
+ # @return [Boolean] whether signature is valid.
146
+ #
147
+ def valid_sig?(message, public_key, signature)
148
+ signature = Schnorr::Signature.decode([signature].pack('H*'))
149
+ Schnorr.valid_sig?([message].pack('H*'), [public_key].pack('H*'), signature.encode)
150
+ end
151
+
152
+ # Verifies the given {Signature} and raises an +Schnorr::InvalidSignatureError+ if it is invalid
153
+ #
154
+ # @api public
155
+ #
156
+ # @example Checking a signature
157
+ # public_key = Nostr::PublicKey.new('15678d8fbc126fa326fac536acd5a6dcb5ef64b3d939abe31d6830cba6cd26d6')
158
+ # private_key = Nostr::PrivateKey.new('7d1e4219a5e7d8342654c675085bfbdee143f0eb0f0921f5541ef1705a2b407d')
159
+ # message = 'Viva la libertad carajo'
160
+ # crypto = Nostr::Crypto.new
161
+ # signature = crypto.sign_message(message, private_key)
162
+ # valid = crypto.valid_sig?(message, public_key, signature)
163
+ # valid # => true
164
+ #
165
+ # @see #valid_sig?
166
+ #
167
+ # @param [String] message A message to be signed with binary format.
168
+ # @param [PublicKey] public_key The public key with binary format.
169
+ # @param [Signature] signature The signature with binary format.
170
+ #
171
+ # @raise [Schnorr::InvalidSignatureError] if the signature is invalid.
172
+ #
173
+ # @return [Boolean] whether signature is valid.
174
+ #
175
+ def check_sig!(message, public_key, signature)
176
+ signature = Schnorr::Signature.decode([signature].pack('H*'))
177
+ Schnorr.check_sig!([message].pack('H*'), [public_key].pack('H*'), signature.encode)
178
+ end
179
+
100
180
  private
101
181
 
102
182
  # Finds a shared key between two keys
103
183
  #
104
184
  # @api private
105
185
  #
106
- # @param private_key [String] 32-bytes hex-encoded private key.
107
- # @param public_key [String] 32-bytes hex-encoded public key.
186
+ # @param private_key [PrivateKey] 32-bytes hex-encoded private key.
187
+ # @param public_key [PublicKey] 32-bytes hex-encoded public key.
108
188
  #
109
189
  # @return [String] A shared key used in the event's content encryption and decryption.
110
190
  #
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Base error class
5
+ class Error < StandardError
6
+ end
7
+ 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