Compare commits

...

16 Commits

Author SHA1 Message Date
Gilles Boccon-Gibod
2e523b6f49 enable opus, enable more options 2025-05-27 11:49:44 -04:00
zxzxwu
3b399ea1a2 Merge pull request #696 from zxzxwu/att
Replace Optional[Connection] AttributeValue parameter type
2025-05-19 21:17:11 +08:00
zxzxwu
84f7cad678 Replace Optional[Connection] att parameter type 2025-05-18 07:55:39 +00:00
Gilles Boccon-Gibod
778f439e1c Merge pull request #695 from dsseng/intel-garfield-peak
intel: add Garfield Peak GfP2 and AX211 firmware loading
2025-05-17 11:02:04 -07:00
Dmitrii Sharshakov
1b95d4e1df intel: add Garfield Peak GfP2 and AX211 firmware loading
Tested with AX211 on an MSI board, using ibt-1040-0041.sfi from linux-firmware.
Firmware loads (after reboot to bootloader), then the controller functions well (ISO fails, but that might be another topic).

Signed-off-by: Dmitrii Sharshakov <d3dx12.xx@gmail.com>
2025-05-17 16:01:16 +02:00
zxzxwu
512f6d4ee1 Merge pull request #694 from zxzxwu/crypto
Fix wrong smp_test parameters
2025-05-14 23:53:43 +08:00
zxzxwu
c52b614abb Fix wrong smp_test parameters 2025-05-14 08:27:11 +00:00
zxzxwu
7b7afc7179 Merge pull request #691 from zxzxwu/crypto
Remove empty crypto.py
2025-05-13 23:40:44 +08:00
zxzxwu
b1c6044533 Remove empty crypto.py 2025-05-13 06:20:58 +00:00
zxzxwu
38499dfe3c Merge pull request #689 from zxzxwu/link_key
Fix: Missing EVENT_LINK_KEY
2025-05-13 12:09:27 +08:00
zxzxwu
b58c29202a Merge pull request #683 from zxzxwu/crypto
Implement builtin cryptography primitives
2025-05-12 20:52:28 +08:00
Josh Wu
ca759ca967 Implement builtin cryptography primitives 2025-05-12 06:23:45 +00:00
zxzxwu
3858bf80c1 Fix missing EVENT_LINK_KEY 2025-05-10 14:58:04 +00:00
Slvr
a88a034ce2 cryptography: bump version to 44.0.3 to fix python parsing (#684)
Bug: 404336381
2025-05-08 08:28:33 -07:00
zxzxwu
6b2cd1147d Merge pull request #682 from zxzxwu/linkkey
Move connection.link_key_type to keystore
2025-05-08 11:23:28 +08:00
Josh Wu
bb8dcaf63e Move connection.link_key_type to keystore 2025-05-06 02:11:25 +08:00
27 changed files with 1227 additions and 418 deletions

View File

@@ -15,6 +15,7 @@
<tr><td>Codec</td><td><span id="codecText"></span></td></tr> <tr><td>Codec</td><td><span id="codecText"></span></td></tr>
<tr><td>Packets</td><td><span id="packetsReceivedText"></span></td></tr> <tr><td>Packets</td><td><span id="packetsReceivedText"></span></td></tr>
<tr><td>Bytes</td><td><span id="bytesReceivedText"></span></td></tr> <tr><td>Bytes</td><td><span id="bytesReceivedText"></span></td></tr>
<tr><td>Bitrate</td><td><span id="bitrate"></span></td></tr>
</table> </table>
</td> </td>
<td> <td>

View File

@@ -7,17 +7,19 @@ let connectionText;
let codecText; let codecText;
let packetsReceivedText; let packetsReceivedText;
let bytesReceivedText; let bytesReceivedText;
let bitrateText;
let streamStateText; let streamStateText;
let connectionStateText; let connectionStateText;
let controlsDiv; let controlsDiv;
let audioOnButton; let audioOnButton;
let mediaSource; let audioDecoder;
let sourceBuffer; let audioCodec;
let audioElement;
let audioContext; let audioContext;
let audioAnalyzer; let audioAnalyzer;
let audioFrequencyBinCount; let audioFrequencyBinCount;
let audioFrequencyData; let audioFrequencyData;
let nextAudioStartPosition = 0;
let audioStartTime = 0;
let packetsReceived = 0; let packetsReceived = 0;
let bytesReceived = 0; let bytesReceived = 0;
let audioState = "stopped"; let audioState = "stopped";
@@ -29,20 +31,17 @@ let bandwidthCanvas;
let bandwidthCanvasContext; let bandwidthCanvasContext;
let bandwidthBinCount; let bandwidthBinCount;
let bandwidthBins = []; let bandwidthBins = [];
let bitrateSamples = [];
const FFT_WIDTH = 800; const FFT_WIDTH = 800;
const FFT_HEIGHT = 256; const FFT_HEIGHT = 256;
const BANDWIDTH_WIDTH = 500; const BANDWIDTH_WIDTH = 500;
const BANDWIDTH_HEIGHT = 100; const BANDWIDTH_HEIGHT = 100;
const BITRATE_WINDOW = 30;
function hexToBytes(hex) {
return Uint8Array.from(hex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
}
function init() { function init() {
initUI(); initUI();
initMediaSource(); initAudioContext();
initAudioElement();
initAnalyzer(); initAnalyzer();
connect(); connect();
@@ -56,6 +55,7 @@ function initUI() {
codecText = document.getElementById("codecText"); codecText = document.getElementById("codecText");
packetsReceivedText = document.getElementById("packetsReceivedText"); packetsReceivedText = document.getElementById("packetsReceivedText");
bytesReceivedText = document.getElementById("bytesReceivedText"); bytesReceivedText = document.getElementById("bytesReceivedText");
bitrateText = document.getElementById("bitrate");
streamStateText = document.getElementById("streamStateText"); streamStateText = document.getElementById("streamStateText");
connectionStateText = document.getElementById("connectionStateText"); connectionStateText = document.getElementById("connectionStateText");
audioSupportMessageText = document.getElementById("audioSupportMessageText"); audioSupportMessageText = document.getElementById("audioSupportMessageText");
@@ -67,17 +67,9 @@ function initUI() {
requestAnimationFrame(onAnimationFrame); requestAnimationFrame(onAnimationFrame);
} }
function initMediaSource() { function initAudioContext() {
mediaSource = new MediaSource(); audioContext = new AudioContext();
mediaSource.onsourceopen = onMediaSourceOpen; audioContext.onstatechange = () => console.log("AudioContext state:", audioContext.state);
mediaSource.onsourceclose = onMediaSourceClose;
mediaSource.onsourceended = onMediaSourceEnd;
}
function initAudioElement() {
audioElement = document.getElementById("audio");
audioElement.src = URL.createObjectURL(mediaSource);
// audioElement.controls = true;
} }
function initAnalyzer() { function initAnalyzer() {
@@ -94,24 +86,16 @@ function initAnalyzer() {
bandwidthCanvasContext = bandwidthCanvas.getContext('2d'); bandwidthCanvasContext = bandwidthCanvas.getContext('2d');
bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)"; bandwidthCanvasContext.fillStyle = "rgb(255, 255, 255)";
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
}
function startAnalyzer() {
// FFT
if (audioElement.captureStream !== undefined) {
audioContext = new AudioContext();
audioAnalyzer = audioContext.createAnalyser();
audioAnalyzer.fftSize = 128;
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
const stream = audioElement.captureStream();
const source = audioContext.createMediaStreamSource(stream);
source.connect(audioAnalyzer);
}
// Bandwidth
bandwidthBinCount = BANDWIDTH_WIDTH / 2; bandwidthBinCount = BANDWIDTH_WIDTH / 2;
bandwidthBins = []; bandwidthBins = [];
bitrateSamples = [];
audioAnalyzer = audioContext.createAnalyser();
audioAnalyzer.fftSize = 128;
audioFrequencyBinCount = audioAnalyzer.frequencyBinCount;
audioFrequencyData = new Uint8Array(audioFrequencyBinCount);
audioAnalyzer.connect(audioContext.destination)
} }
function setConnectionText(message) { function setConnectionText(message) {
@@ -148,7 +132,8 @@ function onAnimationFrame() {
bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT); bandwidthCanvasContext.fillRect(0, 0, BANDWIDTH_WIDTH, BANDWIDTH_HEIGHT);
bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`; bandwidthCanvasContext.fillStyle = `rgb(100, 100, 100)`;
for (let t = 0; t < bandwidthBins.length; t++) { for (let t = 0; t < bandwidthBins.length; t++) {
const lineHeight = (bandwidthBins[t] / 1000) * BANDWIDTH_HEIGHT; const bytesReceived = bandwidthBins[t]
const lineHeight = (bytesReceived / 1000) * BANDWIDTH_HEIGHT;
bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight); bandwidthCanvasContext.fillRect(t * 2, BANDWIDTH_HEIGHT - lineHeight, 2, lineHeight);
} }
@@ -156,28 +141,14 @@ function onAnimationFrame() {
requestAnimationFrame(onAnimationFrame); requestAnimationFrame(onAnimationFrame);
} }
function onMediaSourceOpen() {
console.log(this.readyState);
sourceBuffer = mediaSource.addSourceBuffer("audio/aac");
}
function onMediaSourceClose() {
console.log(this.readyState);
}
function onMediaSourceEnd() {
console.log(this.readyState);
}
async function startAudio() { async function startAudio() {
try { try {
console.log("starting audio..."); console.log("starting audio...");
audioOnButton.disabled = true; audioOnButton.disabled = true;
audioState = "starting"; audioState = "starting";
await audioElement.play(); audioContext.resume();
console.log("audio started"); console.log("audio started");
audioState = "playing"; audioState = "playing";
startAnalyzer();
} catch(error) { } catch(error) {
console.error(`play failed: ${error}`); console.error(`play failed: ${error}`);
audioState = "stopped"; audioState = "stopped";
@@ -185,12 +156,47 @@ async function startAudio() {
} }
} }
function onAudioPacket(packet) { function onDecodedAudio(audioData) {
if (audioState != "stopped") { const bufferSource = audioContext.createBufferSource()
// Queue the audio packet.
sourceBuffer.appendBuffer(packet); const now = audioContext.currentTime;
let nextAudioStartTime = audioStartTime + (nextAudioStartPosition / audioData.sampleRate);
if (nextAudioStartTime < now) {
console.log("starting new audio time base")
audioStartTime = now;
nextAudioStartTime = now;
nextAudioStartPosition = 0;
} else {
console.log(`audio buffer scheduled in ${nextAudioStartTime - now}`)
} }
const audioBuffer = audioContext.createBuffer(
audioData.numberOfChannels,
audioData.numberOfFrames,
audioData.sampleRate
);
for (let channel = 0; channel < audioData.numberOfChannels; channel++) {
audioData.copyTo(
audioBuffer.getChannelData(channel),
{
planeIndex: channel,
format: "f32-planar"
}
)
}
bufferSource.buffer = audioBuffer;
bufferSource.connect(audioAnalyzer)
bufferSource.start(nextAudioStartTime);
nextAudioStartPosition += audioData.numberOfFrames;
}
function onCodecError(error) {
console.log("Codec error:", error)
}
async function onAudioPacket(packet) {
packetsReceived += 1; packetsReceived += 1;
packetsReceivedText.innerText = packetsReceived; packetsReceivedText.innerText = packetsReceived;
bytesReceived += packet.byteLength; bytesReceived += packet.byteLength;
@@ -200,6 +206,48 @@ function onAudioPacket(packet) {
if (bandwidthBins.length > bandwidthBinCount) { if (bandwidthBins.length > bandwidthBinCount) {
bandwidthBins.shift(); bandwidthBins.shift();
} }
bitrateSamples[bitrateSamples.length] = {ts: Date.now(), bytes: packet.byteLength}
if (bitrateSamples.length > BITRATE_WINDOW) {
bitrateSamples.shift();
}
if (bitrateSamples.length >= 2) {
const windowBytes = bitrateSamples.reduce((accumulator, x) => accumulator + x.bytes, 0) - bitrateSamples[0].bytes;
const elapsed = bitrateSamples[bitrateSamples.length-1].ts - bitrateSamples[0].ts;
const bitrate = Math.floor(8 * windowBytes / elapsed)
bitrateText.innerText = `${bitrate} kb/s`
}
if (audioState == "stopped") {
return;
}
if (audioDecoder === undefined) {
let audioConfig;
if (audioCodec == 'aac') {
audioConfig = {
codec: 'mp4a.40.2',
sampleRate: 44100, // ignored
numberOfChannels: 2, // ignored
}
} else if (audioCodec == 'opus') {
audioConfig = {
codec: 'opus',
sampleRate: 48000, // ignored
numberOfChannels: 2, // ignored
}
}
audioDecoder = new AudioDecoder({ output: onDecodedAudio, error: onCodecError });
audioDecoder.configure(audioConfig)
}
const encodedAudio = new EncodedAudioChunk({
type: "key",
data: packet,
timestamp: 0,
transfer: [packet],
});
audioDecoder.decode(encodedAudio);
} }
function onChannelOpen() { function onChannelOpen() {
@@ -249,16 +297,19 @@ function onChannelMessage(message) {
} }
} }
function onHelloMessage(params) { async function onHelloMessage(params) {
codecText.innerText = params.codec; codecText.innerText = params.codec;
if (params.codec != "aac") {
audioOnButton.disabled = true; if (params.codec == "aac" || params.codec == "opus") {
audioSupportMessageText.innerText = "Only AAC can be played, audio will be disabled"; audioCodec = params.codec
audioSupportMessageText.style.display = "inline-block";
} else {
audioSupportMessageText.innerText = ""; audioSupportMessageText.innerText = "";
audioSupportMessageText.style.display = "none"; audioSupportMessageText.style.display = "none";
} else {
audioOnButton.disabled = true;
audioSupportMessageText.innerText = "Only AAC and Opus can be played, audio will be disabled";
audioSupportMessageText.style.display = "inline-block";
} }
if (params.streamState) { if (params.streamState) {
setStreamState(params.streamState); setStreamState(params.streamState);
} }

View File

@@ -50,8 +50,10 @@ from bumble.a2dp import (
make_audio_sink_service_sdp_records, make_audio_sink_service_sdp_records,
A2DP_SBC_CODEC_TYPE, A2DP_SBC_CODEC_TYPE,
A2DP_MPEG_2_4_AAC_CODEC_TYPE, A2DP_MPEG_2_4_AAC_CODEC_TYPE,
A2DP_NON_A2DP_CODEC_TYPE,
SbcMediaCodecInformation, SbcMediaCodecInformation,
AacMediaCodecInformation, AacMediaCodecInformation,
OpusMediaCodecInformation,
) )
from bumble.utils import AsyncRunner from bumble.utils import AsyncRunner
from bumble.codecs import AacAudioRtpPacket from bumble.codecs import AacAudioRtpPacket
@@ -78,6 +80,8 @@ class AudioExtractor:
return AacAudioExtractor() return AacAudioExtractor()
if codec == 'sbc': if codec == 'sbc':
return SbcAudioExtractor() return SbcAudioExtractor()
if codec == 'opus':
return OpusAudioExtractor()
def extract_audio(self, packet: MediaPacket) -> bytes: def extract_audio(self, packet: MediaPacket) -> bytes:
raise NotImplementedError() raise NotImplementedError()
@@ -102,6 +106,13 @@ class SbcAudioExtractor:
return packet.payload[1:] return packet.payload[1:]
# -----------------------------------------------------------------------------
class OpusAudioExtractor:
def extract_audio(self, packet: MediaPacket) -> bytes:
# TODO: parse fields
return packet.payload[1:]
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class Output: class Output:
async def start(self) -> None: async def start(self) -> None:
@@ -235,7 +246,7 @@ class FfplayOutput(QueuedOutput):
await super().start() await super().start()
self.subprocess = await asyncio.create_subprocess_shell( self.subprocess = await asyncio.create_subprocess_shell(
f'ffplay -f {self.codec} pipe:0', f'ffplay -probesize 32 -f {self.codec} pipe:0',
stdin=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
@@ -399,10 +410,24 @@ class Speaker:
STARTED = 2 STARTED = 2
SUSPENDED = 3 SUSPENDED = 3
def __init__(self, device_config, transport, codec, discover, outputs, ui_port): def __init__(
self,
device_config,
transport,
codec,
sampling_frequencies,
bitrate,
vbr,
discover,
outputs,
ui_port,
):
self.device_config = device_config self.device_config = device_config
self.transport = transport self.transport = transport
self.codec = codec self.codec = codec
self.sampling_frequencies = sampling_frequencies
self.bitrate = bitrate
self.vbr = vbr
self.discover = discover self.discover = discover
self.ui_port = ui_port self.ui_port = ui_port
self.device = None self.device = None
@@ -438,32 +463,56 @@ class Speaker:
if self.codec == 'sbc': if self.codec == 'sbc':
return self.sbc_codec_capabilities() return self.sbc_codec_capabilities()
if self.codec == 'opus':
return self.opus_codec_capabilities()
raise RuntimeError('unsupported codec') raise RuntimeError('unsupported codec')
def aac_codec_capabilities(self) -> MediaCodecCapabilities: def aac_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = AacMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [
8000,
11025,
12000,
16000,
22050,
24000,
32000,
44100,
48000,
]:
supported_sampling_frequencies |= (
AacMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities( return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE, media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE, media_codec_type=A2DP_MPEG_2_4_AAC_CODEC_TYPE,
media_codec_information=AacMediaCodecInformation( media_codec_information=AacMediaCodecInformation(
object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC, object_type=AacMediaCodecInformation.ObjectType.MPEG_2_AAC_LC,
sampling_frequency=AacMediaCodecInformation.SamplingFrequency.SF_48000 sampling_frequency=supported_sampling_frequencies,
| AacMediaCodecInformation.SamplingFrequency.SF_44100,
channels=AacMediaCodecInformation.Channels.MONO channels=AacMediaCodecInformation.Channels.MONO
| AacMediaCodecInformation.Channels.STEREO, | AacMediaCodecInformation.Channels.STEREO,
vbr=1, vbr=1 if self.vbr else 0,
bitrate=256000, bitrate=self.bitrate or 256000,
), ),
) )
def sbc_codec_capabilities(self) -> MediaCodecCapabilities: def sbc_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = SbcMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [
16000,
32000,
44100,
48000,
]:
supported_sampling_frequencies |= (
SbcMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities( return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE, media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_SBC_CODEC_TYPE, media_codec_type=A2DP_SBC_CODEC_TYPE,
media_codec_information=SbcMediaCodecInformation( media_codec_information=SbcMediaCodecInformation(
sampling_frequency=SbcMediaCodecInformation.SamplingFrequency.SF_48000 sampling_frequency=supported_sampling_frequencies,
| SbcMediaCodecInformation.SamplingFrequency.SF_44100
| SbcMediaCodecInformation.SamplingFrequency.SF_32000
| SbcMediaCodecInformation.SamplingFrequency.SF_16000,
channel_mode=SbcMediaCodecInformation.ChannelMode.MONO channel_mode=SbcMediaCodecInformation.ChannelMode.MONO
| SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL | SbcMediaCodecInformation.ChannelMode.DUAL_CHANNEL
| SbcMediaCodecInformation.ChannelMode.STEREO | SbcMediaCodecInformation.ChannelMode.STEREO
@@ -481,6 +530,25 @@ class Speaker:
), ),
) )
def opus_codec_capabilities(self) -> MediaCodecCapabilities:
supported_sampling_frequencies = OpusMediaCodecInformation.SamplingFrequency(0)
for sampling_frequency in self.sampling_frequencies or [48000]:
supported_sampling_frequencies |= (
OpusMediaCodecInformation.SamplingFrequency.from_int(sampling_frequency)
)
return MediaCodecCapabilities(
media_type=AVDTP_AUDIO_MEDIA_TYPE,
media_codec_type=A2DP_NON_A2DP_CODEC_TYPE,
media_codec_information=OpusMediaCodecInformation(
frame_size=OpusMediaCodecInformation.FrameSize.FS_10MS
| OpusMediaCodecInformation.FrameSize.FS_20MS,
channel_mode=OpusMediaCodecInformation.ChannelMode.MONO
| OpusMediaCodecInformation.ChannelMode.STEREO
| OpusMediaCodecInformation.ChannelMode.DUAL_MONO,
sampling_frequency=supported_sampling_frequencies,
),
)
async def dispatch_to_outputs(self, function): async def dispatch_to_outputs(self, function):
for output in self.outputs: for output in self.outputs:
await function(output) await function(output)
@@ -675,7 +743,26 @@ def speaker_cli(ctx, device_config):
@click.command() @click.command()
@click.option( @click.option(
'--codec', type=click.Choice(['sbc', 'aac']), default='aac', show_default=True '--codec',
type=click.Choice(['sbc', 'aac', 'opus']),
default='aac',
show_default=True,
)
@click.option(
'--sampling-frequency',
metavar='SAMPLING-FREQUENCY',
type=int,
multiple=True,
help='Enable a sampling frequency (may be specified more than once)',
)
@click.option(
'--bitrate',
metavar='BITRATE',
type=int,
help='Supported bitrate (AAC only)',
)
@click.option(
'--vbr/--no-vbr', is_flag=True, default=True, help='Enable VBR (AAC only)'
) )
@click.option( @click.option(
'--discover', is_flag=True, help='Discover remote endpoints once connected' '--discover', is_flag=True, help='Discover remote endpoints once connected'
@@ -706,7 +793,16 @@ def speaker_cli(ctx, device_config):
@click.option('--device-config', metavar='FILENAME', help='Device configuration file') @click.option('--device-config', metavar='FILENAME', help='Device configuration file')
@click.argument('transport') @click.argument('transport')
def speaker( def speaker(
transport, codec, connect_address, discover, output, ui_port, device_config transport,
codec,
sampling_frequency,
bitrate,
vbr,
connect_address,
discover,
output,
ui_port,
device_config,
): ):
"""Run the speaker.""" """Run the speaker."""
@@ -721,15 +817,27 @@ def speaker(
output = list(filter(lambda x: x != '@ffplay', output)) output = list(filter(lambda x: x != '@ffplay', output))
asyncio.run( asyncio.run(
Speaker(device_config, transport, codec, discover, output, ui_port).run( Speaker(
connect_address device_config,
) transport,
codec,
sampling_frequency,
bitrate,
vbr,
discover,
output,
ui_port,
).run(connect_address)
) )
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def main(): def main():
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper()) logging.basicConfig(
level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper(),
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
datefmt="%H:%M:%S",
)
speaker() speaker()

View File

@@ -479,6 +479,14 @@ class OpusMediaCodecInformation(VendorSpecificMediaCodecInformation):
class SamplingFrequency(enum.IntFlag): class SamplingFrequency(enum.IntFlag):
SF_48000 = 1 << 0 SF_48000 = 1 << 0
@classmethod
def from_int(
cls, sampling_frequency: int
) -> OpusMediaCodecInformation.SamplingFrequency:
if sampling_frequency != 48000:
raise ValueError("no such sampling frequency")
return cls.SF_48000
VENDOR_ID: ClassVar[int] = 0x000000E0 VENDOR_ID: ClassVar[int] = 0x000000E0
CODEC_ID: ClassVar[int] = 0x0001 CODEC_ID: ClassVar[int] = 0x0001

View File

@@ -770,27 +770,25 @@ class AttributeValue(Generic[_T]):
def __init__( def __init__(
self, self,
read: Union[ read: Union[
Callable[[Optional[Connection]], _T], Callable[[Connection], _T],
Callable[[Optional[Connection]], Awaitable[_T]], Callable[[Connection], Awaitable[_T]],
None, None,
] = None, ] = None,
write: Union[ write: Union[
Callable[[Optional[Connection], _T], None], Callable[[Connection, _T], None],
Callable[[Optional[Connection], _T], Awaitable[None]], Callable[[Connection, _T], Awaitable[None]],
None, None,
] = None, ] = None,
): ):
self._read = read self._read = read
self._write = write self._write = write
def read(self, connection: Optional[Connection]) -> Union[_T, Awaitable[_T]]: def read(self, connection: Connection) -> Union[_T, Awaitable[_T]]:
if self._read is None: if self._read is None:
raise InvalidOperationError('AttributeValue has no read function') raise InvalidOperationError('AttributeValue has no read function')
return self._read(connection) return self._read(connection)
def write( def write(self, connection: Connection, value: _T) -> Union[Awaitable[None], None]:
self, connection: Optional[Connection], value: _T
) -> Union[Awaitable[None], None]:
if self._write is None: if self._write is None:
raise InvalidOperationError('AttributeValue has no write function') raise InvalidOperationError('AttributeValue has no write function')
return self._write(connection, value) return self._write(connection, value)
@@ -871,7 +869,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
def decode_value(self, value: bytes) -> _T: def decode_value(self, value: bytes) -> _T:
return value # type: ignore return value # type: ignore
async def read_value(self, connection: Optional[Connection]) -> bytes: async def read_value(self, connection: Connection) -> bytes:
if ( if (
(self.permissions & self.READ_REQUIRES_ENCRYPTION) (self.permissions & self.READ_REQUIRES_ENCRYPTION)
and connection is not None and connection is not None
@@ -913,7 +911,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
return b'' if value is None else self.encode_value(value) return b'' if value is None else self.encode_value(value)
async def write_value(self, connection: Optional[Connection], value: bytes) -> None: async def write_value(self, connection: Connection, value: bytes) -> None:
if ( if (
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION) (self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
and connection is not None and connection is not None

View File

@@ -1,6 +1,6 @@
# Copyright 2021-2022 Google LLC # Copyright 2021-2025 Google LLC
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
@@ -12,12 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# -----------------------------------------------------------------------------
# Crypto support
#
# See Bluetooth spec Vol 3, Part H - 2.2 CRYPTOGRAPHIC TOOLBOX
# -----------------------------------------------------------------------------
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -25,19 +19,15 @@ from __future__ import annotations
import logging import logging
import operator import operator
import secrets import secrets
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.asymmetric.ec import ( try:
generate_private_key, from bumble.crypto.cryptography import EccKey, e, aes_cmac
ECDH, except ImportError:
EllipticCurvePrivateKey, logging.getLogger(__name__).debug(
EllipticCurvePublicNumbers, "Unable to import cryptography, use built-in primitives."
EllipticCurvePrivateNumbers, )
SECP256R1, from bumble.crypto.builtin import EccKey, e, aes_cmac # type: ignore[assignment]
)
from cryptography.hazmat.primitives import cmac
from typing import Tuple
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -46,55 +36,6 @@ from typing import Tuple
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Classes
# -----------------------------------------------------------------------------
class EccKey:
def __init__(self, private_key: EllipticCurvePrivateKey) -> None:
self.private_key = private_key
@classmethod
def generate(cls) -> EccKey:
private_key = generate_private_key(SECP256R1())
return cls(private_key)
@classmethod
def from_private_key_bytes(
cls, d_bytes: bytes, x_bytes: bytes, y_bytes: bytes
) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
x = int.from_bytes(x_bytes, byteorder='big', signed=False)
y = int.from_bytes(y_bytes, byteorder='big', signed=False)
private_key = EllipticCurvePrivateNumbers(
d, EllipticCurvePublicNumbers(x, y, SECP256R1())
).private_key()
return cls(private_key)
@property
def x(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.x.to_bytes(32, byteorder='big')
)
@property
def y(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.y.to_bytes(32, byteorder='big')
)
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
public_key = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key()
shared_key = self.private_key.exchange(ECDH(), public_key)
return shared_key
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Functions # Functions
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -132,19 +73,6 @@ def r() -> bytes:
return secrets.token_bytes(16) return secrets.token_bytes(16)
# -----------------------------------------------------------------------------
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
cipher = Cipher(algorithms.AES(reverse(key)), modes.ECB())
encryptor = cipher.encryptor()
return reverse(encryptor.update(reverse(data)))
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name def ah(k: bytes, r: bytes) -> bytes: # pylint: disable=redefined-outer-name
''' '''
@@ -187,18 +115,6 @@ def s1(k: bytes, r1: bytes, r2: bytes) -> bytes:
return e(k, r2[0:8] + r1[0:8]) return e(k, r2[0:8] + r1[0:8])
# -----------------------------------------------------------------------------
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
mac = cmac.CMAC(algorithms.AES(k))
mac.update(m)
return mac.finalize()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes: def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
''' '''
@@ -209,7 +125,7 @@ def f4(u: bytes, v: bytes, x: bytes, z: bytes) -> bytes:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> Tuple[bytes, bytes]: def f5(w: bytes, n1: bytes, n2: bytes, a1: bytes, a2: bytes) -> tuple[bytes, bytes]:
''' '''
See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation
Function f5 Function f5

652
bumble/crypto/builtin.py Normal file
View File

@@ -0,0 +1,652 @@
# Copyright 2021-2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# The implementation is modified from:
# * AES - https://github.com/ricmoo/pyaes by Richard Moore under MIT License
# * CMAC - https://github.com/pycrypto/pycrypto by contributors under pycrypto License.
# -----------------------------------------------------------------------------
# Built-in implementation of cryptography primitives.
#
# Note: It's very dangerous to use this library in the real world.
# -----------------------------------------------------------------------------
from __future__ import annotations
import dataclasses
import functools
import copy
import secrets
import struct
from typing import Optional
from bumble import core
def _compact_word(word: bytes) -> int:
return int.from_bytes(word, "big")
def _shift_bytes(bs: bytes, xor_lsb: int = 0) -> bytes:
return ((int.from_bytes(bs, "big") << 1) ^ xor_lsb).to_bytes(len(bs) + 1, "big")[1:]
def _xor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
# Based *largely* on the Rijndael implementation
# See: http://csrc.nist.gov/publications/FIPS/FIPS197/FIPS-197.pdf
class _AES:
'''Encapsulates the AES block cipher.
You generally should not need this. Use the AESModeOfOperation classes
below instead.'''
# fmt: off
# Number of rounds by key size
_NUMBER_OF_ROUNDS = {16: 10, 24: 12, 32: 14}
# Round constant words
_RCON = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ]
# S-box and Inverse S-box (S is for Substitution)
_S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ]
_S_INV =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ]
# Transformations for encryption
_T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ]
_T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ]
_T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ]
_T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ]
# Transformations for decryption
_T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ]
_T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ]
_T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ]
_T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ]
# Transformations for decryption key expansion
_U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ]
_U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ]
_U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ]
_U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ]
# fmt: on
def __init__(self, key: bytes) -> None:
if len(key) not in (16, 24, 32):
raise core.InvalidArgumentError(f'Invalid key size {len(key)}')
rounds = self._NUMBER_OF_ROUNDS[len(key)]
# Encryption round keys
self._ke = [[0] * 4 for i in range(rounds + 1)]
# Decryption round keys
self._kd = [[0] * 4 for i in range(rounds + 1)]
round_key_count = (rounds + 1) * 4
kc = len(key) // 4
# Convert the key into ints
tk = [struct.unpack('>i', key[i : i + 4])[0] for i in range(0, len(key), 4)]
# Copy values into round key arrays
for i in range(0, kc):
self._ke[i // 4][i % 4] = tk[i]
self._kd[rounds - (i // 4)][i % 4] = tk[i]
# Key expansion (FIPS-197 section 5.2)
r_con_pointer = 0
t = kc
while t < round_key_count:
tt = tk[kc - 1]
tk[0] ^= (
(self._S[(tt >> 16) & 0xFF] << 24)
^ (self._S[(tt >> 8) & 0xFF] << 16)
^ (self._S[tt & 0xFF] << 8)
^ self._S[(tt >> 24) & 0xFF]
^ (self._RCON[r_con_pointer] << 24)
)
r_con_pointer += 1
if kc != 8:
for i in range(1, kc):
tk[i] ^= tk[i - 1]
# Key expansion for 256-bit keys is "slightly different" (FIPS-197)
else:
for i in range(1, kc // 2):
tk[i] ^= tk[i - 1]
tt = tk[kc // 2 - 1]
tk[kc // 2] ^= (
self._S[tt & 0xFF]
^ (self._S[(tt >> 8) & 0xFF] << 8)
^ (self._S[(tt >> 16) & 0xFF] << 16)
^ (self._S[(tt >> 24) & 0xFF] << 24)
)
for i in range(kc // 2 + 1, kc):
tk[i] ^= tk[i - 1]
# Copy values into round key arrays
j = 0
while j < kc and t < round_key_count:
self._ke[t // 4][t % 4] = tk[j]
self._kd[rounds - (t // 4)][t % 4] = tk[j]
j += 1
t += 1
# Inverse-Cipher-ify the decryption round key (FIPS-197 section 5.3)
for r in range(1, rounds):
for j in range(0, 4):
tt = self._kd[r][j]
self._kd[r][j] = (
self._U1[(tt >> 24) & 0xFF]
^ self._U2[(tt >> 16) & 0xFF]
^ self._U3[(tt >> 8) & 0xFF]
^ self._U4[tt & 0xFF]
)
def encrypt(self, plaintext: bytes) -> bytes:
"""Encrypt a block of plain text using the AES block cipher."""
if len(plaintext) != 16:
raise core.InvalidArgumentError(f'wrong block length {len(plaintext)}')
rounds = len(self._ke) - 1
(s1, s2, s3) = [1, 2, 3]
a = [0, 0, 0, 0]
# Convert plaintext to (ints ^ key)
t = [
(_compact_word(plaintext[4 * i : 4 * i + 4]) ^ self._ke[0][i])
for i in range(0, 4)
]
# Apply round transforms
for r in range(1, rounds):
for i in range(0, 4):
a[i] = (
self._T1[(t[i] >> 24) & 0xFF]
^ self._T2[(t[(i + s1) % 4] >> 16) & 0xFF]
^ self._T3[(t[(i + s2) % 4] >> 8) & 0xFF]
^ self._T4[t[(i + s3) % 4] & 0xFF]
^ self._ke[r][i]
)
t = copy.copy(a)
# The last round is special
result = []
for i in range(0, 4):
tt = self._ke[rounds][i]
result.append((self._S[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append((self._S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
result.append((self._S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
result.append((self._S[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
return bytes(result)
def decrypt(self, cipher_text: bytes) -> bytes:
"""Decrypt a block of cipher text using the AES block cipher."""
if len(cipher_text) != 16:
raise core.InvalidArgumentError(f'wrong block length {len(cipher_text)}')
rounds = len(self._kd) - 1
(s1, s2, s3) = [3, 2, 1]
a = [0, 0, 0, 0]
# Convert ciphertext to (ints ^ key)
t = [
(_compact_word(cipher_text[4 * i : 4 * i + 4]) ^ self._kd[0][i])
for i in range(0, 4)
]
# Apply round transforms
for r in range(1, rounds):
for i in range(0, 4):
a[i] = (
self._T5[(t[i] >> 24) & 0xFF]
^ self._T6[(t[(i + s1) % 4] >> 16) & 0xFF]
^ self._T7[(t[(i + s2) % 4] >> 8) & 0xFF]
^ self._T8[t[(i + s3) % 4] & 0xFF]
^ self._kd[r][i]
)
t = copy.copy(a)
# The last round is special
result = []
for i in range(0, 4):
tt = self._kd[rounds][i]
result.append((self._S_INV[(t[i] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append(
(self._S_INV[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF
)
result.append(
(self._S_INV[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF
)
result.append((self._S_INV[t[(i + s3) % 4] & 0xFF] ^ tt) & 0xFF)
return bytes(result)
class _ECB:
def __init__(self, key: bytes):
self._aes = _AES(key)
def encrypt(self, plaintext: bytes) -> bytes:
return b"".join(
[
self._aes.encrypt(
plaintext[offset : offset + 16].ljust(16, b"\x00") # Pad 0.
)
for offset in range(0, len(plaintext), 16)
]
)
def decrypt(self, cipher_text: bytes) -> bytes:
return b"".join(
[
self._aes.encrypt(cipher_text[offset : offset + 16])
for offset in range(0, len(cipher_text), 16)
]
)
class _CBC:
def __init__(self, key: bytes, iv: bytes = bytes(16)) -> None:
if len(iv) != 16:
raise core.InvalidArgumentError(
f'initialization vector must be 16 bytes, get {len(iv)}'
)
else:
self._last_cipher_block = iv
self._aes = _AES(key)
def encrypt(self, plaintext: bytes) -> bytes:
cipher_text = b""
for offset in range(0, len(plaintext), 16):
pre_cipher_block = _xor(
plaintext[offset : offset + 16], self._last_cipher_block
)
self._last_cipher_block = self._aes.encrypt(pre_cipher_block)
cipher_text += self._last_cipher_block
return cipher_text
def decrypt(self, cipher_text: bytes) -> bytes:
plaintext = b""
for offset in range(0, len(cipher_text), 16):
plaintext += _xor(
self._aes.decrypt(cipher_text[offset : offset + 16]),
self._last_cipher_block,
)
self._last_cipher_block = cipher_text[offset : offset + 16]
return plaintext
class _CMAC:
def __init__(
self,
key: bytes,
msg: bytes = bytes(16),
mac_len: int = 16,
update_after_digest: bool = False,
) -> None:
self.digest_size = mac_len
self._key = key
self._block_size = bs = 16
self._mac_tag: Optional[bytes] = None
self._update_after_digest = update_after_digest
# Section 5.3 of NIST SP 800 38B and Appendix B
if bs == 8:
const_Rb = 0x1B
self._max_size = 8 * (2**21)
elif bs == 16:
const_Rb = 0x87
self._max_size = 16 * (2**48)
else:
raise core.InvalidArgumentError(
f"CMAC requires a cipher with a block size of 8 or 16 bytes, not {bs}"
)
# Compute sub-keys
zero_block = bytes(bs)
self._ecb = _ECB(key)
L = self._ecb.encrypt(zero_block)
if L[0] & 0x80:
self._k1 = _shift_bytes(L, const_Rb)
else:
self._k1 = _shift_bytes(L)
if self._k1[0] & 0x80:
self._k2 = _shift_bytes(self._k1, const_Rb)
else:
self._k2 = _shift_bytes(self._k1)
# Initialize CBC cipher with zero IV
self._cbc = _CBC(key, zero_block)
# Cache for outstanding data to authenticate
self._cache = bytearray(bs)
self._cache_n = 0
# Last piece of cipher text produced
self._last_ct = zero_block
# Last block that was encrypted with AES
self._last_pt: Optional[bytes] = None
# Counter for total message size
self._data_size = 0
if msg:
self.update(msg)
def update(self, msg: bytes) -> _CMAC:
"""Authenticate the next chunk of message.
Args:
data (byte string/byte array/memoryview): The next chunk of data
"""
if self._mac_tag is not None and not self._update_after_digest:
raise core.InvalidStateError(
"update() cannot be called after digest() or verify()"
)
self._data_size += len(msg)
bs = self._block_size
if self._cache_n > 0:
filler = min(bs - self._cache_n, len(msg))
self._cache[self._cache_n : self._cache_n + filler] = msg[:filler]
self._cache_n += filler
if self._cache_n < bs:
return self
msg = msg[filler:]
self._update(self._cache)
self._cache_n = 0
remain = len(msg) % bs
if remain > 0:
self._update(msg[:-remain])
self._cache[:remain] = msg[-remain:]
else:
self._update(msg)
self._cache_n = remain
return self
def _update(self, data_block: bytes) -> None:
"""Update a block aligned to the block boundary"""
bs = self._block_size
assert len(data_block) % bs == 0
if len(data_block) == 0:
return
ct = self._cbc.encrypt(data_block)
if len(data_block) == bs:
second_last = self._last_ct
else:
second_last = ct[-bs * 2 : -bs]
self._last_ct = ct[-bs:]
self._last_pt = _xor(second_last, data_block[-bs:])
def digest(self) -> bytes:
bs = self._block_size
if self._mac_tag is not None and not self._update_after_digest:
return self._mac_tag
if self._data_size > self._max_size:
raise core.InvalidArgumentError("MAC is unsafe for this message")
if self._cache_n == 0 and self._data_size > 0 and self._last_pt:
# Last block was full
pt = _xor(self._last_pt, self._k1)
else:
# Last block is partial (or message length is zero)
partial = self._cache[:]
partial[self._cache_n :] = b'\x80' + b'\x00' * (bs - self._cache_n - 1)
pt = _xor(_xor(self._last_ct, partial), self._k2)
self._mac_tag = self._ecb.encrypt(pt)[: self.digest_size]
return self._mac_tag
# Define the original Point class for clarity and conversion purposes
@dataclasses.dataclass
class _Point:
"""Represents a point on the elliptic curve in affine coordinates."""
curve: _EllipticCurve
x: int = 0
y: int = 0
infinite: bool = False
@dataclasses.dataclass(frozen=True)
class _JacobianPoint:
"""Represents a point on the elliptic curve in Jacobian coordinates."""
curve: _EllipticCurve
x: int = 1 # For point at infinity (1:1:0)
y: int = 1
z: int = 0 # z = 0 indicates point at infinity
@classmethod
def point_at_infinity(cls, curve: _EllipticCurve) -> _JacobianPoint:
return _JacobianPoint(curve=curve, x=1, y=1, z=0)
@classmethod
def from_affine(cls, affine_point: _Point) -> _JacobianPoint:
if affine_point.infinite:
return _JacobianPoint.point_at_infinity(affine_point.curve)
# A simple conversion is (x, y, 1)
return _JacobianPoint(
curve=affine_point.curve, x=affine_point.x, y=affine_point.y, z=1
)
def to_affine(self) -> _Point:
if self.z == 0:
return _Point(infinite=True, curve=self.curve)
p = self.curve.p
inv_z = pow(self.z, -1, p)
affine_x = (self.x * inv_z**2) % p
affine_y = (self.y * inv_z**3) % p
return _Point(curve=self.curve, x=affine_x, y=affine_y, infinite=False)
def double(self) -> _JacobianPoint:
if self.z == 0 or self.y == 0:
return _JacobianPoint.point_at_infinity(self.curve)
s = 4 * self.x * self.y**2
m = 3 * self.x**2 + self.curve.a * self.z**4
x2 = m**2 - 2 * s
y2 = m * (s - x2) - 8 * self.y**4
z2 = 2 * self.y * self.z
p = self.curve.p
return _JacobianPoint(curve=self.curve, x=x2 % p, y=y2 % p, z=z2 % p)
def __add__(self, other: _JacobianPoint) -> _JacobianPoint:
if self.z == 0 and other.z == 0:
return _JacobianPoint.point_at_infinity(self.curve)
elif self.z == 0:
return other
elif other.z == 0:
return self
x1 = self.x
y1 = self.y
z1 = self.z
x2 = other.x
y2 = other.y
z2 = other.z
p = self.curve.p
u1 = (x1 * z2**2) % p
u2 = (x2 * z1**2) % p
s1 = (y1 * z2**3) % p
s2 = (y2 * z1**3) % p
if u1 == u2:
if s1 != s2:
return _JacobianPoint.point_at_infinity(self.curve)
else:
return self.double()
else:
h = u2 - u1
r = s2 - s1
h3 = h**3 % p
u1h2 = (u1 * h**2) % p
x3 = r**2 - h3 - 2 * u1h2
y3 = r * (u1h2 - x3) - s1 * h3
z3 = h * z1 * z2
return _JacobianPoint(self.curve, x3 % p, y3 % p, z3 % p)
def __mul__(self, k: int) -> _JacobianPoint:
addend = self
result = _JacobianPoint.point_at_infinity(self.curve)
while k > 0:
if k % 2 != 0:
result = result + addend
addend = addend.double()
k = k >> 1
return result
def __rmul__(self, k: int) -> _JacobianPoint:
return self * k
@dataclasses.dataclass
class _EllipticCurve:
p: int
a: int
b: int
n: int
g_x: int
g_y: int
_generator_jacobian: _JacobianPoint = dataclasses.field(init=False)
def __post_init__(self):
self._generator_jacobian = _JacobianPoint(
curve=self, x=self.g_x, y=self.g_y, z=1
)
@dataclasses.dataclass
class PrivateKey:
key: int
curve: _EllipticCurve
def generate_private_key(self) -> PrivateKey:
"""Generates a random private key."""
return self.PrivateKey(key=secrets.randbelow(self.n), curve=self)
def generate_public_key(self, private_key: int) -> _Point:
"""Generates a public key from a private key using Jacobian coordinates for scalar multiplication."""
public_key_jacobian = self._generator_jacobian * private_key
return public_key_jacobian.to_affine()
def ecdh_shared_secret(self, private_key: int, other_public_key: _Point) -> bytes:
"""Computes the shared secret using ECDH."""
other_public_key_jacobian = _JacobianPoint.from_affine(other_public_key)
shared_point_jacobian = other_public_key_jacobian * private_key
shared_point_affine = shared_point_jacobian.to_affine()
if shared_point_affine.infinite:
raise core.InvalidPacketError(
"Shared secret calculation resulted in the point at infinite"
)
return shared_point_affine.x.to_bytes(32, 'big')
@classmethod
def SECP256R1(cls) -> _EllipticCurve:
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 # Curve order
g_x = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
g_y = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
return _EllipticCurve(p=p, a=a, b=b, n=n, g_x=g_x, g_y=g_y)
class EccKey:
def __init__(self, private_key: _EllipticCurve.PrivateKey) -> None:
self.private_key = private_key
@functools.cached_property
def x(self) -> bytes:
return self.private_key.curve.generate_public_key(
self.private_key.key
).x.to_bytes(32, byteorder='big')
@functools.cached_property
def y(self) -> bytes:
return self.private_key.curve.generate_public_key(
self.private_key.key
).y.to_bytes(32, byteorder='big')
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
return self.private_key.curve.ecdh_shared_secret(
self.private_key.key,
_Point(x=x, y=y, curve=self.private_key.curve),
)
@classmethod
def generate(cls) -> EccKey:
return EccKey(_EllipticCurve.SECP256R1().generate_private_key())
@classmethod
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
return EccKey(_EllipticCurve.PrivateKey(d, _EllipticCurve.SECP256R1()))
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
return _ECB(key[::-1]).encrypt(data[::-1])[::-1]
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
return _CMAC(key=k, msg=m).digest()

View File

@@ -0,0 +1,84 @@
# Copyright 2021-2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import functools
from cryptography.hazmat.primitives import ciphers
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import cmac
def e(key: bytes, data: bytes) -> bytes:
'''
AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output.
See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e
'''
cipher = ciphers.Cipher(algorithms.AES(key[::-1]), modes.ECB())
encryptor = cipher.encryptor()
return encryptor.update(data[::-1])[::-1]
class EccKey:
def __init__(self, private_key: ec.EllipticCurvePrivateKey) -> None:
self.private_key = private_key
@classmethod
def generate(cls) -> EccKey:
return EccKey(ec.generate_private_key(ec.SECP256R1()))
@classmethod
def from_private_key_bytes(cls, d_bytes: bytes) -> EccKey:
d = int.from_bytes(d_bytes, byteorder='big', signed=False)
return EccKey(ec.derive_private_key(d, ec.SECP256R1()))
@functools.cached_property
def x(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.x.to_bytes(32, byteorder='big')
)
@functools.cached_property
def y(self) -> bytes:
return (
self.private_key.public_key()
.public_numbers()
.y.to_bytes(32, byteorder='big')
)
def dh(self, public_key_x: bytes, public_key_y: bytes) -> bytes:
x = int.from_bytes(public_key_x, byteorder='big', signed=False)
y = int.from_bytes(public_key_y, byteorder='big', signed=False)
return self.private_key.exchange(
ec.ECDH(),
ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key(),
)
def aes_cmac(m: bytes, k: bytes) -> bytes:
'''
See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC
NOTE: the input and output of this internal function are in big-endian byte order
'''
mac = cmac.CMAC(algorithms.AES(k))
mac.update(m)
return mac.finalize()

View File

@@ -1589,7 +1589,6 @@ class Connection(utils.CompositeEventEmitter):
encryption_key_size: int encryption_key_size: int
authenticated: bool authenticated: bool
sc: bool sc: bool
link_key_type: Optional[int] # [Classic only]
gatt_client: gatt_client.Client gatt_client: gatt_client.Client
pairing_peer_io_capability: Optional[int] pairing_peer_io_capability: Optional[int]
pairing_peer_authentication_requirements: Optional[int] pairing_peer_authentication_requirements: Optional[int]
@@ -1629,6 +1628,7 @@ class Connection(utils.CompositeEventEmitter):
EVENT_PAIRING = "pairing" EVENT_PAIRING = "pairing"
EVENT_PAIRING_FAILURE = "pairing_failure" EVENT_PAIRING_FAILURE = "pairing_failure"
EVENT_SECURITY_REQUEST = "security_request" EVENT_SECURITY_REQUEST = "security_request"
EVENT_LINK_KEY = "link_key"
@utils.composite_listener @utils.composite_listener
class Listener: class Listener:
@@ -1692,7 +1692,6 @@ class Connection(utils.CompositeEventEmitter):
self.encryption_key_size = 0 self.encryption_key_size = 0
self.authenticated = False self.authenticated = False
self.sc = False self.sc = False
self.link_key_type = None
self.att_mtu = ATT_DEFAULT_MTU self.att_mtu = ATT_DEFAULT_MTU
self.data_length = DEVICE_DEFAULT_DATA_LENGTH self.data_length = DEVICE_DEFAULT_DATA_LENGTH
self.gatt_client = gatt_client.Client(self) # Per-connection client self.gatt_client = gatt_client.Client(self) # Per-connection client
@@ -5068,16 +5067,16 @@ class Device(utils.CompositeEventEmitter):
# [Classic only] # [Classic only]
@host_event_handler @host_event_handler
def on_link_key(self, bd_addr, link_key, key_type): def on_link_key(self, bd_addr: hci.Address, link_key: bytes, key_type: int) -> None:
# Store the keys in the key store # Store the keys in the key store
if self.keystore: if self.keystore:
authenticated = key_type in ( authenticated = key_type in (
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE, hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE, hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
) )
pairing_keys = PairingKeys() pairing_keys = PairingKeys(
pairing_keys.link_key = PairingKeys.Key( link_key=PairingKeys.Key(value=link_key, authenticated=authenticated),
value=link_key, authenticated=authenticated link_key_type=key_type,
) )
utils.cancel_on_event( utils.cancel_on_event(
@@ -5087,7 +5086,6 @@ class Device(utils.CompositeEventEmitter):
if connection := self.find_connection_by_bd_addr( if connection := self.find_connection_by_bd_addr(
bd_addr, transport=PhysicalTransport.BR_EDR bd_addr, transport=PhysicalTransport.BR_EDR
): ):
connection.link_key_type = key_type
connection.emit(connection.EVENT_LINK_KEY) connection.emit(connection.EVENT_LINK_KEY)
def add_service(self, service): def add_service(self, service):

View File

@@ -50,6 +50,7 @@ logger = logging.getLogger(__name__)
INTEL_USB_PRODUCTS = { INTEL_USB_PRODUCTS = {
(0x8087, 0x0032), # AX210 (0x8087, 0x0032), # AX210
(0x8087, 0x0033), # AX211
(0x8087, 0x0036), # BE200 (0x8087, 0x0036), # BE200
} }
@@ -293,6 +294,7 @@ class HardwareVariant(utils.OpenIntEnum):
# This is a just a partial list. # This is a just a partial list.
# Add other constants here as new hardware is encountered and tested. # Add other constants here as new hardware is encountered and tested.
TYPHOON_PEAK = 0x17 TYPHOON_PEAK = 0x17
GARFIELD_PEAK = 0x19
GALE_PEAK = 0x1C GALE_PEAK = 0x1C
@@ -471,6 +473,7 @@ class Driver(common.Driver):
raise DriverError("hardware platform not supported") raise DriverError("hardware platform not supported")
if hardware_info.variant not in ( if hardware_info.variant not in (
HardwareVariant.TYPHOON_PEAK, HardwareVariant.TYPHOON_PEAK,
HardwareVariant.GARFIELD_PEAK,
HardwareVariant.GALE_PEAK, HardwareVariant.GALE_PEAK,
): ):
raise DriverError("hardware variant not supported") raise DriverError("hardware variant not supported")

View File

@@ -579,11 +579,7 @@ class Descriptor(Attribute):
if isinstance(self.value, bytes): if isinstance(self.value, bytes):
value_str = self.value.hex() value_str = self.value.hex()
elif isinstance(self.value, CharacteristicValue): elif isinstance(self.value, CharacteristicValue):
value = self.value.read(None) value_str = '<dynamic>'
if isinstance(value, bytes):
value_str = value.hex()
else:
value_str = '<async>'
else: else:
value_str = '<...>' value_str = '<...>'
return ( return (

View File

@@ -315,11 +315,8 @@ class Server(utils.EventEmitter):
self.add_service(service) self.add_service(service)
def read_cccd( def read_cccd(
self, connection: Optional[Connection], characteristic: Characteristic self, connection: Connection, characteristic: Characteristic
) -> bytes: ) -> bytes:
if connection is None:
return bytes([0, 0])
subscribers = self.subscribers.get(connection.handle) subscribers = self.subscribers.get(connection.handle)
cccd = None cccd = None
if subscribers: if subscribers:

View File

@@ -22,14 +22,15 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import dataclasses
import logging import logging
import os import os
import json import json
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Any
from typing_extensions import Self from typing_extensions import Self
from bumble.colors import color from bumble.colors import color
from bumble.hci import Address from bumble import hci
if TYPE_CHECKING: if TYPE_CHECKING:
from bumble.device import Device from bumble.device import Device
@@ -42,16 +43,17 @@ logger = logging.getLogger(__name__)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclasses.dataclass
class PairingKeys: class PairingKeys:
@dataclasses.dataclass
class Key: class Key:
def __init__(self, value, authenticated=False, ediv=None, rand=None): value: bytes
self.value = value authenticated: bool = False
self.authenticated = authenticated ediv: Optional[int] = None
self.ediv = ediv rand: Optional[bytes] = None
self.rand = rand
@classmethod @classmethod
def from_dict(cls, key_dict): def from_dict(cls, key_dict: dict[str, Any]) -> PairingKeys.Key:
value = bytes.fromhex(key_dict['value']) value = bytes.fromhex(key_dict['value'])
authenticated = key_dict.get('authenticated', False) authenticated = key_dict.get('authenticated', False)
ediv = key_dict.get('ediv') ediv = key_dict.get('ediv')
@@ -61,7 +63,7 @@ class PairingKeys:
return cls(value, authenticated, ediv, rand) return cls(value, authenticated, ediv, rand)
def to_dict(self): def to_dict(self) -> dict[str, Any]:
key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated} key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated}
if self.ediv is not None: if self.ediv is not None:
key_dict['ediv'] = self.ediv key_dict['ediv'] = self.ediv
@@ -70,39 +72,42 @@ class PairingKeys:
return key_dict return key_dict
def __init__(self): address_type: Optional[hci.AddressType] = None
self.address_type = None ltk: Optional[Key] = None
self.ltk = None ltk_central: Optional[Key] = None
self.ltk_central = None ltk_peripheral: Optional[Key] = None
self.ltk_peripheral = None irk: Optional[Key] = None
self.irk = None csrk: Optional[Key] = None
self.csrk = None link_key: Optional[Key] = None # Classic
self.link_key = None # Classic link_key_type: Optional[int] = None # Classic
@staticmethod @classmethod
def key_from_dict(keys_dict, key_name): def key_from_dict(cls, keys_dict: dict[str, Any], key_name: str) -> Optional[Key]:
key_dict = keys_dict.get(key_name) key_dict = keys_dict.get(key_name)
if key_dict is None: if key_dict is None:
return None return None
return PairingKeys.Key.from_dict(key_dict) return PairingKeys.Key.from_dict(key_dict)
@staticmethod @classmethod
def from_dict(keys_dict): def from_dict(cls, keys_dict: dict[str, Any]) -> PairingKeys:
keys = PairingKeys() return PairingKeys(
address_type=(
hci.AddressType(t)
if (t := keys_dict.get('address_type')) is not None
else None
),
ltk=PairingKeys.key_from_dict(keys_dict, 'ltk'),
ltk_central=PairingKeys.key_from_dict(keys_dict, 'ltk_central'),
ltk_peripheral=PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral'),
irk=PairingKeys.key_from_dict(keys_dict, 'irk'),
csrk=PairingKeys.key_from_dict(keys_dict, 'csrk'),
link_key=PairingKeys.key_from_dict(keys_dict, 'link_key'),
link_key_type=keys_dict.get('link_key_type'),
)
keys.address_type = keys_dict.get('address_type') def to_dict(self) -> dict[str, Any]:
keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk') keys: dict[str, Any] = {}
keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central')
keys.ltk_peripheral = PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral')
keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk')
keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk')
keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key')
return keys
def to_dict(self):
keys = {}
if self.address_type is not None: if self.address_type is not None:
keys['address_type'] = self.address_type keys['address_type'] = self.address_type
@@ -125,9 +130,12 @@ class PairingKeys:
if self.link_key is not None: if self.link_key is not None:
keys['link_key'] = self.link_key.to_dict() keys['link_key'] = self.link_key.to_dict()
if self.link_key_type is not None:
keys['link_key_type'] = self.link_key_type
return keys return keys
def print(self, prefix=''): def print(self, prefix: str = '') -> None:
keys_dict = self.to_dict() keys_dict = self.to_dict()
for container_property, value in keys_dict.items(): for container_property, value in keys_dict.items():
if isinstance(value, dict): if isinstance(value, dict):
@@ -156,20 +164,28 @@ class KeyStore:
all_keys = await self.get_all() all_keys = await self.get_all()
await asyncio.gather(*(self.delete(name) for (name, _) in all_keys)) await asyncio.gather(*(self.delete(name) for (name, _) in all_keys))
async def get_resolving_keys(self): async def get_resolving_keys(self) -> list[tuple[bytes, hci.Address]]:
all_keys = await self.get_all() all_keys = await self.get_all()
resolving_keys = [] resolving_keys = []
for name, keys in all_keys: for name, keys in all_keys:
if keys.irk is not None: if keys.irk is not None:
if keys.address_type is None: resolving_keys.append(
address_type = Address.RANDOM_DEVICE_ADDRESS (
else: keys.irk.value,
address_type = keys.address_type hci.Address(
resolving_keys.append((keys.irk.value, Address(name, address_type))) name,
(
keys.address_type
if keys.address_type is not None
else hci.Address.RANDOM_DEVICE_ADDRESS
),
),
)
)
return resolving_keys return resolving_keys
async def print(self, prefix=''): async def print(self, prefix: str = '') -> None:
entries = await self.get_all() entries = await self.get_all()
separator = '' separator = ''
for name, keys in entries: for name, keys in entries:
@@ -177,8 +193,8 @@ class KeyStore:
keys.print(prefix=prefix + ' ') keys.print(prefix=prefix + ' ')
separator = '\n' separator = '\n'
@staticmethod @classmethod
def create_for_device(device: Device) -> KeyStore: def create_for_device(cls, device: Device) -> KeyStore:
if device.config.keystore is None: if device.config.keystore is None:
return MemoryKeyStore() return MemoryKeyStore()
@@ -266,9 +282,9 @@ class JsonKeyStore(KeyStore):
filename = params[0] filename = params[0]
# Use a namespace based on the device address # Use a namespace based on the device address
if device.public_address not in (Address.ANY, Address.ANY_RANDOM): if device.public_address not in (hci.Address.ANY, hci.Address.ANY_RANDOM):
namespace = str(device.public_address) namespace = str(device.public_address)
elif device.random_address != Address.ANY_RANDOM: elif device.random_address != hci.Address.ANY_RANDOM:
namespace = str(device.random_address) namespace = str(device.random_address)
else: else:
namespace = JsonKeyStore.DEFAULT_NAMESPACE namespace = JsonKeyStore.DEFAULT_NAMESPACE

View File

@@ -15,6 +15,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import contextlib import contextlib
from collections.abc import Awaitable
import grpc import grpc
import logging import logging
@@ -24,6 +25,7 @@ from bumble import hci
from bumble.core import ( from bumble.core import (
PhysicalTransport, PhysicalTransport,
ProtocolError, ProtocolError,
InvalidArgumentError,
) )
import bumble.utils import bumble.utils
from bumble.device import Connection as BumbleConnection, Device from bumble.device import Connection as BumbleConnection, Device
@@ -188,35 +190,6 @@ class PairingDelegate(BasePairingDelegate):
self.service.event_queue.put_nowait(event) self.service.event_queue.put_nowait(event)
BR_LEVEL_REACHED: Dict[SecurityLevel, Callable[[BumbleConnection], bool]] = {
LEVEL0: lambda connection: True,
LEVEL1: lambda connection: connection.encryption == 0 or connection.authenticated,
LEVEL2: lambda connection: connection.encryption != 0 and connection.authenticated,
LEVEL3: lambda connection: connection.encryption != 0
and connection.authenticated
and connection.link_key_type
in (
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
),
LEVEL4: lambda connection: connection.encryption
== hci.HCI_Encryption_Change_Event.AES_CCM
and connection.authenticated
and connection.link_key_type
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
}
LE_LEVEL_REACHED: Dict[LESecurityLevel, Callable[[BumbleConnection], bool]] = {
LE_LEVEL1: lambda connection: True,
LE_LEVEL2: lambda connection: connection.encryption != 0,
LE_LEVEL3: lambda connection: connection.encryption != 0
and connection.authenticated,
LE_LEVEL4: lambda connection: connection.encryption != 0
and connection.authenticated
and connection.sc,
}
class SecurityService(SecurityServicer): class SecurityService(SecurityServicer):
def __init__(self, device: Device, config: Config) -> None: def __init__(self, device: Device, config: Config) -> None:
self.log = utils.BumbleServerLoggerAdapter( self.log = utils.BumbleServerLoggerAdapter(
@@ -248,6 +221,59 @@ class SecurityService(SecurityServicer):
self.device.pairing_config_factory = pairing_config_factory self.device.pairing_config_factory = pairing_config_factory
async def _classic_level_reached(
self, level: SecurityLevel, connection: BumbleConnection
) -> bool:
if level == LEVEL0:
return True
if level == LEVEL1:
return connection.encryption == 0 or connection.authenticated
if level == LEVEL2:
return connection.encryption != 0 and connection.authenticated
link_key_type: Optional[int] = None
if (keystore := connection.device.keystore) and (
keys := await keystore.get(str(connection.peer_address))
):
link_key_type = keys.link_key_type
self.log.debug("link_key_type: %d", link_key_type)
if level == LEVEL3:
return (
connection.encryption != 0
and connection.authenticated
and link_key_type
in (
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
)
)
if level == LEVEL4:
return (
connection.encryption == hci.HCI_Encryption_Change_Event.AES_CCM
and connection.authenticated
and link_key_type
== hci.HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE
)
raise InvalidArgumentError(f"Unexpected level {level}")
def _le_level_reached(
self, level: LESecurityLevel, connection: BumbleConnection
) -> bool:
if level == LE_LEVEL1:
return True
if level == LE_LEVEL2:
return connection.encryption != 0
if level == LE_LEVEL3:
return connection.encryption != 0 and connection.authenticated
if level == LE_LEVEL4:
return (
connection.encryption != 0
and connection.authenticated
and connection.sc
)
raise InvalidArgumentError(f"Unexpected level {level}")
@utils.rpc @utils.rpc
async def OnPairing( async def OnPairing(
self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext self, request: AsyncIterator[PairingEventAnswer], context: grpc.ServicerContext
@@ -290,7 +316,7 @@ class SecurityService(SecurityServicer):
] == oneof ] == oneof
# security level already reached # security level already reached
if self.reached_security_level(connection, level): if await self.reached_security_level(connection, level):
return SecureResponse(success=empty_pb2.Empty()) return SecureResponse(success=empty_pb2.Empty())
# trigger pairing if needed # trigger pairing if needed
@@ -361,7 +387,7 @@ class SecurityService(SecurityServicer):
return SecureResponse(encryption_failure=empty_pb2.Empty()) return SecureResponse(encryption_failure=empty_pb2.Empty())
# security level has been reached ? # security level has been reached ?
if self.reached_security_level(connection, level): if await self.reached_security_level(connection, level):
return SecureResponse(success=empty_pb2.Empty()) return SecureResponse(success=empty_pb2.Empty())
return SecureResponse(not_reached=empty_pb2.Empty()) return SecureResponse(not_reached=empty_pb2.Empty())
@@ -388,13 +414,10 @@ class SecurityService(SecurityServicer):
pair_task: Optional[asyncio.Future[None]] = None pair_task: Optional[asyncio.Future[None]] = None
async def authenticate() -> None: async def authenticate() -> None:
assert connection
if (encryption := connection.encryption) != 0: if (encryption := connection.encryption) != 0:
self.log.debug('Disable encryption...') self.log.debug('Disable encryption...')
try: with contextlib.suppress(Exception):
await connection.encrypt(enable=False) await connection.encrypt(enable=False)
except:
pass
self.log.debug('Disable encryption: done') self.log.debug('Disable encryption: done')
self.log.debug('Authenticate...') self.log.debug('Authenticate...')
@@ -413,15 +436,13 @@ class SecurityService(SecurityServicer):
return wrapper return wrapper
def try_set_success(*_: Any) -> None: async def try_set_success(*_: Any) -> None:
assert connection if await self.reached_security_level(connection, level):
if self.reached_security_level(connection, level):
self.log.debug('Wait for security: done') self.log.debug('Wait for security: done')
wait_for_security.set_result('success') wait_for_security.set_result('success')
def on_encryption_change(*_: Any) -> None: async def on_encryption_change(*_: Any) -> None:
assert connection if await self.reached_security_level(connection, level):
if self.reached_security_level(connection, level):
self.log.debug('Wait for security: done') self.log.debug('Wait for security: done')
wait_for_security.set_result('success') wait_for_security.set_result('success')
elif ( elif (
@@ -436,7 +457,7 @@ class SecurityService(SecurityServicer):
if self.need_pairing(connection, level): if self.need_pairing(connection, level):
pair_task = asyncio.create_task(connection.pair()) pair_task = asyncio.create_task(connection.pair())
listeners: Dict[str, Callable[..., None]] = { listeners: Dict[str, Callable[..., Union[None, Awaitable[None]]]] = {
'disconnection': set_failure('connection_died'), 'disconnection': set_failure('connection_died'),
'pairing_failure': set_failure('pairing_failure'), 'pairing_failure': set_failure('pairing_failure'),
'connection_authentication_failure': set_failure('authentication_failure'), 'connection_authentication_failure': set_failure('authentication_failure'),
@@ -455,7 +476,7 @@ class SecurityService(SecurityServicer):
watcher.on(connection, event, listener) watcher.on(connection, event, listener)
# security level already reached # security level already reached
if self.reached_security_level(connection, level): if await self.reached_security_level(connection, level):
return WaitSecurityResponse(success=empty_pb2.Empty()) return WaitSecurityResponse(success=empty_pb2.Empty())
self.log.debug('Wait for security...') self.log.debug('Wait for security...')
@@ -465,24 +486,20 @@ class SecurityService(SecurityServicer):
# wait for `authenticate` to finish if any # wait for `authenticate` to finish if any
if authenticate_task is not None: if authenticate_task is not None:
self.log.debug('Wait for authentication...') self.log.debug('Wait for authentication...')
try: with contextlib.suppress(Exception):
await authenticate_task # type: ignore await authenticate_task # type: ignore
except:
pass
self.log.debug('Authenticated') self.log.debug('Authenticated')
# wait for `pair` to finish if any # wait for `pair` to finish if any
if pair_task is not None: if pair_task is not None:
self.log.debug('Wait for authentication...') self.log.debug('Wait for authentication...')
try: with contextlib.suppress(Exception):
await pair_task # type: ignore await pair_task # type: ignore
except:
pass
self.log.debug('paired') self.log.debug('paired')
return WaitSecurityResponse(**kwargs) return WaitSecurityResponse(**kwargs)
def reached_security_level( async def reached_security_level(
self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel] self, connection: BumbleConnection, level: Union[SecurityLevel, LESecurityLevel]
) -> bool: ) -> bool:
self.log.debug( self.log.debug(
@@ -492,15 +509,14 @@ class SecurityService(SecurityServicer):
'encryption': connection.encryption, 'encryption': connection.encryption,
'authenticated': connection.authenticated, 'authenticated': connection.authenticated,
'sc': connection.sc, 'sc': connection.sc,
'link_key_type': connection.link_key_type,
} }
) )
) )
if isinstance(level, LESecurityLevel): if isinstance(level, LESecurityLevel):
return LE_LEVEL_REACHED[level](connection) return self._le_level_reached(level, connection)
return BR_LEVEL_REACHED[level](connection) return await self._classic_level_reached(level, connection)
def need_pairing(self, connection: BumbleConnection, level: int) -> bool: def need_pairing(self, connection: BumbleConnection, level: int) -> bool:
if connection.transport == PhysicalTransport.LE: if connection.transport == PhysicalTransport.LE:

View File

@@ -198,8 +198,7 @@ class AudioInputControlPoint:
audio_input_state: AudioInputState audio_input_state: AudioInputState
gain_settings_properties: GainSettingsProperties gain_settings_properties: GainSettingsProperties
async def on_write(self, connection: Optional[Connection], value: bytes) -> None: async def on_write(self, connection: Connection, value: bytes) -> None:
assert connection
opcode = AudioInputControlPointOpCode(value[0]) opcode = AudioInputControlPointOpCode(value[0])
@@ -320,11 +319,10 @@ class AudioInputDescription:
audio_input_description: str = "Bluetooth" audio_input_description: str = "Bluetooth"
attribute: Optional[Attribute] = None attribute: Optional[Attribute] = None
def on_read(self, _connection: Optional[Connection]) -> str: def on_read(self, _connection: Connection) -> str:
return self.audio_input_description return self.audio_input_description
async def on_write(self, connection: Optional[Connection], value: str) -> None: async def on_write(self, connection: Connection, value: str) -> None:
assert connection
assert self.attribute assert self.attribute
self.audio_input_description = value self.audio_input_description = value

View File

@@ -590,7 +590,7 @@ class AseStateMachine(gatt.Characteristic):
# Readonly. Do nothing in the setter. # Readonly. Do nothing in the setter.
pass pass
def on_read(self, _: Optional[device.Connection]) -> bytes: def on_read(self, _: device.Connection) -> bytes:
return self.value return self.value
def __str__(self) -> str: def __str__(self) -> str:

View File

@@ -200,7 +200,7 @@ class AshaService(gatt.TemplateService):
# Handler for audio control commands # Handler for audio control commands
async def _on_audio_control_point_write( async def _on_audio_control_point_write(
self, connection: Optional[Connection], value: bytes self, connection: Connection, value: bytes
) -> None: ) -> None:
_logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}') _logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
opcode = value[0] opcode = value[0]
@@ -247,7 +247,7 @@ class AshaService(gatt.TemplateService):
) )
# Handler for volume control # Handler for volume control
def _on_volume_write(self, connection: Optional[Connection], value: bytes) -> None: def _on_volume_write(self, connection: Connection, value: bytes) -> None:
_logger.debug(f'--- VOLUME Write:{value[0]}') _logger.debug(f'--- VOLUME Write:{value[0]}')
self.volume = value[0] self.volume = value[0]
self.emit(self.EVENT_VOLUME_CHANGED) self.emit(self.EVENT_VOLUME_CHANGED)

View File

@@ -164,12 +164,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
super().__init__(characteristics) super().__init__(characteristics)
async def on_sirk_read(self, connection: Optional[device.Connection]) -> bytes: async def on_sirk_read(self, connection: device.Connection) -> bytes:
if self.set_identity_resolving_key_type == SirkType.PLAINTEXT: if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
sirk_bytes = self.set_identity_resolving_key sirk_bytes = self.set_identity_resolving_key
else: else:
assert connection
if connection.transport == core.PhysicalTransport.LE: if connection.transport == core.PhysicalTransport.LE:
key = await connection.device.get_long_term_key( key = await connection.device.get_long_term_key(
connection_handle=connection.handle, rand=b'', ediv=0 connection_handle=connection.handle, rand=b'', ediv=0

View File

@@ -127,9 +127,7 @@ class GenericAttributeProfileService(gatt.TemplateService):
return b'' return b''
def get_database_hash(self, connection: device.Connection | None) -> bytes: def get_database_hash(self, connection: device.Connection) -> bytes:
assert connection
m = b''.join( m = b''.join(
[ [
self.get_attribute_data(attribute) self.get_attribute_data(attribute)

View File

@@ -335,9 +335,8 @@ class HearingAccessService(gatt.TemplateService):
utils.cancel_on_event(connection, 'disconnection', on_connection_async()) utils.cancel_on_event(connection, 'disconnection', on_connection_async())
def _on_read_active_preset_index( def _on_read_active_preset_index(self, connection: Connection) -> bytes:
self, __connection__: Optional[Connection] del connection # Unused
) -> bytes:
return bytes([self.active_preset_index]) return bytes([self.active_preset_index])
# TODO this need to be triggered when device is unbonded # TODO this need to be triggered when device is unbonded
@@ -345,18 +344,13 @@ class HearingAccessService(gatt.TemplateService):
self.preset_changed_operations_history_per_device.pop(addr) self.preset_changed_operations_history_per_device.pop(addr)
async def _on_write_hearing_aid_preset_control_point( async def _on_write_hearing_aid_preset_control_point(
self, connection: Optional[Connection], value: bytes self, connection: Connection, value: bytes
): ):
assert connection
opcode = HearingAidPresetControlPointOpcode(value[0]) opcode = HearingAidPresetControlPointOpcode(value[0])
handler = getattr(self, '_on_' + opcode.name.lower()) handler = getattr(self, '_on_' + opcode.name.lower())
await handler(connection, value) await handler(connection, value)
async def _on_read_presets_request( async def _on_read_presets_request(self, connection: Connection, value: bytes):
self, connection: Optional[Connection], value: bytes
):
assert connection
if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
logging.warning(f'HAS require MTU >= 49: {connection}') logging.warning(f'HAS require MTU >= 49: {connection}')
@@ -471,10 +465,7 @@ class HearingAccessService(gatt.TemplateService):
for connection in self.currently_connected_clients: for connection in self.currently_connected_clients:
await self._preset_changed_operation(connection) await self._preset_changed_operation(connection)
async def _on_write_preset_name( async def _on_write_preset_name(self, connection: Connection, value: bytes):
self, connection: Optional[Connection], value: bytes
):
assert connection
if self.read_presets_request_in_progress: if self.read_presets_request_in_progress:
raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS) raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
@@ -522,10 +513,7 @@ class HearingAccessService(gatt.TemplateService):
for connection in self.currently_connected_clients: for connection in self.currently_connected_clients:
await self.notify_active_preset_for_connection(connection) await self.notify_active_preset_for_connection(connection)
async def set_active_preset( async def set_active_preset(self, connection: Connection, value: bytes) -> None:
self, connection: Optional[Connection], value: bytes
) -> None:
assert connection
index = value[1] index = value[1]
preset = self.preset_records.get(index, None) preset = self.preset_records.get(index, None)
if ( if (
@@ -542,16 +530,11 @@ class HearingAccessService(gatt.TemplateService):
self.active_preset_index = index self.active_preset_index = index
await self.notify_active_preset() await self.notify_active_preset()
async def _on_set_active_preset( async def _on_set_active_preset(self, connection: Connection, value: bytes):
self, connection: Optional[Connection], value: bytes
):
await self.set_active_preset(connection, value) await self.set_active_preset(connection, value)
async def set_next_or_previous_preset( async def set_next_or_previous_preset(self, connection: Connection, is_previous):
self, connection: Optional[Connection], is_previous
):
'''Set the next or the previous preset as active''' '''Set the next or the previous preset as active'''
assert connection
if self.active_preset_index == 0x00: if self.active_preset_index == 0x00:
raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE) raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
@@ -581,17 +564,17 @@ class HearingAccessService(gatt.TemplateService):
await self.notify_active_preset() await self.notify_active_preset()
async def _on_set_next_preset( async def _on_set_next_preset(
self, connection: Optional[Connection], __value__: bytes self, connection: Connection, __value__: bytes
) -> None: ) -> None:
await self.set_next_or_previous_preset(connection, False) await self.set_next_or_previous_preset(connection, False)
async def _on_set_previous_preset( async def _on_set_previous_preset(
self, connection: Optional[Connection], __value__: bytes self, connection: Connection, __value__: bytes
) -> None: ) -> None:
await self.set_next_or_previous_preset(connection, True) await self.set_next_or_previous_preset(connection, True)
async def _on_set_active_preset_synchronized_locally( async def _on_set_active_preset_synchronized_locally(
self, connection: Optional[Connection], value: bytes self, connection: Connection, value: bytes
): ):
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support
@@ -602,7 +585,7 @@ class HearingAccessService(gatt.TemplateService):
# TODO (low priority) inform other server of the change # TODO (low priority) inform other server of the change
async def _on_set_next_preset_synchronized_locally( async def _on_set_next_preset_synchronized_locally(
self, connection: Optional[Connection], __value__: bytes self, connection: Connection, __value__: bytes
): ):
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support
@@ -613,7 +596,7 @@ class HearingAccessService(gatt.TemplateService):
# TODO (low priority) inform other server of the change # TODO (low priority) inform other server of the change
async def _on_set_previous_preset_synchronized_locally( async def _on_set_previous_preset_synchronized_locally(
self, connection: Optional[Connection], __value__: bytes self, connection: Connection, __value__: bytes
): ):
if ( if (
self.server_features.preset_synchronization_support self.server_features.preset_synchronization_support

View File

@@ -287,11 +287,8 @@ class MediaControlService(gatt.TemplateService):
) )
async def on_media_control_point( async def on_media_control_point(
self, connection: Optional[device.Connection], data: bytes self, connection: device.Connection, data: bytes
) -> None: ) -> None:
if not connection:
raise core.InvalidStateError()
opcode = MediaControlPointOpcode(data[0]) opcode = MediaControlPointOpcode(data[0])
await connection.device.notify_subscriber( await connection.device.notify_subscriber(

View File

@@ -146,14 +146,12 @@ class VolumeControlService(gatt.TemplateService):
included_services=list(included_services), included_services=list(included_services),
) )
def _on_read_volume_state(self, _connection: Optional[device.Connection]) -> bytes: def _on_read_volume_state(self, _connection: device.Connection) -> bytes:
return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter)) return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
def _on_write_volume_control_point( def _on_write_volume_control_point(
self, connection: Optional[device.Connection], value: bytes self, connection: device.Connection, value: bytes
) -> None: ) -> None:
assert connection
opcode = VolumeControlPointOpcode(value[0]) opcode = VolumeControlPointOpcode(value[0])
change_counter = value[1] change_counter = value[1]

View File

@@ -86,7 +86,7 @@ class VolumeOffsetState:
assert self.attribute is not None assert self.attribute is not None
await connection.device.notify_subscribers(attribute=self.attribute) await connection.device.notify_subscribers(attribute=self.attribute)
def on_read(self, _connection: Optional[Connection]) -> bytes: def on_read(self, _connection: Connection) -> bytes:
return bytes(self) return bytes(self)
@@ -103,11 +103,10 @@ class VocsAudioLocation:
audio_location = AudioLocation(struct.unpack('<I', data)[0]) audio_location = AudioLocation(struct.unpack('<I', data)[0])
return cls(audio_location) return cls(audio_location)
def on_read(self, _connection: Optional[Connection]) -> bytes: def on_read(self, _connection: Connection) -> bytes:
return bytes(self) return bytes(self)
async def on_write(self, connection: Optional[Connection], value: bytes) -> None: async def on_write(self, connection: Connection, value: bytes) -> None:
assert connection
assert self.attribute assert self.attribute
self.audio_location = AudioLocation(int.from_bytes(value, 'little')) self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
@@ -118,8 +117,7 @@ class VocsAudioLocation:
class VolumeOffsetControlPoint: class VolumeOffsetControlPoint:
volume_offset_state: VolumeOffsetState volume_offset_state: VolumeOffsetState
async def on_write(self, connection: Optional[Connection], value: bytes) -> None: async def on_write(self, connection: Connection, value: bytes) -> None:
assert connection
opcode = value[0] opcode = value[0]
if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET: if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
@@ -159,11 +157,10 @@ class AudioOutputDescription:
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
return self.audio_output_description.encode('utf-8') return self.audio_output_description.encode('utf-8')
def on_read(self, _connection: Optional[Connection]) -> bytes: def on_read(self, _connection: Connection) -> bytes:
return bytes(self) return bytes(self)
async def on_write(self, connection: Optional[Connection], value: bytes) -> None: async def on_write(self, connection: Connection, value: bytes) -> None:
assert connection
assert self.attribute assert self.attribute
self.audio_output_description = value.decode('utf-8') self.audio_output_description = value.decode('utf-8')

View File

@@ -1380,8 +1380,10 @@ class Session:
ediv=self.ltk_ediv, ediv=self.ltk_ediv,
rand=self.ltk_rand, rand=self.ltk_rand,
) )
if not self.peer_ltk:
logger.error("peer_ltk is None")
peer_ltk_key = PairingKeys.Key( peer_ltk_key = PairingKeys.Key(
value=self.peer_ltk, value=self.peer_ltk or b'',
authenticated=authenticated, authenticated=authenticated,
ediv=self.peer_ediv, ediv=self.peer_ediv,
rand=self.peer_rand, rand=self.peer_rand,

View File

@@ -13,11 +13,11 @@ dependencies = [
"aiohttp ~= 3.8; platform_system!='Emscripten'", "aiohttp ~= 3.8; platform_system!='Emscripten'",
"appdirs >= 1.4; platform_system!='Emscripten'", "appdirs >= 1.4; platform_system!='Emscripten'",
"click >= 8.1.3; platform_system!='Emscripten'", "click >= 8.1.3; platform_system!='Emscripten'",
"cryptography >= 39; platform_system!='Emscripten'", "cryptography >= 44.0.3; platform_system!='Emscripten'",
# Pyodide bundles a version of cryptography that is built for wasm, which may not match the # Pyodide bundles a version of cryptography that is built for wasm, which may not match the
# versions available on PyPI. Relax the version requirement since it's better than being # versions available on PyPI. Relax the version requirement since it's better than being
# completely unable to import the package in case of version mismatch. # completely unable to import the package in case of version mismatch.
"cryptography >= 39.0; platform_system=='Emscripten'", "cryptography >= 44.0.3; platform_system=='Emscripten'",
"grpcio >= 1.62.1; platform_system!='Emscripten'", "grpcio >= 1.62.1; platform_system!='Emscripten'",
"humanize >= 4.6.0; platform_system!='Emscripten'", "humanize >= 4.6.0; platform_system!='Emscripten'",
"libusb1 >= 2.0.1; platform_system!='Emscripten'", "libusb1 >= 2.0.1; platform_system!='Emscripten'",

View File

@@ -136,9 +136,9 @@ async def test_characteristic_encoding():
Characteristic.READABLE, Characteristic.READABLE,
123, 123,
) )
x = await c.read_value(None) x = await c.read_value(Mock())
assert x == bytes([123]) assert x == bytes([123])
await c.write_value(None, bytes([122])) await c.write_value(Mock(), bytes([122]))
assert c.value == 122 assert c.value == 122
class FooProxy(CharacteristicProxy): class FooProxy(CharacteristicProxy):
@@ -334,7 +334,7 @@ async def test_CharacteristicAdapter() -> None:
) )
v = bytes([3, 4, 5]) v = bytes([3, 4, 5])
await c.write_value(None, v) await c.write_value(Mock(), v)
assert c.value == v assert c.value == v
# Simple delegated adapter # Simple delegated adapter
@@ -342,11 +342,11 @@ async def test_CharacteristicAdapter() -> None:
c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x)) c, lambda x: bytes(reversed(x)), lambda x: bytes(reversed(x))
) )
delegated_value = await delegated.read_value(None) delegated_value = await delegated.read_value(Mock())
assert delegated_value == bytes(reversed(v)) assert delegated_value == bytes(reversed(v))
delegated_value2 = bytes([3, 4, 5]) delegated_value2 = bytes([3, 4, 5])
await delegated.write_value(None, delegated_value2) await delegated.write_value(Mock(), delegated_value2)
assert delegated.value == bytes(reversed(delegated_value2)) assert delegated.value == bytes(reversed(delegated_value2))
# Packed adapter with single element format # Packed adapter with single element format
@@ -355,10 +355,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = packed_value_ref c.value = packed_value_ref
packed = PackedCharacteristicAdapter(c, '>H') packed = PackedCharacteristicAdapter(c, '>H')
packed_value_read = await packed.read_value(None) packed_value_read = await packed.read_value(Mock())
assert packed_value_read == packed_value_bytes assert packed_value_read == packed_value_bytes
c.value = b'' c.value = b''
await packed.write_value(None, packed_value_bytes) await packed.write_value(Mock(), packed_value_bytes)
assert packed.value == packed_value_ref assert packed.value == packed_value_ref
# Packed adapter with multi-element format # Packed adapter with multi-element format
@@ -368,10 +368,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = (v1, v2) c.value = (v1, v2)
packed_multi = PackedCharacteristicAdapter(c, '>HH') packed_multi = PackedCharacteristicAdapter(c, '>HH')
packed_multi_read_value = await packed_multi.read_value(None) packed_multi_read_value = await packed_multi.read_value(Mock())
assert packed_multi_read_value == packed_multi_value_bytes assert packed_multi_read_value == packed_multi_value_bytes
packed_multi.value = b'' packed_multi.value = b''
await packed_multi.write_value(None, packed_multi_value_bytes) await packed_multi.write_value(Mock(), packed_multi_value_bytes)
assert packed_multi.value == (v1, v2) assert packed_multi.value == (v1, v2)
# Mapped adapter # Mapped adapter
@@ -382,10 +382,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = mapped c.value = mapped
packed_mapped = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2')) packed_mapped = MappedCharacteristicAdapter(c, '>HH', ('v1', 'v2'))
packed_mapped_read_value = await packed_mapped.read_value(None) packed_mapped_read_value = await packed_mapped.read_value(Mock())
assert packed_mapped_read_value == packed_mapped_value_bytes assert packed_mapped_read_value == packed_mapped_value_bytes
c.value = b'' c.value = b''
await packed_mapped.write_value(None, packed_mapped_value_bytes) await packed_mapped.write_value(Mock(), packed_mapped_value_bytes)
assert packed_mapped.value == mapped assert packed_mapped.value == mapped
# UTF-8 adapter # UTF-8 adapter
@@ -394,10 +394,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = string_value c.value = string_value
string_c = UTF8CharacteristicAdapter(c) string_c = UTF8CharacteristicAdapter(c)
string_read_value = await string_c.read_value(None) string_read_value = await string_c.read_value(Mock())
assert string_read_value == string_value_bytes assert string_read_value == string_value_bytes
c.value = b'' c.value = b''
await string_c.write_value(None, string_value_bytes) await string_c.write_value(Mock(), string_value_bytes)
assert string_c.value == string_value assert string_c.value == string_value
# Class adapter # Class adapter
@@ -419,10 +419,10 @@ async def test_CharacteristicAdapter() -> None:
c.value = class_value c.value = class_value
class_c = SerializableCharacteristicAdapter(c, BlaBla) class_c = SerializableCharacteristicAdapter(c, BlaBla)
class_read_value = await class_c.read_value(None) class_read_value = await class_c.read_value(Mock())
assert class_read_value == class_value_bytes assert class_read_value == class_value_bytes
class_c.value = b'' class_c.value = b''
await class_c.write_value(None, class_value_bytes) await class_c.write_value(Mock(), class_value_bytes)
assert isinstance(class_c.value, BlaBla) assert isinstance(class_c.value, BlaBla)
assert class_c.value.a == class_value.a assert class_c.value.a == class_value.a
assert class_c.value.b == class_value.b assert class_c.value.b == class_value.b
@@ -436,10 +436,10 @@ async def test_CharacteristicAdapter() -> None:
enum_value_bytes = int(enum_value).to_bytes(3, 'big') enum_value_bytes = int(enum_value).to_bytes(3, 'big')
c.value = enum_value c.value = enum_value
enum_c = EnumCharacteristicAdapter(c, MyEnum, 3, 'big') enum_c = EnumCharacteristicAdapter(c, MyEnum, 3, 'big')
enum_read_value = await enum_c.read_value(None) enum_read_value = await enum_c.read_value(Mock())
assert enum_read_value == enum_value_bytes assert enum_read_value == enum_value_bytes
enum_c.value = b'' enum_c.value = b''
await enum_c.write_value(None, enum_value_bytes) await enum_c.write_value(Mock(), enum_value_bytes)
assert isinstance(enum_c.value, MyEnum) assert isinstance(enum_c.value, MyEnum)
assert enum_c.value == enum_value assert enum_c.value == enum_value
@@ -1254,7 +1254,7 @@ Characteristic(handle=0x0005, end=0x0005, uuid=UUID-16:2A01 (Appearance), READ)
Service(handle=0x0006, end=0x000D, uuid=UUID-16:1801 (Generic Attribute)) Service(handle=0x0006, end=0x000D, uuid=UUID-16:1801 (Generic Attribute))
CharacteristicDeclaration(handle=0x0007, value_handle=0x0008, uuid=UUID-16:2A05 (Service Changed), INDICATE) CharacteristicDeclaration(handle=0x0007, value_handle=0x0008, uuid=UUID-16:2A05 (Service Changed), INDICATE)
Characteristic(handle=0x0008, end=0x0009, uuid=UUID-16:2A05 (Service Changed), INDICATE) Characteristic(handle=0x0008, end=0x0009, uuid=UUID-16:2A05 (Service Changed), INDICATE)
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=0000) Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=<dynamic>)
CharacteristicDeclaration(handle=0x000A, value_handle=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE) CharacteristicDeclaration(handle=0x000A, value_handle=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE)
Characteristic(handle=0x000B, end=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE) Characteristic(handle=0x000B, end=0x000B, uuid=UUID-16:2B29 (Client Supported Features), READ|WRITE)
CharacteristicDeclaration(handle=0x000C, value_handle=0x000D, uuid=UUID-16:2B2A (Database Hash), READ) CharacteristicDeclaration(handle=0x000C, value_handle=0x000D, uuid=UUID-16:2B2A (Database Hash), READ)
@@ -1262,7 +1262,7 @@ Characteristic(handle=0x000D, end=0x000D, uuid=UUID-16:2B2A (Database Hash), REA
Service(handle=0x000E, end=0x0011, uuid=3A657F47-D34F-46B3-B1EC-698E29B6B829) Service(handle=0x000E, end=0x0011, uuid=3A657F47-D34F-46B3-B1EC-698E29B6B829)
CharacteristicDeclaration(handle=0x000F, value_handle=0x0010, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY) CharacteristicDeclaration(handle=0x000F, value_handle=0x0010, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
Characteristic(handle=0x0010, end=0x0011, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY) Characteristic(handle=0x0010, end=0x0011, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, READ|WRITE|NOTIFY)
Descriptor(handle=0x0011, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)""" Descriptor(handle=0x0011, type=UUID-16:2902 (Client Characteristic Configuration), value=<dynamic>)"""
) )

View File

@@ -21,17 +21,30 @@ from unittest import mock
from bumble import smp from bumble import smp
from bumble import pairing from bumble import pairing
from bumble import crypto
from bumble.crypto import EccKey, aes_cmac, ah, c1, f4, f5, f6, g2, h6, h7, s1 from bumble.crypto import EccKey, aes_cmac, ah, c1, f4, f5, f6, g2, h6, h7, s1
from bumble.pairing import OobData, OobSharedData, LeRole from bumble.pairing import OobData, OobSharedData, LeRole
from bumble.hci import Address from bumble.hci import Address
from bumble.core import AdvertisingData from bumble.core import AdvertisingData
from bumble.device import Device from bumble.device import Device
from typing import Optional from typing import Optional, Any
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# pylint: disable=invalid-name # pylint: disable=invalid-name
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@pytest.fixture(
scope="session", params=["bumble.crypto.builtin", "bumble.crypto.cryptography"]
)
def crypto_backend(request):
backend = pytest.importorskip(request.param)
with (
mock.patch.object(crypto, "e", backend.e),
mock.patch.object(crypto, "aes_cmac", backend.aes_cmac),
mock.patch.object(crypto, "EccKey", backend.EccKey),
):
yield
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -40,7 +53,7 @@ def reversed_hex(hex_str: str) -> bytes:
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_ecc(): def test_ecc(crypto_backend):
key = EccKey.generate() key = EccKey.generate()
x = key.x x = key.x
y = key.y y = key.y
@@ -69,21 +82,17 @@ def test_ecc():
) )
dhkey = 'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698' dhkey = 'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698'
key_a = EccKey.from_private_key_bytes( key_a = EccKey.from_private_key_bytes(bytes.fromhex(private_A))
bytes.fromhex(private_A), bytes.fromhex(public_A_x), bytes.fromhex(public_A_y)
)
shared_key = key_a.dh(bytes.fromhex(public_B_x), bytes.fromhex(public_B_y)) shared_key = key_a.dh(bytes.fromhex(public_B_x), bytes.fromhex(public_B_y))
assert shared_key == bytes.fromhex(dhkey) assert shared_key == bytes.fromhex(dhkey)
key_b = EccKey.from_private_key_bytes( key_b = EccKey.from_private_key_bytes(bytes.fromhex(private_B))
bytes.fromhex(private_B), bytes.fromhex(public_B_x), bytes.fromhex(public_B_y)
)
shared_key = key_b.dh(bytes.fromhex(public_A_x), bytes.fromhex(public_A_y)) shared_key = key_b.dh(bytes.fromhex(public_A_x), bytes.fromhex(public_A_y))
assert shared_key == bytes.fromhex(dhkey) assert shared_key == bytes.fromhex(dhkey)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_c1(): def test_c1(crypto_backend):
k = bytes(16) k = bytes(16)
r = reversed_hex('5783D52156AD6F0E6388274EC6702EE0') r = reversed_hex('5783D52156AD6F0E6388274EC6702EE0')
pres = reversed_hex('05000800000302') pres = reversed_hex('05000800000302')
@@ -97,7 +106,7 @@ def test_c1():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_s1(): def test_s1(crypto_backend):
k = bytes(16) k = bytes(16)
r1 = reversed_hex('000F0E0D0C0B0A091122334455667788') r1 = reversed_hex('000F0E0D0C0B0A091122334455667788')
r2 = reversed_hex('010203040506070899AABBCCDDEEFF00') r2 = reversed_hex('010203040506070899AABBCCDDEEFF00')
@@ -106,7 +115,7 @@ def test_s1():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_aes_cmac(): def test_aes_cmac(crypto_backend):
m = b'' m = b''
k = bytes.fromhex('2b7e1516 28aed2a6 abf71588 09cf4f3c') k = bytes.fromhex('2b7e1516 28aed2a6 abf71588 09cf4f3c')
cmac = aes_cmac(m, k) cmac = aes_cmac(m, k)
@@ -135,7 +144,7 @@ def test_aes_cmac():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_f4(): def test_f4(crypto_backend):
u = reversed_hex( u = reversed_hex(
'20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6' '20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6'
) )
@@ -149,7 +158,7 @@ def test_f4():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_f5(): def test_f5(crypto_backend):
w = reversed_hex( w = reversed_hex(
'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698' 'ec0234a3 57c8ad05 341010a6 0a397d9b 99796b13 b4f866f1 868d34f3 73bfa698'
) )
@@ -163,7 +172,7 @@ def test_f5():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_f6(): def test_f6(crypto_backend):
n1 = reversed_hex('d5cb8454 d177733e ffffb2ec 712baeab') n1 = reversed_hex('d5cb8454 d177733e ffffb2ec 712baeab')
n2 = reversed_hex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf') n2 = reversed_hex('a6e8e7cc 25a75f6e 216583f7 ff3dc4cf')
mac_key = reversed_hex('2965f176 a1084a02 fd3f6a20 ce636e20') mac_key = reversed_hex('2965f176 a1084a02 fd3f6a20 ce636e20')
@@ -176,7 +185,7 @@ def test_f6():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_g2(): def test_g2(crypto_backend):
u = reversed_hex( u = reversed_hex(
'20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6' '20b003d2 f297be2c 5e2c83a7 e9f9a5b9 eff49111 acf4fddb cc030148 0e359de6'
) )
@@ -190,21 +199,21 @@ def test_g2():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_h6(): def test_h6(crypto_backend):
KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b') KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
KEY_ID = bytes.fromhex('6c656272') KEY_ID = bytes.fromhex('6c656272')
assert h6(KEY, KEY_ID) == reversed_hex('2d9ae102 e76dc91c e8d3a9e2 80b16399') assert h6(KEY, KEY_ID) == reversed_hex('2d9ae102 e76dc91c e8d3a9e2 80b16399')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_h7(): def test_h7(crypto_backend):
KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b') KEY = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
SALT = bytes.fromhex('00000000 00000000 00000000 746D7031') SALT = bytes.fromhex('00000000 00000000 00000000 746D7031')
assert h7(SALT, KEY) == reversed_hex('fb173597 c6a3c0ec d2998c2a 75a57011') assert h7(SALT, KEY) == reversed_hex('fb173597 c6a3c0ec d2998c2a 75a57011')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_ah(): def test_ah(crypto_backend):
irk = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b') irk = reversed_hex('ec0234a3 57c8ad05 341010a6 0a397d9b')
prand = reversed_hex('708194') prand = reversed_hex('708194')
value = ah(irk, prand) value = ah(irk, prand)
@@ -213,7 +222,7 @@ def test_ah():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
def test_oob_data(): def test_oob_data(crypto_backend):
oob_data = OobData( oob_data = OobData(
address=Address("F0:F1:F2:F3:F4:F5"), address=Address("F0:F1:F2:F3:F4:F5"),
role=LeRole.BOTH_PERIPHERAL_PREFERRED, role=LeRole.BOTH_PERIPHERAL_PREFERRED,
@@ -237,7 +246,7 @@ def test_oob_data():
(True, '287ad379 dca40253 0a39f1f4 3047b835'), (True, '287ad379 dca40253 0a39f1f4 3047b835'),
], ],
) )
def test_ltk_to_link_key(ct2: bool, expected: str): def test_ltk_to_link_key(ct2: bool, expected: str, crypto_backend: Any):
LTK = reversed_hex('368df9bc e3264b58 bd066c33 334fbf64') LTK = reversed_hex('368df9bc e3264b58 bd066c33 334fbf64')
assert smp.Session.derive_link_key(LTK, ct2) == reversed_hex(expected) assert smp.Session.derive_link_key(LTK, ct2) == reversed_hex(expected)
@@ -250,7 +259,7 @@ def test_ltk_to_link_key(ct2: bool, expected: str):
(True, 'e85e09eb 5eccb3e2 69418a13 3211bc79'), (True, 'e85e09eb 5eccb3e2 69418a13 3211bc79'),
], ],
) )
def test_link_key_to_ltk(ct2: bool, expected: str): def test_link_key_to_ltk(ct2: bool, expected: str, crypto_backend: Any):
LINK_KEY = reversed_hex('05040302 01000908 07060504 03020100') LINK_KEY = reversed_hex('05040302 01000908 07060504 03020100')
assert smp.Session.derive_ltk(LINK_KEY, ct2) == reversed_hex(expected) assert smp.Session.derive_ltk(LINK_KEY, ct2) == reversed_hex(expected)
@@ -291,6 +300,7 @@ async def test_send_identity_address_command(
public_address: Address, public_address: Address,
random_address: Address, random_address: Address,
expected_identity_address: Address, expected_identity_address: Address,
crypto_backend: Any,
): ):
device = Device() device = Device()
device.public_address = public_address device.public_address = public_address
@@ -304,19 +314,3 @@ async def test_send_identity_address_command(
actual_command = mock_method.call_args.args[0] actual_command = mock_method.call_args.args[0]
assert actual_command.addr_type == expected_identity_address.address_type assert actual_command.addr_type == expected_identity_address.address_type
assert actual_command.bd_addr == expected_identity_address assert actual_command.bd_addr == expected_identity_address
# -----------------------------------------------------------------------------
if __name__ == '__main__':
test_ecc()
test_c1()
test_s1()
test_aes_cmac()
test_f4()
test_f5()
test_f6()
test_g2()
test_h6()
test_h7()
test_ah()
test_oob_data()