discordrb 3.6.0 → 3.7.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/.github/workflows/release.yml +10 -4
- data/.rubocop.yml +7 -0
- data/CHANGELOG.md +60 -3
- data/discordrb.gemspec +1 -1
- data/lib/discordrb/api/channel.rb +3 -3
- data/lib/discordrb/api/server.rb +2 -2
- data/lib/discordrb/api/user.rb +9 -16
- data/lib/discordrb/api.rb +5 -0
- data/lib/discordrb/bot.rb +24 -4
- data/lib/discordrb/container.rb +15 -0
- data/lib/discordrb/data/channel.rb +72 -2
- data/lib/discordrb/data/collectibles.rb +5 -3
- data/lib/discordrb/data/emoji.rb +1 -1
- data/lib/discordrb/data/interaction.rb +41 -9
- data/lib/discordrb/data/member.rb +39 -5
- data/lib/discordrb/data/message.rb +52 -25
- data/lib/discordrb/data/overwrite.rb +2 -3
- data/lib/discordrb/data/primary_server.rb +1 -1
- data/lib/discordrb/data/profile.rb +21 -40
- data/lib/discordrb/data/reaction.rb +25 -1
- data/lib/discordrb/data/role_subscription.rb +41 -0
- data/lib/discordrb/data/server.rb +6 -4
- data/lib/discordrb/data.rb +1 -0
- data/lib/discordrb/events/interactions.rb +7 -1
- data/lib/discordrb/events/message.rb +32 -3
- data/lib/discordrb/events/reactions.rb +58 -0
- data/lib/discordrb/gateway.rb +2 -5
- data/lib/discordrb/id_object.rb +1 -1
- data/lib/discordrb/permissions.rb +2 -3
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/network.rb +64 -52
- data/lib/discordrb/voice/opcodes.rb +29 -0
- data/lib/discordrb/voice/sodium.rb +137 -78
- data/lib/discordrb/voice/timer.rb +19 -0
- data/lib/discordrb/voice/voice_bot.rb +20 -42
- data/lib/discordrb.rb +3 -1
- metadata +10 -10
data/lib/discordrb/gateway.rb
CHANGED
|
@@ -298,9 +298,7 @@ module Discordrb
|
|
|
298
298
|
send_identify(@token, {
|
|
299
299
|
os: RUBY_PLATFORM,
|
|
300
300
|
browser: 'discordrb',
|
|
301
|
-
device: 'discordrb'
|
|
302
|
-
referrer: '',
|
|
303
|
-
referring_domain: ''
|
|
301
|
+
device: 'discordrb'
|
|
304
302
|
}, compress, LARGE_THRESHOLD, @shard_key, @intents)
|
|
305
303
|
end
|
|
306
304
|
|
|
@@ -677,9 +675,8 @@ module Discordrb
|
|
|
677
675
|
LOGGER.log_exception(e)
|
|
678
676
|
end
|
|
679
677
|
|
|
680
|
-
# rubocop:disable Lint/UselessConstantScoping
|
|
681
678
|
ZLIB_SUFFIX = "\x00\x00\xFF\xFF".b.freeze
|
|
682
|
-
|
|
679
|
+
private_constant :ZLIB_SUFFIX
|
|
683
680
|
|
|
684
681
|
def handle_message(msg)
|
|
685
682
|
case @compress_mode
|
data/lib/discordrb/id_object.rb
CHANGED
|
@@ -145,9 +145,8 @@ module Discordrb
|
|
|
145
145
|
|
|
146
146
|
# Comparison based on permission bits
|
|
147
147
|
def ==(other)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# rubocop:enable Lint/Void
|
|
148
|
+
return false unless other.is_a?(Discordrb::Permissions)
|
|
149
|
+
|
|
151
150
|
bits == other.bits
|
|
152
151
|
end
|
|
153
152
|
end
|
data/lib/discordrb/version.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'socket'
|
|
|
5
5
|
require 'json'
|
|
6
6
|
|
|
7
7
|
require 'discordrb/websocket'
|
|
8
|
+
require 'discordrb/voice/opcodes'
|
|
8
9
|
|
|
9
10
|
begin
|
|
10
11
|
LIBSODIUM_AVAILABLE = if ENV['DISCORDRB_NONACL']
|
|
@@ -22,14 +23,14 @@ module Discordrb::Voice
|
|
|
22
23
|
# Signifies to Discord that encryption should be used
|
|
23
24
|
# @deprecated Discord now supports multiple encryption options.
|
|
24
25
|
# TODO: Resolve replacement for this constant.
|
|
25
|
-
ENCRYPTED_MODE = '
|
|
26
|
+
ENCRYPTED_MODE = 'aead_xchacha20_poly1305_rtpsize'
|
|
26
27
|
|
|
27
28
|
# Signifies to Discord that no encryption should be used
|
|
28
29
|
# @deprecated Discord no longer supports unencrypted voice communication.
|
|
29
30
|
PLAIN_MODE = 'plain'
|
|
30
31
|
|
|
31
32
|
# Encryption modes supported by Discord
|
|
32
|
-
ENCRYPTION_MODES = %w[
|
|
33
|
+
ENCRYPTION_MODES = %w[aead_xchacha20_poly1305_rtpsize].freeze
|
|
33
34
|
|
|
34
35
|
# Represents a UDP connection to a voice server. This connection is used to send the actual audio data.
|
|
35
36
|
class VoiceUDP
|
|
@@ -82,15 +83,11 @@ module Discordrb::Voice
|
|
|
82
83
|
# sequence number multiplied by 960)
|
|
83
84
|
def send_audio(buf, sequence, time)
|
|
84
85
|
# Header of the audio packet
|
|
85
|
-
header =
|
|
86
|
+
header = generate_header(sequence, time)
|
|
86
87
|
|
|
87
|
-
nonce = generate_nonce
|
|
88
|
-
buf = encrypt_audio(buf, nonce)
|
|
89
|
-
|
|
90
|
-
data = header + buf
|
|
91
|
-
|
|
92
|
-
# xsalsa20_poly1305 does not require an appended nonce
|
|
93
|
-
data += nonce unless @mode == 'xsalsa20_poly1305'
|
|
88
|
+
nonce = generate_nonce
|
|
89
|
+
buf = encrypt_audio(buf, header, nonce)
|
|
90
|
+
data = header + buf + nonce.byteslice(0, 4)
|
|
94
91
|
|
|
95
92
|
send_packet(data)
|
|
96
93
|
end
|
|
@@ -120,46 +117,44 @@ module Discordrb::Voice
|
|
|
120
117
|
|
|
121
118
|
# Encrypts audio data using libsodium
|
|
122
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
|
|
123
121
|
# @param nonce [String] The nonce to be used to encrypt the data
|
|
124
122
|
# @return [String] the audio data, encrypted
|
|
125
|
-
def encrypt_audio(buf, nonce)
|
|
123
|
+
def encrypt_audio(buf, header, nonce)
|
|
126
124
|
raise 'No secret key found, despite encryption being enabled!' unless @secret_key
|
|
127
125
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
case @mode
|
|
127
|
+
when 'aead_xchacha20_poly1305_rtpsize'
|
|
128
|
+
Discordrb::Voice::XChaCha20AEAD.encrypt(buf, header, nonce, @secret_key)
|
|
129
|
+
else
|
|
130
|
+
raise "`#{@mode}' is not a supported encryption mode"
|
|
131
|
+
end
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
def send_packet(packet)
|
|
135
135
|
@socket.send(packet, 0, @ip, @port)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
# @param header [String] The header of the packet, to be used as the nonce
|
|
139
138
|
# @return [String]
|
|
140
|
-
|
|
141
|
-
# The nonce generated depends on the encryption mode.
|
|
142
|
-
# In xsalsa20_poly1305 the nonce is the header plus twelve null bytes for padding.
|
|
143
|
-
# In xsalsa20_poly1305_suffix, the nonce is 24 random bytes
|
|
144
|
-
# In xsalsa20_poly1305_lite, the nonce is an incremental 4 byte int.
|
|
145
|
-
def generate_nonce(header)
|
|
139
|
+
def generate_nonce
|
|
146
140
|
case @mode
|
|
147
|
-
when '
|
|
148
|
-
|
|
149
|
-
when 'xsalsa20_poly1305_suffix'
|
|
150
|
-
Random.urandom(24)
|
|
151
|
-
when 'xsalsa20_poly1305_lite'
|
|
152
|
-
case @lite_nonce
|
|
141
|
+
when 'aead_xchacha20_poly1305_rtpsize'
|
|
142
|
+
case @incremental_nonce
|
|
153
143
|
when nil, 0xff_ff_ff_ff
|
|
154
|
-
@
|
|
144
|
+
@incremental_nonce = 0
|
|
155
145
|
else
|
|
156
|
-
@
|
|
146
|
+
@incremental_nonce += 1
|
|
157
147
|
end
|
|
158
|
-
[@
|
|
148
|
+
[@incremental_nonce].pack('N').ljust(24, "\0")
|
|
159
149
|
else
|
|
160
150
|
raise "`#{@mode}' is not a supported encryption mode"
|
|
161
151
|
end
|
|
162
152
|
end
|
|
153
|
+
|
|
154
|
+
# @return [String]
|
|
155
|
+
def generate_header(sequence, time)
|
|
156
|
+
[0x80, 0x78, sequence, time, @ssrc].pack('CCnNN')
|
|
157
|
+
end
|
|
163
158
|
end
|
|
164
159
|
|
|
165
160
|
# Represents a websocket client connection to the voice server. The websocket connection (sometimes called vWS) is
|
|
@@ -167,7 +162,7 @@ module Discordrb::Voice
|
|
|
167
162
|
# circle around users on Discord, and obtaining UDP connection info.
|
|
168
163
|
class VoiceWS
|
|
169
164
|
# The version of the voice gateway that's supposed to be used.
|
|
170
|
-
VOICE_GATEWAY_VERSION =
|
|
165
|
+
VOICE_GATEWAY_VERSION = 8
|
|
171
166
|
|
|
172
167
|
# @return [VoiceUDP] the UDP voice connection over which the actual audio data is sent.
|
|
173
168
|
attr_reader :udp
|
|
@@ -186,7 +181,7 @@ module Discordrb::Voice
|
|
|
186
181
|
@token = token
|
|
187
182
|
@session = session
|
|
188
183
|
|
|
189
|
-
@endpoint = endpoint
|
|
184
|
+
@endpoint = endpoint
|
|
190
185
|
|
|
191
186
|
@udp = VoiceUDP.new
|
|
192
187
|
end
|
|
@@ -197,15 +192,15 @@ module Discordrb::Voice
|
|
|
197
192
|
# @param session_id [String] The voice session ID
|
|
198
193
|
# @param token [String] The Discord authentication token
|
|
199
194
|
def send_init(server_id, bot_user_id, session_id, token)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
send_opcode(
|
|
196
|
+
Opcodes::IDENTIFY,
|
|
197
|
+
{
|
|
203
198
|
server_id: server_id,
|
|
204
199
|
user_id: bot_user_id,
|
|
205
200
|
session_id: session_id,
|
|
206
201
|
token: token
|
|
207
202
|
}
|
|
208
|
-
|
|
203
|
+
)
|
|
209
204
|
end
|
|
210
205
|
|
|
211
206
|
# Sends the UDP connection packet (op 1)
|
|
@@ -213,9 +208,9 @@ module Discordrb::Voice
|
|
|
213
208
|
# @param port [Integer] The port to bind UDP to
|
|
214
209
|
# @param mode [Object] Which mode to use for the voice connection
|
|
215
210
|
def send_udp_connection(ip, port, mode)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
211
|
+
send_opcode(
|
|
212
|
+
Opcodes::SELECT_PROTOCOL,
|
|
213
|
+
{
|
|
219
214
|
protocol: 'udp',
|
|
220
215
|
data: {
|
|
221
216
|
address: ip,
|
|
@@ -223,7 +218,7 @@ module Discordrb::Voice
|
|
|
223
218
|
mode: mode
|
|
224
219
|
}
|
|
225
220
|
}
|
|
226
|
-
|
|
221
|
+
)
|
|
227
222
|
end
|
|
228
223
|
|
|
229
224
|
# Send a heartbeat (op 3), has to be done every @heartbeat_interval seconds or the connection will terminate
|
|
@@ -231,22 +226,33 @@ module Discordrb::Voice
|
|
|
231
226
|
millis = Time.now.strftime('%s%L').to_i
|
|
232
227
|
@bot.debug("Sending voice heartbeat at #{millis}")
|
|
233
228
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
229
|
+
send_opcode(
|
|
230
|
+
Opcodes::HEARTBEAT,
|
|
231
|
+
{
|
|
232
|
+
t: millis,
|
|
233
|
+
seq_ack: @seq
|
|
234
|
+
}
|
|
235
|
+
)
|
|
238
236
|
end
|
|
239
237
|
|
|
240
238
|
# Send a speaking packet (op 5). This determines the green circle around the avatar in the voice channel
|
|
241
239
|
# @param value [true, false, Integer] Whether or not the bot should be speaking, can also be a bitmask denoting audio type.
|
|
242
240
|
def send_speaking(value)
|
|
243
241
|
@bot.debug("Speaking: #{value}")
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
send_opcode(
|
|
243
|
+
Opcodes::SPEAKING,
|
|
244
|
+
{
|
|
247
245
|
speaking: value,
|
|
248
246
|
delay: 0
|
|
249
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
|
|
250
256
|
}.to_json)
|
|
251
257
|
end
|
|
252
258
|
|
|
@@ -265,8 +271,10 @@ module Discordrb::Voice
|
|
|
265
271
|
@bot.debug("Received VWS message! #{msg}")
|
|
266
272
|
packet = JSON.parse(msg)
|
|
267
273
|
|
|
274
|
+
@seq = packet['seq'] if packet['seq']
|
|
275
|
+
|
|
268
276
|
case packet['op']
|
|
269
|
-
when
|
|
277
|
+
when Discordrb::Voice::Opcodes::READY
|
|
270
278
|
# Opcode 2 contains data to initialize the UDP connection
|
|
271
279
|
@ws_data = packet['d']
|
|
272
280
|
|
|
@@ -277,16 +285,20 @@ module Discordrb::Voice
|
|
|
277
285
|
|
|
278
286
|
@udp.connect(@ws_data['ip'], @port, @ssrc)
|
|
279
287
|
@udp.send_discovery
|
|
280
|
-
when
|
|
288
|
+
when Discordrb::Voice::Opcodes::SESSION_DESCRIPTION
|
|
281
289
|
# Opcode 4 sends the secret key used for encryption
|
|
282
290
|
@ws_data = packet['d']
|
|
283
291
|
|
|
292
|
+
# Reset the sequence when starting a new session
|
|
293
|
+
@seq = 0
|
|
294
|
+
|
|
284
295
|
@ready = true
|
|
285
296
|
@udp.secret_key = @ws_data['secret_key'].pack('C*')
|
|
286
297
|
@udp.mode = @ws_data['mode']
|
|
287
|
-
when
|
|
298
|
+
when Discordrb::Voice::Opcodes::HELLO
|
|
288
299
|
# Opcode 8 contains the heartbeat interval.
|
|
289
300
|
@heartbeat_interval = packet['d']['heartbeat_interval']
|
|
301
|
+
send_heartbeat
|
|
290
302
|
end
|
|
291
303
|
end
|
|
292
304
|
|
|
@@ -347,7 +359,7 @@ module Discordrb::Voice
|
|
|
347
359
|
end
|
|
348
360
|
|
|
349
361
|
def init_ws
|
|
350
|
-
host = "wss://#{@endpoint}
|
|
362
|
+
host = "wss://#{@endpoint}/?v=#{VOICE_GATEWAY_VERSION}"
|
|
351
363
|
@bot.debug("Connecting VWS to host: #{host}")
|
|
352
364
|
|
|
353
365
|
# Connect the WS
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Voice websocket opcodes
|
|
4
|
+
# @!visibility private
|
|
5
|
+
module Discordrb::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
|
|
@@ -1,98 +1,157 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'ffi'
|
|
4
|
+
require 'securerandom'
|
|
4
5
|
|
|
6
|
+
# :nodoc:
|
|
5
7
|
module Discordrb::Voice
|
|
6
8
|
# @!visibility private
|
|
7
9
|
module Sodium
|
|
8
|
-
extend
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
attach_function
|
|
20
|
-
|
|
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
|
|
21
72
|
end
|
|
22
73
|
|
|
23
|
-
|
|
24
|
-
# @!visibility private
|
|
25
|
-
class SecretBox
|
|
26
|
-
# Exception raised when a key or nonce with invalid length is used
|
|
27
|
-
class LengthError < RuntimeError
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Exception raised when encryption or decryption fails
|
|
31
|
-
class CryptoError < RuntimeError
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Required key length
|
|
35
|
-
KEY_LENGTH = Sodium.crypto_secretbox_xsalsa20poly1305_keybytes
|
|
36
|
-
|
|
37
|
-
# Required nonce length
|
|
38
|
-
NONCE_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_noncebytes
|
|
39
|
-
|
|
40
|
-
# Zero byte padding for encryption
|
|
41
|
-
ZERO_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_zerobytes
|
|
42
|
-
|
|
43
|
-
# Zero byte padding for decryption
|
|
44
|
-
BOX_ZERO_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_boxzerobytes
|
|
74
|
+
Sodium.sodium_init
|
|
45
75
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
49
81
|
|
|
50
|
-
|
|
82
|
+
# Generates a random key
|
|
83
|
+
# @return [String] binary key
|
|
84
|
+
def self.generate_key
|
|
85
|
+
SecureRandom.random_bytes(KEY_BYTES)
|
|
51
86
|
end
|
|
52
87
|
|
|
53
|
-
#
|
|
54
|
-
# @
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
raise(LengthError, 'Nonce length') if nonce.bytesize != NONCE_BYTES
|
|
58
|
-
|
|
59
|
-
message_padded = prepend_zeroes(ZERO_BYTES, message)
|
|
60
|
-
buffer = zero_string(message_padded.bytesize)
|
|
61
|
-
|
|
62
|
-
success = Sodium.crypto_secretbox_xsalsa20poly1305(buffer, message_padded, message_padded.bytesize, nonce, @key)
|
|
63
|
-
raise(CryptoError, "Encryption failed (#{success})") unless success.zero?
|
|
64
|
-
|
|
65
|
-
remove_zeroes(BOX_ZERO_BYTES, buffer)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Decrypts the given ciphertext using this box's key
|
|
69
|
-
# @param nonce [String] encryption nonce for this ciphertext
|
|
70
|
-
# @param ciphertext [String] ciphertext to decrypt
|
|
71
|
-
def open(nonce, ciphertext)
|
|
72
|
-
raise(LengthError, 'Nonce length') if nonce.bytesize != NONCE_BYTES
|
|
73
|
-
|
|
74
|
-
ct_padded = prepend_zeroes(BOX_ZERO_BYTES, ciphertext)
|
|
75
|
-
buffer = zero_string(ct_padded.bytesize)
|
|
76
|
-
|
|
77
|
-
success = Sodium.crypto_secretbox_xsalsa20poly1305_open(buffer, ct_padded, ct_padded.bytesize, nonce, @key)
|
|
78
|
-
raise(CryptoError, "Decryption failed (#{success})") unless success.zero?
|
|
79
|
-
|
|
80
|
-
remove_zeroes(ZERO_BYTES, buffer)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
private
|
|
84
|
-
|
|
85
|
-
def zero_string(size)
|
|
86
|
-
str = "\0" * size
|
|
87
|
-
str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding)
|
|
88
|
+
# Generates a random nonce
|
|
89
|
+
# @return [String] binary nonce
|
|
90
|
+
def self.generate_nonce
|
|
91
|
+
SecureRandom.random_bytes(NONCE_BYTES)
|
|
88
92
|
end
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
92
124
|
end
|
|
93
125
|
|
|
94
|
-
|
|
95
|
-
|
|
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)
|
|
96
155
|
end
|
|
97
156
|
end
|
|
98
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 Discordrb::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
|
+
Discordrb::Voice::WinTimer.time_begin_period(1)
|
|
17
|
+
|
|
18
|
+
at_exit { Discordrb::Voice::WinTimer.time_end_period(1) }
|
|
19
|
+
end
|