348 lines
11 KiB
Python
348 lines
11 KiB
Python
|
|
"""
|
||
|
|
Davey
|
||
|
|
|
||
|
|
A Discord Audio & Video End-to-End Encryption (DAVE) Protocol implementation in Python.
|
||
|
|
|
||
|
|
:copyright: (c) 2025-present Snazzah
|
||
|
|
:license: MIT
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Final, List, Optional
|
||
|
|
from enum import Enum
|
||
|
|
|
||
|
|
__version__: str = ...
|
||
|
|
__author__: Final = "Snazzah"
|
||
|
|
__copyright__: Final = "Copyright 2025-present Snazzah"
|
||
|
|
__license__: Final = "MIT"
|
||
|
|
DEBUG_BUILD: bool = ...
|
||
|
|
""" Whether davey is using a debug build."""
|
||
|
|
DAVE_PROTOCOL_VERSION: int = ...
|
||
|
|
"""The maximum supported version of the DAVE protocol."""
|
||
|
|
|
||
|
|
class SigningKeyPair:
|
||
|
|
"""
|
||
|
|
A signing key pair. This is needed if you want to pass your own key pair or store the key pair for later.
|
||
|
|
|
||
|
|
:param private: The private key.
|
||
|
|
:param public: The public key.
|
||
|
|
"""
|
||
|
|
def __init__(self, private: bytes, public: bytes) -> None: ...
|
||
|
|
private: bytes
|
||
|
|
"""The private key of this key pair."""
|
||
|
|
public: bytes
|
||
|
|
"""The public key of this key pair."""
|
||
|
|
def __repr__(self) -> str: ...
|
||
|
|
|
||
|
|
def generate_p256_keypair() -> SigningKeyPair:
|
||
|
|
"""Create a P256 signing key pair."""
|
||
|
|
...
|
||
|
|
|
||
|
|
def generate_displayable_code(data: bytes, desired_length: int, group_size: int) -> str:
|
||
|
|
"""
|
||
|
|
Generate a displayable code.
|
||
|
|
|
||
|
|
See: https://daveprotocol.com/#displayable-codes
|
||
|
|
|
||
|
|
:param data: The data to generate a code with.
|
||
|
|
:param desired_length: The desired length of the code.
|
||
|
|
:param group_size: The group size of the code.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
|
||
|
|
def generate_key_fingerprint(version: int, key: bytes, user_id: int) -> bytes:
|
||
|
|
"""
|
||
|
|
Generate a key fingerprint.
|
||
|
|
|
||
|
|
See: https://daveprotocol.com/#verification-fingerprint
|
||
|
|
|
||
|
|
:param version: The version of the fingerprint.
|
||
|
|
:param key: The key to fingerprint.
|
||
|
|
:param user_id: The user ID of this fingerprint.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
|
||
|
|
def generate_pairwise_fingerprint(
|
||
|
|
version: int,
|
||
|
|
local_key: bytes,
|
||
|
|
local_user_id: int,
|
||
|
|
remote_key: bytes,
|
||
|
|
remote_user_id: int,
|
||
|
|
) -> bytes:
|
||
|
|
"""
|
||
|
|
Generate a pairwise fingerprint.
|
||
|
|
|
||
|
|
See: https://daveprotocol.com/#verification-fingerprint
|
||
|
|
|
||
|
|
:param version: The version of the fingerprint.
|
||
|
|
:param local_key: The local user's key.
|
||
|
|
:param local_user_id: The local user's ID.
|
||
|
|
:param remote_key: The remote user's key.
|
||
|
|
:param remote_user_id: The remote user's ID.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
|
||
|
|
class Codec(Enum):
|
||
|
|
"""The type of codec being used."""
|
||
|
|
|
||
|
|
unknown = 0
|
||
|
|
opus = 1
|
||
|
|
vp8 = 2
|
||
|
|
vp9 = 3
|
||
|
|
h264 = 4
|
||
|
|
h265 = 5
|
||
|
|
av1 = 6
|
||
|
|
|
||
|
|
class MediaType(Enum):
|
||
|
|
"""The type of media being referenced."""
|
||
|
|
|
||
|
|
audio = 0
|
||
|
|
video = 1
|
||
|
|
|
||
|
|
class ProposalsOperationType(Enum):
|
||
|
|
"""
|
||
|
|
The operation type of the proposals payload.
|
||
|
|
See the [DAVE Protocol Whitepaper on opcode 27](https://daveprotocol.com/#dave_mls_proposals-27) for technical details.
|
||
|
|
"""
|
||
|
|
|
||
|
|
append = 0
|
||
|
|
revoke = 1
|
||
|
|
|
||
|
|
class SessionStatus(Enum):
|
||
|
|
"""The status of the DAVE session."""
|
||
|
|
|
||
|
|
inactive = 0
|
||
|
|
pending = 1
|
||
|
|
awaiting_response = 2
|
||
|
|
active = 3
|
||
|
|
|
||
|
|
class EncryptionStats:
|
||
|
|
successes: int
|
||
|
|
"""Number of encryption successes"""
|
||
|
|
failures: int
|
||
|
|
"""Number of encryption failures"""
|
||
|
|
duration: int
|
||
|
|
"""Total encryption duration in microseconds"""
|
||
|
|
attempts: int
|
||
|
|
"""Total amounts of encryption attempts"""
|
||
|
|
max_attempts: int
|
||
|
|
"""Maximum attempts reached at encryption"""
|
||
|
|
|
||
|
|
class DecryptionStats:
|
||
|
|
successes: int
|
||
|
|
"""Number of decryption successes"""
|
||
|
|
failures: int
|
||
|
|
"""Number of decryption failures"""
|
||
|
|
duration: int
|
||
|
|
"""Total decryption duration in microseconds"""
|
||
|
|
attempts: int
|
||
|
|
"""Total amounts of decryption attempts"""
|
||
|
|
passthroughs: int
|
||
|
|
"""Total amounts of packets that passed through"""
|
||
|
|
|
||
|
|
class CommitWelcome:
|
||
|
|
"""
|
||
|
|
Contains the commit and optional welcome for
|
||
|
|
[dave_mls_commit_welcome (28)](https://daveprotocol.com/#dave_mls_commit_welcome-28).
|
||
|
|
"""
|
||
|
|
|
||
|
|
commit: bytes
|
||
|
|
welcome: Optional[bytes]
|
||
|
|
|
||
|
|
class DaveSession:
|
||
|
|
"""
|
||
|
|
A DAVE session.
|
||
|
|
|
||
|
|
:param protocol_version: The protocol version to use.
|
||
|
|
:param user_id: The user ID of the session.
|
||
|
|
:param channel_id: The channel ID of the session.
|
||
|
|
:param key_pair: The key pair to use for this session. Will generate a new one if not specified.
|
||
|
|
"""
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
protocol_version: int,
|
||
|
|
user_id: int,
|
||
|
|
channel_id: int,
|
||
|
|
key_pair: Optional[SigningKeyPair] = None,
|
||
|
|
) -> None: ...
|
||
|
|
|
||
|
|
protocol_version: int
|
||
|
|
"""he DAVE protocol version used for this session."""
|
||
|
|
user_id: int
|
||
|
|
"""The user ID for this session."""
|
||
|
|
channel_id: int
|
||
|
|
"""The channel ID (group ID in MLS standards) for this session."""
|
||
|
|
epoch: Optional[int]
|
||
|
|
"""The epoch for this session, `undefined` if there is no group yet."""
|
||
|
|
own_leaf_index: Optional[int]
|
||
|
|
"""Your own leaf index for this session, `undefined` if there is no group yet."""
|
||
|
|
ciphersuite: int
|
||
|
|
"""The ciphersuite being used in this session."""
|
||
|
|
status: SessionStatus
|
||
|
|
"""The current status of the session."""
|
||
|
|
ready: bool
|
||
|
|
"""Whether the session is ready to encrypt/decrypt."""
|
||
|
|
voice_privacy_code: Optional[str]
|
||
|
|
"""
|
||
|
|
Get the voice privacy code of this session's group.
|
||
|
|
The result of this is created and cached each time a new transition is executed.
|
||
|
|
|
||
|
|
See: https://daveprotocol.com/#displayable-codes
|
||
|
|
"""
|
||
|
|
|
||
|
|
def reset(self) -> None:
|
||
|
|
"""
|
||
|
|
Resets the session by deleting the group and clearing the storage.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def reinit(
|
||
|
|
self,
|
||
|
|
protocol_version: int,
|
||
|
|
user_id: int,
|
||
|
|
channel_id: int,
|
||
|
|
key_pair: Optional[SigningKeyPair] = None,
|
||
|
|
) -> None:
|
||
|
|
"""
|
||
|
|
Resets and re-initializes the session.
|
||
|
|
|
||
|
|
:param protocol_version: The protocol version to use.
|
||
|
|
:param user_id: The user ID of the session.
|
||
|
|
:param channel_id: The channel ID of the session.
|
||
|
|
:param key_pair: The key pair to use for this session. Will generate a new one if not specified.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def get_epoch_authenticator(self) -> Optional[bytes]:
|
||
|
|
"""Get the epoch authenticator of this session's group."""
|
||
|
|
...
|
||
|
|
def set_external_sender(self, external_sender_data: bytes) -> None:
|
||
|
|
"""
|
||
|
|
Set the external sender this session will recieve from.
|
||
|
|
See the [DAVE Protocol Whitepaper on opcode 25](https://daveprotocol.com/#dave_mls_external_sender_package-25) for technical details.
|
||
|
|
|
||
|
|
:param external_sender_data: The serialized external sender data.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def get_serialized_key_package(self) -> bytes:
|
||
|
|
"""
|
||
|
|
Create, store, and return the serialized key package buffer.
|
||
|
|
Key packages are not meant to be reused, and will be recreated on each call of this function.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def process_proposals(
|
||
|
|
self,
|
||
|
|
operation_type: ProposalsOperationType,
|
||
|
|
proposals: bytes,
|
||
|
|
expected_user_ids: Optional[List[int]] = None,
|
||
|
|
) -> Optional[CommitWelcome]:
|
||
|
|
"""
|
||
|
|
Process proposals from the voice server.
|
||
|
|
See the [DAVE Protocol Whitepaper on opcode 27](https://daveprotocol.com/#dave_mls_proposals-27) for technical details.
|
||
|
|
|
||
|
|
:param operation_type: The operation type of the proposals.
|
||
|
|
:param proposals: The operation type of the proposals.
|
||
|
|
:param expected_user_ids: The expected user IDs to come from the proposals.
|
||
|
|
If provided, this will reject unexpected user IDs.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def process_welcome(self, welcome: bytes) -> None:
|
||
|
|
"""
|
||
|
|
Process a welcome message.
|
||
|
|
See the [DAVE Protocol Whitepaper on opcode 30](https://daveprotocol.com/#dave_mls_welcome-30) for technical details.
|
||
|
|
|
||
|
|
:param welcome: The welcome message to process.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def process_commit(self, commit: bytes) -> None:
|
||
|
|
"""
|
||
|
|
Process a commit.
|
||
|
|
See the [DAVE Protocol Whitepaper on opcode 29](https://daveprotocol.com/#dave_mls_announce_commit_transition-29) for technical details.
|
||
|
|
|
||
|
|
:param commit: The commit to process.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def get_verification_code(self, user_id: int) -> str:
|
||
|
|
"""
|
||
|
|
Get the verification code of another member of the group.
|
||
|
|
|
||
|
|
See: https://daveprotocol.com/#displayable-codes
|
||
|
|
|
||
|
|
:param user_id: The ID of the user to get the verification code of.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def get_pairwise_fingerprint(self, version: int, user_id: int) -> bytes:
|
||
|
|
"""
|
||
|
|
Create a pairwise fingerprint of you and another member.
|
||
|
|
|
||
|
|
See: https://daveprotocol.com/#verification-fingerprint
|
||
|
|
|
||
|
|
:param version: The version of the fingerprint.
|
||
|
|
:param user_id: The ID of the user to get the pairwise fingerprint of.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def encrypt(self, media_type: MediaType, codec: Codec, packet: bytes) -> bytes:
|
||
|
|
"""
|
||
|
|
End-to-end encrypt a packet.
|
||
|
|
|
||
|
|
:param media_type: The type of media to encrypt.
|
||
|
|
:param codec: The codec of the packet.
|
||
|
|
:param packet: The packet to encrypt.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def encrypt_opus(self, packet: bytes) -> bytes:
|
||
|
|
"""
|
||
|
|
End-to-end encrypt an opus packet.
|
||
|
|
|
||
|
|
:param packet: The packet to encrypt.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def get_encryption_stats(
|
||
|
|
self, media_type: Optional[MediaType] = None
|
||
|
|
) -> EncryptionStats:
|
||
|
|
"""
|
||
|
|
Get encryption stats.
|
||
|
|
|
||
|
|
:param media_type: The media type to get stats for. Defaults to audio.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def decrypt(self, user_id: int, media_type: MediaType, packet: bytes) -> bytes:
|
||
|
|
"""
|
||
|
|
Decrypt an end-to-end encrypted packet.
|
||
|
|
|
||
|
|
:param user_id: The ID of the user to decrypt the packet for.
|
||
|
|
:param media_type: The type of media to decrypt.
|
||
|
|
:param packet: The packet to decrypt.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def get_decryption_stats(
|
||
|
|
self, user_id: int, media_type: Optional[MediaType] = None
|
||
|
|
) -> Optional[DecryptionStats]:
|
||
|
|
"""
|
||
|
|
Get decryption stats.
|
||
|
|
|
||
|
|
:param user_id: The ID of the user to get stats for.
|
||
|
|
:param media_type: The media type to get stats for. Defaults to audio.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def get_user_ids(self) -> List[str]:
|
||
|
|
"""Get the IDs of the users in the current group."""
|
||
|
|
...
|
||
|
|
def can_passthrough(self, user_id: int) -> bool:
|
||
|
|
"""
|
||
|
|
Check whether this user's decryptor is in passthrough mode.
|
||
|
|
If passthrough mode is enabled, then unencrypted packets are allowed to be passed through the decryptor.
|
||
|
|
|
||
|
|
:param user_id: The ID of the user to check their decryptoe's passthrough mode.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def set_passthrough_mode(
|
||
|
|
self, passthrough_mode: bool, transition_expiry: Optional[int] = None
|
||
|
|
) -> None:
|
||
|
|
"""
|
||
|
|
Set whether passthrough mode is enabled on all decryptors.
|
||
|
|
|
||
|
|
:param passthrough_mode: Whether to enable passthrough mode.
|
||
|
|
:param transition_expiry: The transition expiry (in seconds) to use when disabling passthrough mode, defaults to 10 seconds.
|
||
|
|
"""
|
||
|
|
...
|
||
|
|
def __repr__(self) -> str: ...
|