onyxcord 1.1.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 (133) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/CONTRIBUTING.md +13 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  8. data/.github/pull_request_template.md +37 -0
  9. data/.github/workflows/ci.yml +78 -0
  10. data/.github/workflows/codeql.yml +65 -0
  11. data/.github/workflows/deploy.yml +54 -0
  12. data/.github/workflows/release.yml +51 -0
  13. data/.gitignore +16 -0
  14. data/.markdownlint.json +4 -0
  15. data/.overcommit.yml +7 -0
  16. data/.rspec +2 -0
  17. data/.rubocop.yml +129 -0
  18. data/.yardopts +1 -0
  19. data/CHANGELOG.md +0 -0
  20. data/Gemfile +7 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +305 -0
  23. data/Rakefile +17 -0
  24. data/bin/console +15 -0
  25. data/bin/setup +7 -0
  26. data/lib/onyxcord/allowed_mentions.rb +43 -0
  27. data/lib/onyxcord/api/application.rb +316 -0
  28. data/lib/onyxcord/api/channel.rb +700 -0
  29. data/lib/onyxcord/api/interaction.rb +67 -0
  30. data/lib/onyxcord/api/invite.rb +44 -0
  31. data/lib/onyxcord/api/server.rb +775 -0
  32. data/lib/onyxcord/api/user.rb +158 -0
  33. data/lib/onyxcord/api/webhook.rb +163 -0
  34. data/lib/onyxcord/api.rb +335 -0
  35. data/lib/onyxcord/await.rb +51 -0
  36. data/lib/onyxcord/bot.rb +1971 -0
  37. data/lib/onyxcord/cache.rb +326 -0
  38. data/lib/onyxcord/colour_rgb.rb +43 -0
  39. data/lib/onyxcord/commands/command_bot.rb +511 -0
  40. data/lib/onyxcord/commands/container.rb +112 -0
  41. data/lib/onyxcord/commands/events.rb +11 -0
  42. data/lib/onyxcord/commands/parser.rb +327 -0
  43. data/lib/onyxcord/commands/rate_limiter.rb +144 -0
  44. data/lib/onyxcord/configuration.rb +125 -0
  45. data/lib/onyxcord/container.rb +988 -0
  46. data/lib/onyxcord/data/activity.rb +271 -0
  47. data/lib/onyxcord/data/application.rb +341 -0
  48. data/lib/onyxcord/data/attachment.rb +91 -0
  49. data/lib/onyxcord/data/audit_logs.rb +438 -0
  50. data/lib/onyxcord/data/avatar_decoration.rb +26 -0
  51. data/lib/onyxcord/data/call.rb +22 -0
  52. data/lib/onyxcord/data/channel.rb +1355 -0
  53. data/lib/onyxcord/data/channel_tag.rb +69 -0
  54. data/lib/onyxcord/data/collectibles.rb +47 -0
  55. data/lib/onyxcord/data/component.rb +583 -0
  56. data/lib/onyxcord/data/embed.rb +258 -0
  57. data/lib/onyxcord/data/emoji.rb +123 -0
  58. data/lib/onyxcord/data/install_params.rb +24 -0
  59. data/lib/onyxcord/data/integration.rb +144 -0
  60. data/lib/onyxcord/data/interaction.rb +1141 -0
  61. data/lib/onyxcord/data/invite.rb +137 -0
  62. data/lib/onyxcord/data/member.rb +528 -0
  63. data/lib/onyxcord/data/message.rb +612 -0
  64. data/lib/onyxcord/data/message_activity.rb +41 -0
  65. data/lib/onyxcord/data/overwrite.rb +109 -0
  66. data/lib/onyxcord/data/poll.rb +365 -0
  67. data/lib/onyxcord/data/primary_server.rb +60 -0
  68. data/lib/onyxcord/data/profile.rb +79 -0
  69. data/lib/onyxcord/data/reaction.rb +64 -0
  70. data/lib/onyxcord/data/recipient.rb +34 -0
  71. data/lib/onyxcord/data/role.rb +449 -0
  72. data/lib/onyxcord/data/role_connection_data.rb +69 -0
  73. data/lib/onyxcord/data/role_subscription.rb +41 -0
  74. data/lib/onyxcord/data/scheduled_event.rb +513 -0
  75. data/lib/onyxcord/data/server.rb +1614 -0
  76. data/lib/onyxcord/data/server_preview.rb +68 -0
  77. data/lib/onyxcord/data/snapshot.rb +112 -0
  78. data/lib/onyxcord/data/team.rb +98 -0
  79. data/lib/onyxcord/data/timestamp.rb +69 -0
  80. data/lib/onyxcord/data/user.rb +324 -0
  81. data/lib/onyxcord/data/voice_region.rb +46 -0
  82. data/lib/onyxcord/data/voice_state.rb +41 -0
  83. data/lib/onyxcord/data/webhook.rb +238 -0
  84. data/lib/onyxcord/data.rb +57 -0
  85. data/lib/onyxcord/errors.rb +246 -0
  86. data/lib/onyxcord/event_executor.rb +80 -0
  87. data/lib/onyxcord/events/await.rb +48 -0
  88. data/lib/onyxcord/events/bans.rb +60 -0
  89. data/lib/onyxcord/events/channels.rb +225 -0
  90. data/lib/onyxcord/events/generic.rb +129 -0
  91. data/lib/onyxcord/events/guilds.rb +269 -0
  92. data/lib/onyxcord/events/integrations.rb +100 -0
  93. data/lib/onyxcord/events/interactions.rb +624 -0
  94. data/lib/onyxcord/events/invites.rb +127 -0
  95. data/lib/onyxcord/events/lifetime.rb +31 -0
  96. data/lib/onyxcord/events/members.rb +110 -0
  97. data/lib/onyxcord/events/message.rb +399 -0
  98. data/lib/onyxcord/events/polls.rb +118 -0
  99. data/lib/onyxcord/events/presence.rb +131 -0
  100. data/lib/onyxcord/events/raw.rb +74 -0
  101. data/lib/onyxcord/events/reactions.rb +218 -0
  102. data/lib/onyxcord/events/roles.rb +87 -0
  103. data/lib/onyxcord/events/scheduled_events.rb +171 -0
  104. data/lib/onyxcord/events/threads.rb +100 -0
  105. data/lib/onyxcord/events/typing.rb +73 -0
  106. data/lib/onyxcord/events/voice_server_update.rb +48 -0
  107. data/lib/onyxcord/events/voice_state_update.rb +106 -0
  108. data/lib/onyxcord/events/webhooks.rb +65 -0
  109. data/lib/onyxcord/gateway.rb +890 -0
  110. data/lib/onyxcord/id_object.rb +39 -0
  111. data/lib/onyxcord/light/data.rb +62 -0
  112. data/lib/onyxcord/light/integrations.rb +73 -0
  113. data/lib/onyxcord/light/light_bot.rb +58 -0
  114. data/lib/onyxcord/light.rb +8 -0
  115. data/lib/onyxcord/logger.rb +120 -0
  116. data/lib/onyxcord/message_components.rb +70 -0
  117. data/lib/onyxcord/paginator.rb +60 -0
  118. data/lib/onyxcord/permissions.rb +255 -0
  119. data/lib/onyxcord/rate_limiter/gateway.rb +42 -0
  120. data/lib/onyxcord/rate_limiter/rest.rb +89 -0
  121. data/lib/onyxcord/version.rb +7 -0
  122. data/lib/onyxcord/voice/encoder.rb +115 -0
  123. data/lib/onyxcord/voice/network.rb +380 -0
  124. data/lib/onyxcord/voice/opcodes.rb +29 -0
  125. data/lib/onyxcord/voice/sodium.rb +157 -0
  126. data/lib/onyxcord/voice/timer.rb +19 -0
  127. data/lib/onyxcord/voice/voice_bot.rb +386 -0
  128. data/lib/onyxcord/webhooks.rb +14 -0
  129. data/lib/onyxcord/websocket.rb +62 -0
  130. data/lib/onyxcord.rb +180 -0
  131. data/onyxcord-webhooks.gemspec +30 -0
  132. data/onyxcord.gemspec +50 -0
  133. metadata +421 -0
@@ -0,0 +1,380 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'websocket-client-simple'
4
+ require 'socket'
5
+ require 'json'
6
+
7
+ require 'onyxcord/websocket'
8
+ require 'onyxcord/voice/opcodes'
9
+
10
+ begin
11
+ LIBSODIUM_AVAILABLE = if ENV['ONYXCORD_NONACL']
12
+ false
13
+ else
14
+ require 'onyxcord/voice/sodium'
15
+ end
16
+ rescue LoadError
17
+ puts "libsodium not available! You can continue to use onyxcord as normal but voice support won't work.
18
+ Read https://github.com/kruldevb/OnyxCord/wiki/Installing-libsodium for more details."
19
+ LIBSODIUM_AVAILABLE = false
20
+ end
21
+
22
+ module OnyxCord::Voice
23
+ # Signifies to Discord that encryption should be used
24
+ # @deprecated Discord now supports multiple encryption options.
25
+ # TODO: Resolve replacement for this constant.
26
+ ENCRYPTED_MODE = 'aead_xchacha20_poly1305_rtpsize'
27
+
28
+ # Signifies to Discord that no encryption should be used
29
+ # @deprecated Discord no longer supports unencrypted voice communication.
30
+ PLAIN_MODE = 'plain'
31
+
32
+ # Encryption modes supported by Discord
33
+ ENCRYPTION_MODES = %w[aead_xchacha20_poly1305_rtpsize].freeze
34
+
35
+ # Represents a UDP connection to a voice server. This connection is used to send the actual audio data.
36
+ class VoiceUDP
37
+ # @return [true, false] whether or not UDP communications are encrypted.
38
+ # @deprecated Discord no longer supports unencrypted voice communication.
39
+ attr_accessor :encrypted
40
+ alias_method :encrypted?, :encrypted
41
+
42
+ # Sets the secret key used for encryption
43
+ attr_writer :secret_key
44
+
45
+ # The UDP encryption mode
46
+ attr_reader :mode
47
+
48
+ # @!visibility private
49
+ attr_writer :mode
50
+
51
+ # Creates a new UDP connection. Only creates a socket as the discovery reply may come before the data is
52
+ # initialized.
53
+ def initialize
54
+ @socket = UDPSocket.new
55
+ @encrypted = true
56
+ end
57
+
58
+ # Initializes the UDP socket with data obtained from opcode 2.
59
+ # @param ip [String] The IP address to connect to.
60
+ # @param port [Integer] The port to connect to.
61
+ # @param ssrc [Integer] The Super Secret Relay Code (SSRC). Discord uses this to identify different voice users
62
+ # on the same endpoint.
63
+ def connect(ip, port, ssrc)
64
+ @ip = ip
65
+ @port = port
66
+ @ssrc = ssrc
67
+ end
68
+
69
+ # Waits for a UDP discovery reply, and returns the sent data.
70
+ # @return [Array(String, Integer)] the IP and port received from the discovery reply.
71
+ def receive_discovery_reply
72
+ # Wait for a UDP message
73
+ message = @socket.recv(74)
74
+ ip = message[8..-3].delete("\0")
75
+ port = message[-2..].unpack1('n')
76
+ [ip, port]
77
+ end
78
+
79
+ # Makes an audio packet from a buffer and sends it to Discord.
80
+ # @param buf [String] The audio data to send, must be exactly one Opus frame
81
+ # @param sequence [Integer] The packet sequence number, incremented by one for subsequent packets
82
+ # @param time [Integer] When this packet should be played back, in no particular unit (essentially just the
83
+ # sequence number multiplied by 960)
84
+ def send_audio(buf, sequence, time)
85
+ # Header of the audio packet
86
+ header = generate_header(sequence, time)
87
+
88
+ nonce = generate_nonce
89
+ buf = encrypt_audio(buf, header, nonce)
90
+ data = header + buf + nonce.byteslice(0, 4)
91
+
92
+ send_packet(data)
93
+ end
94
+
95
+ # Sends the UDP discovery packet with the internally stored SSRC. Discord will send a reply afterwards which can
96
+ # be received using {#receive_discovery_reply}
97
+ def send_discovery
98
+ # Create empty packet
99
+ discovery_packet = ''
100
+
101
+ # Add Type request (0x1 = request, 0x2 = response)
102
+ discovery_packet += [0x1].pack('n')
103
+
104
+ # Add Length (excluding Type and itself = 70)
105
+ discovery_packet += [70].pack('n')
106
+
107
+ # Add SSRC
108
+ discovery_packet += [@ssrc].pack('N')
109
+
110
+ # Add 66 zeroes so the packet is 74 bytes long
111
+ discovery_packet += "\0" * 66
112
+
113
+ send_packet(discovery_packet)
114
+ end
115
+
116
+ private
117
+
118
+ # Encrypts audio data using libsodium
119
+ # @param buf [String] The encoded audio data to be encrypted
120
+ # @param header [String] The RTP header of the packet, used as associated data
121
+ # @param nonce [String] The nonce to be used to encrypt the data
122
+ # @return [String] the audio data, encrypted
123
+ def encrypt_audio(buf, header, nonce)
124
+ raise 'No secret key found, despite encryption being enabled!' unless @secret_key
125
+
126
+ case @mode
127
+ when 'aead_xchacha20_poly1305_rtpsize'
128
+ OnyxCord::Voice::XChaCha20AEAD.encrypt(buf, header, nonce, @secret_key)
129
+ else
130
+ raise "`#{@mode}' is not a supported encryption mode"
131
+ end
132
+ end
133
+
134
+ def send_packet(packet)
135
+ @socket.send(packet, 0, @ip, @port)
136
+ end
137
+
138
+ # @return [String]
139
+ def generate_nonce
140
+ case @mode
141
+ when 'aead_xchacha20_poly1305_rtpsize'
142
+ case @incremental_nonce
143
+ when nil, 0xff_ff_ff_ff
144
+ @incremental_nonce = 0
145
+ else
146
+ @incremental_nonce += 1
147
+ end
148
+ [@incremental_nonce].pack('N').ljust(24, "\0")
149
+ else
150
+ raise "`#{@mode}' is not a supported encryption mode"
151
+ end
152
+ end
153
+
154
+ # @return [String]
155
+ def generate_header(sequence, time)
156
+ [0x80, 0x78, sequence, time, @ssrc].pack('CCnNN')
157
+ end
158
+ end
159
+
160
+ # Represents a websocket client connection to the voice server. The websocket connection (sometimes called vWS) is
161
+ # used to manage general data about the connection, such as sending the speaking packet, which determines the green
162
+ # circle around users on Discord, and obtaining UDP connection info.
163
+ class VoiceWS
164
+ # The version of the voice gateway that's supposed to be used.
165
+ VOICE_GATEWAY_VERSION = 8
166
+
167
+ # @return [VoiceUDP] the UDP voice connection over which the actual audio data is sent.
168
+ attr_reader :udp
169
+
170
+ # Makes a new voice websocket client, but doesn't connect it (see {#connect} for that)
171
+ # @param channel [Channel] The voice channel to connect to
172
+ # @param bot [Bot] The regular bot to which this vWS is bound
173
+ # @param token [String] The authentication token which is also used for REST requests
174
+ # @param session [String] The voice session ID Discord sends over the regular websocket
175
+ # @param endpoint [String] The endpoint URL to connect to
176
+ def initialize(channel, bot, token, session, endpoint)
177
+ raise 'libsodium is unavailable - unable to create voice bot! Please read https://github.com/kruldevb/OnyxCord/wiki/Installing-libsodium' unless LIBSODIUM_AVAILABLE
178
+
179
+ @channel = channel
180
+ @bot = bot
181
+ @token = token
182
+ @session = session
183
+
184
+ @endpoint = endpoint
185
+
186
+ @udp = VoiceUDP.new
187
+ end
188
+
189
+ # Send a connection init packet (op 0)
190
+ # @param server_id [Integer] The ID of the server to connect to
191
+ # @param bot_user_id [Integer] The ID of the bot that is connecting
192
+ # @param session_id [String] The voice session ID
193
+ # @param token [String] The Discord authentication token
194
+ def send_init(server_id, bot_user_id, session_id, token)
195
+ send_opcode(
196
+ Opcodes::IDENTIFY,
197
+ {
198
+ server_id: server_id,
199
+ user_id: bot_user_id,
200
+ session_id: session_id,
201
+ token: token
202
+ }
203
+ )
204
+ end
205
+
206
+ # Sends the UDP connection packet (op 1)
207
+ # @param ip [String] The IP to bind UDP to
208
+ # @param port [Integer] The port to bind UDP to
209
+ # @param mode [Object] Which mode to use for the voice connection
210
+ def send_udp_connection(ip, port, mode)
211
+ send_opcode(
212
+ Opcodes::SELECT_PROTOCOL,
213
+ {
214
+ protocol: 'udp',
215
+ data: {
216
+ address: ip,
217
+ port: port,
218
+ mode: mode
219
+ }
220
+ }
221
+ )
222
+ end
223
+
224
+ # Send a heartbeat (op 3), has to be done every @heartbeat_interval seconds or the connection will terminate
225
+ def send_heartbeat
226
+ millis = Time.now.strftime('%s%L').to_i
227
+ @bot.debug("Sending voice heartbeat at #{millis}")
228
+
229
+ send_opcode(
230
+ Opcodes::HEARTBEAT,
231
+ {
232
+ t: millis,
233
+ seq_ack: @seq
234
+ }
235
+ )
236
+ end
237
+
238
+ # Send a speaking packet (op 5). This determines the green circle around the avatar in the voice channel
239
+ # @param value [true, false, Integer] Whether or not the bot should be speaking, can also be a bitmask denoting audio type.
240
+ def send_speaking(value)
241
+ @bot.debug("Speaking: #{value}")
242
+ send_opcode(
243
+ Opcodes::SPEAKING,
244
+ {
245
+ speaking: value,
246
+ delay: 0
247
+ }
248
+ )
249
+ end
250
+
251
+ def send_opcode(opcode, data)
252
+ @bot.debug("Sending voice opcode #{opcode} with data: #{data}")
253
+ @client.send({
254
+ op: opcode,
255
+ d: data
256
+ }.to_json)
257
+ end
258
+
259
+ # Event handlers; public for websocket-simple to work correctly
260
+ # @!visibility private
261
+ def websocket_open
262
+ # Give the current thread a name ('Voice Web Socket Internal')
263
+ Thread.current[:onyxcord_name] = 'vws-i'
264
+
265
+ # Send the init packet
266
+ send_init(@channel.server.id, @bot.profile.id, @session, @token)
267
+ end
268
+
269
+ # @!visibility private
270
+ def websocket_message(msg)
271
+ @bot.debug("Received VWS message! #{msg}")
272
+ packet = JSON.parse(msg)
273
+
274
+ @seq = packet['seq'] if packet['seq']
275
+
276
+ case packet['op']
277
+ when OnyxCord::Voice::Opcodes::READY
278
+ # Opcode 2 contains data to initialize the UDP connection
279
+ @ws_data = packet['d']
280
+
281
+ @ssrc = @ws_data['ssrc']
282
+ @port = @ws_data['port']
283
+
284
+ @udp_mode = (ENCRYPTION_MODES & @ws_data['modes']).first
285
+
286
+ @udp.connect(@ws_data['ip'], @port, @ssrc)
287
+ @udp.send_discovery
288
+ when OnyxCord::Voice::Opcodes::SESSION_DESCRIPTION
289
+ # Opcode 4 sends the secret key used for encryption
290
+ @ws_data = packet['d']
291
+
292
+ # Reset the sequence when starting a new session
293
+ @seq = 0
294
+
295
+ @ready = true
296
+ @udp.secret_key = @ws_data['secret_key'].pack('C*')
297
+ @udp.mode = @ws_data['mode']
298
+ when OnyxCord::Voice::Opcodes::HELLO
299
+ # Opcode 8 contains the heartbeat interval.
300
+ @heartbeat_interval = packet['d']['heartbeat_interval']
301
+ send_heartbeat
302
+ end
303
+ end
304
+
305
+ # Communication goes like this:
306
+ # me discord
307
+ # | |
308
+ # websocket connect -> |
309
+ # | |
310
+ # | <- websocket opcode 2
311
+ # | |
312
+ # UDP discovery -> |
313
+ # | |
314
+ # | <- UDP reply packet
315
+ # | |
316
+ # websocket opcode 1 -> |
317
+ # | |
318
+ # ...
319
+ def connect
320
+ # Connect websocket
321
+ @thread = Thread.new do
322
+ Thread.current[:onyxcord_name] = 'vws'
323
+ init_ws
324
+ end
325
+
326
+ @bot.debug('Started websocket initialization, now waiting for UDP discovery reply')
327
+
328
+ # Now wait for opcode 2 and the resulting UDP reply packet
329
+ ip, port = @udp.receive_discovery_reply
330
+ @bot.debug("UDP discovery reply received! #{ip} #{port}")
331
+
332
+ # Send UDP init packet with received UDP data
333
+ send_udp_connection(ip, port, @udp_mode)
334
+
335
+ @bot.debug('Waiting for op 4 now')
336
+
337
+ # Wait for op 4, then finish
338
+ sleep 0.05 until @ready
339
+ end
340
+
341
+ # Disconnects the websocket and kills the thread
342
+ def destroy
343
+ @heartbeat_running = false
344
+ end
345
+
346
+ private
347
+
348
+ def heartbeat_loop
349
+ @heartbeat_running = true
350
+ while @heartbeat_running
351
+ if @heartbeat_interval
352
+ sleep @heartbeat_interval / 1000.0
353
+ send_heartbeat
354
+ else
355
+ # If no interval has been set yet, sleep a second and check again
356
+ sleep 1
357
+ end
358
+ end
359
+ end
360
+
361
+ def init_ws
362
+ host = "wss://#{@endpoint}/?v=#{VOICE_GATEWAY_VERSION}"
363
+ @bot.debug("Connecting VWS to host: #{host}")
364
+
365
+ # Connect the WS
366
+ @client = OnyxCord::WebSocket.new(
367
+ host,
368
+ method(:websocket_open),
369
+ method(:websocket_message),
370
+ proc { |e| OnyxCord::LOGGER.error "VWS error: #{e}" },
371
+ proc { |e| OnyxCord::LOGGER.warn "VWS close: #{e}" }
372
+ )
373
+
374
+ @bot.debug('VWS connected')
375
+
376
+ # Block any further execution
377
+ heartbeat_loop
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Voice websocket opcodes
4
+ # @!visibility private
5
+ module OnyxCord::Voice::Opcodes
6
+ IDENTIFY = 0
7
+ SELECT_PROTOCOL = 1
8
+ READY = 2
9
+ HEARTBEAT = 3
10
+ SESSION_DESCRIPTION = 4
11
+ SPEAKING = 5
12
+ HEARTBEAT_ACK = 6
13
+ RESUME = 7
14
+ HELLO = 8
15
+ RESUMED = 9
16
+ CLIENT_CONNECT = 10
17
+ CLIENT_DISCONNECT = 11
18
+ DAVE_PREPARE_TRANSITION = 21
19
+ DAVE_EXECUTE_TRANSITION = 22
20
+ DAVE_TRANSITION_READY = 23
21
+ DAVE_PREPARE_EPOCH = 24
22
+ DAVE_MLS_EXTERNAL_SENDER = 25
23
+ DAVE_MLS_KEY_PACKAGE = 26
24
+ DAVE_MLS_PROPOSALS = 27
25
+ DAVE_MLS_COMMIT_WELCOME = 28
26
+ DAVE_MLS_ANNOUNCE_COMMIT_TRANSITION = 29
27
+ DAVE_MLS_WELCOME = 30
28
+ DAVE_MLS_INVALID_COMMIT_WELCOME = 31
29
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require 'securerandom'
5
+
6
+ # :nodoc:
7
+ module OnyxCord::Voice
8
+ # @!visibility private
9
+ module Sodium
10
+ extend FFI::Library
11
+ ffi_lib 'sodium'
12
+
13
+ # @!group Constants
14
+
15
+ # Initializes libsodium
16
+ # @return [Integer] 0 on success
17
+ attach_function :sodium_init, [], :int
18
+
19
+ # Returns the key size (in bytes)
20
+ # @return [Integer]
21
+ attach_function :crypto_aead_xchacha20poly1305_ietf_keybytes, [], :size_t
22
+
23
+ # Returns the nonce size (in bytes)
24
+ # @return [Integer]
25
+ attach_function :crypto_aead_xchacha20poly1305_ietf_npubbytes, [], :size_t
26
+
27
+ # Returns the authentication tag size (in bytes)
28
+ # @return [Integer]
29
+ attach_function :crypto_aead_xchacha20poly1305_ietf_abytes, [], :size_t
30
+
31
+ # @!endgroup
32
+
33
+ # @!group AEAD Encrypt/Decrypt
34
+
35
+ # Performs authenticated encryption using XChaCha20-Poly1305
36
+ #
37
+ # @!macro [attach] crypto_aead_xchacha20poly1305_ietf_encrypt
38
+ # @param c [FFI::Pointer] output buffer for ciphertext
39
+ # @param clen_p [FFI::Pointer] output pointer for ciphertext length
40
+ # @param m [FFI::Pointer] input message pointer
41
+ # @param mlen [Integer] length of the message
42
+ # @param ad [FFI::Pointer] pointer to associated data
43
+ # @param adlen [Integer] length of associated data
44
+ # @param nsec [FFI::Pointer, nil] (not used, must be nil)
45
+ # @param npub [FFI::Pointer] nonce pointer
46
+ # @param k [FFI::Pointer] key pointer
47
+ # @return [Integer] 0 on success
48
+ attach_function :crypto_aead_xchacha20poly1305_ietf_encrypt, %i[
49
+ pointer pointer pointer ulong_long
50
+ pointer ulong_long
51
+ pointer pointer pointer
52
+ ], :int
53
+
54
+ # Decrypts XChaCha20-Poly1305 AEAD-encrypted data
55
+ # @!macro [attach] crypto_aead_xchacha20poly1305_ietf_decrypt
56
+ # @param m [FFI::Pointer] output buffer for decrypted message
57
+ # @param mlen_p [FFI::Pointer] output pointer for decrypted length
58
+ # @param nsec [FFI::Pointer, nil] (not used, must be nil)
59
+ # @param c [FFI::Pointer] ciphertext pointer
60
+ # @param clen [Integer] length of ciphertext
61
+ # @param ad [FFI::Pointer] pointer to associated data
62
+ # @param adlen [Integer] length of associated data
63
+ # @param npub [FFI::Pointer] nonce pointer
64
+ # @param k [FFI::Pointer] key pointer
65
+ # @return [Integer] 0 on success
66
+ attach_function :crypto_aead_xchacha20poly1305_ietf_decrypt, %i[
67
+ pointer pointer pointer pointer ulong_long
68
+ pointer ulong_long pointer pointer
69
+ ], :int
70
+
71
+ # @!endgroup
72
+ end
73
+
74
+ Sodium.sodium_init
75
+
76
+ # High-level wrapper class
77
+ class XChaCha20AEAD
78
+ KEY_BYTES = Sodium.crypto_aead_xchacha20poly1305_ietf_keybytes
79
+ NONCE_BYTES = Sodium.crypto_aead_xchacha20poly1305_ietf_npubbytes
80
+ TAG_BYTES = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes
81
+
82
+ # Generates a random key
83
+ # @return [String] binary key
84
+ def self.generate_key
85
+ SecureRandom.random_bytes(KEY_BYTES)
86
+ end
87
+
88
+ # Generates a random nonce
89
+ # @return [String] binary nonce
90
+ def self.generate_nonce
91
+ SecureRandom.random_bytes(NONCE_BYTES)
92
+ end
93
+
94
+ # Encrypts a message using XChaCha20-Poly1305
95
+ #
96
+ # @param message [String] plaintext to encrypt
97
+ # @param key [String] 32-byte encryption key
98
+ # @param nonce [String] 24-byte nonce
99
+ # @param add [String] optional associated data
100
+ # @return [String] ciphertext (includes the auth tag)
101
+ def self.encrypt(message, add, nonce, key)
102
+ raise ArgumentError, 'Invalid key size' unless key.bytesize == KEY_BYTES
103
+ raise ArgumentError, 'Invalid nonce size' unless nonce.bytesize == NONCE_BYTES
104
+
105
+ message_ptr = FFI::MemoryPointer.from_string(message)
106
+ ad_ptr = FFI::MemoryPointer.from_string(add)
107
+
108
+ c_len = message.bytesize + TAG_BYTES
109
+ ciphertext = FFI::MemoryPointer.new(:uchar, c_len)
110
+ clen_p = FFI::MemoryPointer.new(:ulong_long)
111
+
112
+ result = Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
113
+ ciphertext, clen_p,
114
+ message_ptr, message.bytesize,
115
+ ad_ptr, add.bytesize,
116
+ nil,
117
+ FFI::MemoryPointer.from_string(nonce),
118
+ FFI::MemoryPointer.from_string(key)
119
+ )
120
+
121
+ raise 'Encryption failed' unless result.zero?
122
+
123
+ ciphertext.read_string(clen_p.read_ulong_long)
124
+ end
125
+
126
+ # Decrypts a ciphertext using XChaCha20-Poly1305
127
+ #
128
+ # @param ciphertext [String] the encrypted data (with tag)
129
+ # @param key [String] 32-byte decryption key
130
+ # @param nonce [String] 24-byte nonce
131
+ # @param add [String] optional associated data
132
+ # @return [String] decrypted plaintext
133
+ def self.decrypt(ciphertext, add, nonce, key)
134
+ raise ArgumentError, 'Invalid key size' unless key.bytesize == KEY_BYTES
135
+ raise ArgumentError, 'Invalid nonce size' unless nonce.bytesize == NONCE_BYTES
136
+
137
+ c_ptr = FFI::MemoryPointer.from_string(ciphertext)
138
+ ad_ptr = FFI::MemoryPointer.from_string(add)
139
+
140
+ m_ptr = FFI::MemoryPointer.new(:uchar, ciphertext.bytesize - TAG_BYTES)
141
+ mlen_p = FFI::MemoryPointer.new(:ulong_long)
142
+
143
+ result = Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
144
+ m_ptr, mlen_p,
145
+ nil,
146
+ c_ptr, ciphertext.bytesize,
147
+ ad_ptr, add.bytesize,
148
+ FFI::MemoryPointer.from_string(nonce),
149
+ FFI::MemoryPointer.from_string(key)
150
+ )
151
+
152
+ raise 'Decryption failed' unless result.zero?
153
+
154
+ m_ptr.read_string(mlen_p.read_ulong_long)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ if RUBY_PLATFORM.match?(/mswin|mingw|windows/)
4
+
5
+ # @!visibility private
6
+ module OnyxCord::Voice::WinTimer
7
+ extend FFI::Library
8
+
9
+ ffi_lib 'winmm'
10
+
11
+ attach_function :time_begin_period, :timeBeginPeriod, [:uint], :uint
12
+
13
+ attach_function :time_end_period, :timeEndPeriod, [:uint], :uint
14
+ end
15
+
16
+ OnyxCord::Voice::WinTimer.time_begin_period(1)
17
+
18
+ at_exit { OnyxCord::Voice::WinTimer.time_end_period(1) }
19
+ end