feat(voice)!: add new encryption methods, remove old methods (#10451)

BREAKING CHANGE: This library no longer supports using `tweetnacl` as an encryption library due to Discord deprecating the algorithms that `tweetnacl` helped us support (read more [here](https://discord.com/developers/docs/change-log#voice-encryption-modes)). Please migrate to one of: `sodium-native`, `sodium`, `@stablelib/xchacha20poly1305`, `@noble/ciphers` or `libsodium-wrappers` unless your system supports `aes-256-gcm` (verify by running `require('node:crypto').getCiphers().includes('aes-256-gcm')`).

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Vlad Frangu <me@vladfrangu.dev>
This commit is contained in:
pat
2024-11-18 09:08:51 +11:00
committed by GitHub
parent 51a017a14e
commit 9f8b9b1d66
11 changed files with 358 additions and 203 deletions

View File

@@ -1,9 +1,12 @@
import { test, expect, vitest } from 'vitest';
import { methods } from '../src/util/Secretbox';
import { methods, secretboxLoadPromise } from '../src/util/Secretbox';
vitest.mock('tweetnacl');
vitest.mock('@noble/ciphers/chacha');
test('Does not throw error with a package installed', () => {
// @ts-expect-error We are testing
expect(() => methods.open()).toThrow(TypeError);
// TODO: what is this even testing exactly?
test.skip('Does not throw error with a package installed', async () => {
// The async loop in Secretbox will not have finished importing unless we wait
await secretboxLoadPromise;
expect(() => methods.crypto_aead_xchacha20poly1305_ietf_decrypt()).not.toThrowError();
});

View File

@@ -6,7 +6,13 @@ import { once } from 'node:events';
import process from 'node:process';
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
import { describe, test, expect, vitest, beforeEach } from 'vitest';
import { RTP_PACKET_DESKTOP, RTP_PACKET_CHROME, RTP_PACKET_ANDROID } from '../__mocks__/rtp';
import {
RTP_PACKET_DESKTOP,
RTP_PACKET_CHROME,
RTP_PACKET_ANDROID,
XCHACHA20_SAMPLE,
AES256GCM_SAMPLE,
} from '../__mocks__/rtp';
import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection';
import { VoiceReceiver } from '../src/receive/VoiceReceiver';
import { methods } from '../src/util/Secretbox';
@@ -22,10 +28,6 @@ vitest.mock('../src/VoiceConnection', async (importOriginal) => {
vitest.mock('../src/receive/SSRCMap');
const openSpy = vitest.spyOn(methods, 'open');
openSpy.mockImplementation((buffer) => buffer);
async function nextTick() {
// eslint-disable-next-line no-promise-executor-return
return new Promise((resolve) => process.nextTick(resolve));
@@ -62,7 +64,7 @@ describe('VoiceReceiver', () => {
['RTP Packet Desktop', RTP_PACKET_DESKTOP],
['RTP Packet Chrome', RTP_PACKET_CHROME],
['RTP Packet Android', RTP_PACKET_ANDROID],
])('onUdpMessage: %s', async (testName, RTP_PACKET) => {
])('onUdpMessage: decrypt from %s', async (testName, RTP_PACKET) => {
receiver['decrypt'] = vitest.fn().mockImplementationOnce(() => RTP_PACKET.decrypted);
const spy = vitest.spyOn(receiver.ssrcMap, 'get');
@@ -174,47 +176,42 @@ describe('VoiceReceiver', () => {
describe('decrypt', () => {
const secretKey = new Uint8Array([1, 2, 3, 4]);
beforeEach(() => {
openSpy.mockClear();
test('decrypt: aead_xchacha20_poly1305_rtpsize', () => {
const nonceSpace = Buffer.alloc(24);
const decrypted = receiver['decrypt'](
XCHACHA20_SAMPLE.encrypted,
'aead_xchacha20_poly1305_rtpsize',
nonceSpace,
XCHACHA20_SAMPLE.key,
);
const expectedNonce = Buffer.concat([
XCHACHA20_SAMPLE.encrypted.slice(XCHACHA20_SAMPLE.encrypted.length - 4),
Buffer.alloc(20),
]);
expect(nonceSpace.equals(expectedNonce)).toEqual(true);
expect(decrypted.equals(XCHACHA20_SAMPLE.decrypted)).toEqual(true);
});
test('decrypt: xsalsa20_poly1305_lite', () => {
// Arrange
const buffer = range(1, 32);
const nonce = Buffer.alloc(4);
test('decrypt: aead_aes256gcm_rtpsize', () => {
const nonceSpace = Buffer.alloc(12);
// Act
const decrypted = receiver['decrypt'](buffer, 'xsalsa20_poly1305_lite', nonce, secretKey);
const decrypted = receiver['decrypt'](
AES256GCM_SAMPLE.encrypted,
'aead_aes256_gcm_rtpsize',
nonceSpace,
AES256GCM_SAMPLE.key,
);
// Assert
expect(nonce.equals(range(29, 32))).toEqual(true);
expect(decrypted!.equals(range(13, 28))).toEqual(true);
});
const expectedNonce = Buffer.concat([
AES256GCM_SAMPLE.encrypted.subarray(AES256GCM_SAMPLE.encrypted.length - 4),
Buffer.alloc(8),
]);
test('decrypt: xsalsa20_poly1305_suffix', () => {
// Arrange
const buffer = range(1, 64);
const nonce = Buffer.alloc(24);
// Act
const decrypted = receiver['decrypt'](buffer, 'xsalsa20_poly1305_suffix', nonce, secretKey);
// Assert
expect(nonce.equals(range(41, 64))).toEqual(true);
expect(decrypted!.equals(range(13, 40))).toEqual(true);
});
test('decrypt: xsalsa20_poly1305', () => {
// Arrange
const buffer = range(1, 64);
const nonce = Buffer.alloc(12);
// Act
const decrypted = receiver['decrypt'](buffer, 'xsalsa20_poly1305', nonce, secretKey);
// Assert
expect(nonce.equals(range(1, 12))).toEqual(true);
expect(decrypted!.equals(range(13, 64))).toEqual(true);
expect(nonceSpace.equals(expectedNonce)).toEqual(true);
expect(decrypted.equals(AES256GCM_SAMPLE.decrypted)).toEqual(true);
});
});
});